Einführung in die Windows-Programmierung mit MFC - IT

Transcription

Einführung in die Windows-Programmierung mit MFC - IT
Einführung in die
Windows-Programmierung
mit MFC
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Markus Geiger, Winfried Keim,
Oliver Kleinknecht, Markus Schuler,
Conny Weiß
Einführung in die Windows-Programmierung mit MFC
Inhaltsverzeichnis
A Inhaltsverzeichnis
A
INHALTSVERZEICHNIS
2
B
ABBILDUNGSVERZEICHNIS
7
C
TABELLENVERZEICHNIS
9
1
DAS ERSTE PROGRAMM
1.1
1.2
1.3
1.3.1
1.3.2
1.3.3
1.4
1.4.1
1.4.2
1.4.3
1.4.4
1.4.5
1.5
1.5.1
1.5.2
2
10
WINDOWS 3.X, WINDOWS 95, WINDOWS NT, WINDOWS CE UND DIE WIN32-API
DATENTYPEN
RESSOURCE-DATEIEN
BEISPIEL FÜR EINE RESSOURCE-DATEI
PRÄPROZESSOR ANWEISUNGEN INNERHALB EINER RESSOURCE-DATEI
DEFINITION VON BEDIENELEMENTEN EINER BENUTZEROBERFLÄCHE
„HELLO, WORLD“ ALS WIN32-API PROGRAMM
DIE FUNKTION WINMAIN
REGISTRIERUNG EINER FENSTERKLASSE
ERZEUGEN EINES FENSTERS
NACHRICHTENKONZEPT
VERSENDEN VON NACHRICHTEN INNERHALB EINER ANWENDUNG
HELLO3.EXE ALS ZUSAMMENFASSUNG DER BISHERIGEN KAPITEL
DIALOGE MIT DER WIN32-API
ABLAUFBESCHREIBUNG VON HELLO3.EXE
DIE ENTWICKLUNGSUMGEBUNG
2.1 PROJEKTFENSTER
2.1.1 KLASSENBROWSER
2.1.1.1 Eine neue Klasse einfügen
2.1.1.2 Eine neue Methode einfügen
2.1.1.3 Ein neues Attribut einfügen
2.1.2 RESSOURCE-EDITOR
2.1.2.1 Dialog-Editor
2.1.2.2 Menü-Editor
2.1.2.3 Bitmap-Editor
2.1.2.4 String Table - Editor
2.1.3 DER DATEI-BROWSER
2.2 AUSGABE-FENSTER
2.3 DER APPLICATION-WIZARD
2.4 KLASSEN-ASSISTENT
10
11
12
14
15
16
18
19
22
23
24
29
30
35
37
39
39
40
40
40
41
42
43
45
47
48
49
50
50
50
Seite 2
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Inhaltsverzeichnis
2.4.1 NACHRICHTENZUORDNUNGSTABELLE
2.4.2 MEMBERVARIABLEN
2.5 DIE ONLINE-HILFE
51
54
55
3
56
EINFÜHRUNG IN DAS MFC-ANWENDUNGSGERÜST
3.1 WAS IST MFC ?
3.2 EIN KURZER ÜBERBLICK ÜBER DIE REVISIONSGESCHICHTE DER MFC
3.3 DAS ERSTE MFC-PROGRAMM: „HELLO MFC !!“
3.3.1 SCHRITT 1: ANLEGEN DES PROJEKTS
3.3.2 SCHRITT 2: FUNKTIONALITÄT HINZUFÜGEN – AUSGABE IN DAS ANSICHTSFENSTER
56
57
58
60
66
4
70
NACHRICHTENVERARBEITUNG MIT DER MFC
4.1 MAKROS ZUR NACHRICHTENVERARBEITUNG
4.1.1 BENUTZERDEFINIERTE NACHRICHTEN
71
73
5
76
CONTROLS
5.1
5.2
5.3
5.4
5.5
5.5.1
5.5.2
5.6
5.7
5.8
5.9
5.9.1
5.9.2
6
GRUNDGERÜST
CSTATIC
CEDIT
CBUTTON
CLISTBOX
GANZ ALLGEMEIN
BENUTZERDEFINIERTE LISTBOX
CCOMBOBOX
CTREECTRL
CTABCTRL
MENÜS
DAS HAUPTMENÜ
EIN POPUP-MENÜ
77
79
82
87
92
92
99
100
105
109
110
110
113
115
DIALOGE
6.1 MODALE DIALOGE
6.1.1 DER DIALOG-DATEN-AUSTAUSCH
6.1.2 EIN MODALER DIALOG STARTEN
6.1.3 NACHRICHTENTABELLE IN EINEM DIALOG
6.1.4 INITIALISIEREN EINES DIALOGS.
6.1.5 DER DIREKTE ZUGRIFF AUF STEUERELEMENTE
6.1.6 ZUWEISUNG EINER ABGELEITETEN KLASSE AN EIN STEUERELEMENT (SUBCLASSING)
6.1.7 WAS GESCHIEHT ZUM SCHLUß
6.2 NICHT MODALE DIALOGE
6.2.1 EIN NICHT MODALER DIALOG STARTEN
6.3 DIALOGBASIERTE ANWENDUNG
115
117
118
119
120
121
122
123
123
124
125
7
127
7.1
DOKUMENTE UND ANSICHTEN
EIN DOKUMENT + EINE ANSICHT = SINGLE DOCUMENT INTERFACE
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
127
Seite 3
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Inhaltsverzeichnis
7.1.1 DOKUMENT/VIEW – DAS FUNDAMENT
7.1.2 DIE ÜBERARBEITETE INITINSTANCE-FUNKTION
7.1.3 DAS DOKUMENTOBJEKT
7.1.4 DAS ANSICHT-OBJEKT (VIEW-OBJECT)
7.1.5 DAS RAHMENFENSTER-OBJEKT
7.1.6 DYNAMISCHE OBJEKTERZEUGUNG
7.1.7 MEHR ÜBER DAS SDI-DOKUMENTENTEMPLATE
7.1.8 REGISTRIERUNG VON DOKUMENT-TYPEN BEI DER SHELL DES BETRIEBSYSTEMS
7.1.9 KOMMANDO-ROUTING (COMMAND ROUTING)
7.1.10 VORDEFINIERTE KOMMANDO-ID‘S UND DEFAULT-KOMMANDO-IMPLEMENTATIONEN
7.1.11 DIE ERSTE DOCUMENT/VIEW-ANWENDUNG – PAINT
7.1.11.1 Das Programm
7.1.11.2 Die Klasse CPaintApp
7.1.11.3 Die Klasse CMainFrame
7.1.11.4 Die Klasse CPaintDoc
7.1.11.5 Die Klasse CPaintView
7.1.11.6 Die Klasse Cline
7.2 MEHR ANSICHT
7.2.1 MEHRERE ANSICHTEN – SPLITTER-WINDOWS
7.2.2 DYNAMISCHE SPLITTER-WINDOWS ERZEUGEN
7.2.3 DAS LINKEN DER VIEWS
7.2.4 SPLITTED PAINT
7.2.5 STATISCHE SPLITTER-WINDOWS
7.2.6 THREE-WAY SPLITTER-WINDOWS
7.2.7 CSPLITTERWND SELBST GEKOCHT (CSPLITTERWND ABLEITEN)
7.2.8 SPLITTER-WINDOWS IN MDI-ANWENDUNGEN
7.3 MEHR DOKUMENT
7.3.1 MFC – UND DAS MDI-INTERFACE
7.3.2 ALTERNATIVEN ZU MDI
7.3.3 ANHANG ZU DOKUMENTE UND ANSICHTEN
128
131
133
137
140
141
143
145
147
150
151
152
152
154
154
155
155
156
156
158
160
162
163
164
165
166
166
166
169
170
8
171
DATEIEN UND SERIALISIERUNG
8.1 DAS DATEISYSTEM UND MFC
8.2 AUSNAHMEN IM DATEIGESCHÄFT– CFILEEXCEPTION
8.3 SERIALISIERUNG
8.3.1 WAS IST SERIALISIERUNG
8.3.2 MFC UND SERIALISIERUNG
8.3.2.1 CObject – die Mutter aller Klassen
8.3.2.2 CObject und die Serialisierung
8.3.3 EINE APPLIKATION SERIALISIERT IHRE DATEN
171
172
175
175
176
176
176
177
9
182
MULTITHREADING
9.1 KEINE ANGST VOR THREADS!
9.1.1 WAS SIND THREADS?
9.1.2 BESONDERHEITEN DES MULTITHREADING UNTER WIN32
9.1.3 ZUSAMMENFASSUNG
9.2 WOZU THREADS ?
9.3 DAS ERSTE MFC – MULTITHREADING-PROGRAMM
9.3.1 ERZEUGUNG VON THREADS
9.3.2 PROGRAMMIERUNG VON MULTITHREADING
9.3.3 INTER-THREAD-KOMMUNIKATION UND SYNCHRONISIERUNG
182
182
183
183
184
185
185
185
191
Seite 4
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Inhaltsverzeichnis
9.3.3.1 Probleme beim gemeinsamen Zugriff auf Systemressourcen
9.3.3.2 Wie kommt ein Thread in den Besitz eines Synchronisierungsobjektes ?
9.3.3.3 Wie gibt ein Thread ein Synchronisierungsobjekt wieder frei ?
9.3.3.4 Vorteile von CSingleLock und CMultiLock
9.3.3.5 Unterschied zwischen CCriticalSection, CMutex und CSemaphore
9.3.3.6 Zusammenfassung
9.3.3.7 Ereignisse als Mittel der Inter-Thread-Kommunikation
9.3.4 FORTSETZUNG DES BEISPIELS PRIMZAHLENGENERATOR
9.3.5 BEENDEN DES ARBEITS-THREADS
9.3.5.1 Die endgültige Version der Threadfunktion
9.3.6 THREAD-PRIORITÄTEN
9.4 BENUTZEROBERFLÄCHENTHREADS: NICHT NUR AN DER OBERFLÄCHE
193
194
195
195
196
196
197
197
199
201
203
205
10
210
EINFÜHRUNG IN DIE SPEICHERVERWALTUNG VON WIN32
10.1 VIRTUELLE ADRESSRÄUME
10.1.1 ABBILDUNG VON VIRTUELLEM AUF PHYSISCHEN SPEICHER
10.1.2 AUFTEILUNG DES VIRTUELLEN SPEICHERS UNTER WINDOWS 95
10.1.3 AUFTEILUNG DES VIRTUELLEN SPEICHERS UNTER WINDOWS NT
10.2 VERWENDEN VON VIRTUELLEM SPEICHER IN EIGENEN ANWENDUNGEN
10.2.1 DIREKTE ANFORDERUNG VON SEITEN DES VIRTUELLEN ADRESSRAUMES
10.2.1.1 Unterschiede zwischen reserviertem und belegtem Speicher
10.2.1.2 Zugriffsrechte auf Speicherseiten
10.2.2 FREIGEBEN VON SPEICHERSEITEN: VIRTUALFREE
10.2.3 SPEICHERBASIERTE DATEIEN (MEMORY MAPPED FILES)
10.2.3.1 Schritt 1: Erzeugen oder Öffnen des Dateiobjekts
10.2.3.2 Schritt 3: Abbilden des Dateiinhalts in den Adressraum
10.2.3.3 Schritt 4: Entfernen der Dateiansicht aus dem Adressraum
10.2.3.4 Schritt 5 und 6: Schließen des Dateiabbildungsobjekts und des Dateiobjekts
10.2.4 WIN32 – HEAPS
10.2.4.1 Erzeugen eines zusätzlichen Heaps
10.2.4.2 Allokieren von Speicher auf einem zusätzlichen Heap
10.2.4.3 Freigabe eines Speicherbereichs
10.2.4.4 Abbau eines zusätzlichen Heaps
10.2.4.5 Verwendung von zusätzlichen Heaps in C++
210
211
213
214
215
215
216
216
217
218
219
221
223
223
224
225
225
226
227
227
11
229
DYNAMISCHE BIBLIOTHEKEN
11.1
11.2
11.3
11.4
11.5
11.5.1
11.5.2
11.5.3
11.6
11.6.1
11.6.2
11.6.3
11.6.4
11.6.5
11.7
GRUNDLAGEN
IMPLIZITE UND EXPLIZITE BINDUNG
SUCHREIHENFOLGE BEI DER DYNAMISCHEN BINDUNG
DLL-TYPEN
INITIALISIEREN EINER DLL
INITIALISIEREN EINER WIN32-DLL
INITIALISIEREN EINER NORMALEN DLL, DIE MFC VERWENDET
INITIALISIERUNG EINER MFC-ERWEITERUNGS-DLL
BEISPIEL 1: EINE NORMALE DLL, DIE MFC VERWENDET
ERSTER SCHRITT: ANLEGEN DES PROJEKTS
ZWEITER SCHRITT: DEKLARIEREN DER EXPORTIERTEN FUNKTIONEN
DRITTER SCHRITT: IMPLEMENTATION DER FIFO-QUEUE-KLASSE
VIERTER SCHRITT: IMPLEMENTATION DER EXPORTIERTEN FUNKTIONEN
DER CLIENT
BEISPIEL 2: EINE MFC-ERWEITERUNGS-DLL
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
229
231
232
232
232
232
233
234
234
234
236
236
238
239
241
Seite 5
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Inhaltsverzeichnis
11.7.1
11.7.2
11.7.3
12
SCHRITT 1: ANLEGEN DES PROJEKTS „PRIMEGEN“
SCHRITT 2: EINBINDEN UND ANPASSEN DER VORHANDENEN KLASSE
SCHRITT 3: ANPASSEN DES CLIENT-PROJEKTS
241
242
242
ENTWICKLUNG VON KOMPONENTENSOFTWARE MIT MFC
243
12.1
12.2
12.3
12.3.1
12.3.2
12.3.3
12.3.4
12.3.5
12.4
12.5
12.5.1
12.5.2
12.5.3
12.5.4
12.5.5
12.6
12.6.1
12.6.2
12.6.3
12.6.4
12.6.5
12.6.6
12.7
13
13.1
WAS IST KOMPONENTENSOFTWARE ?
DIE TECHNOLOGIE
GRUNDLAGEN
OBJEKTE
EINDEUTIGE IDENTIFIZIERUNG VON OBJEKTEN UND INTERFACES MITTELS GUIDS
DAS INTERFACE, DIE STANDARDISIERTE SCHNITTSTELLE
DAS BASISINTERFACE IUNKNOWN
ERZEUGEN VON OBJEKTEN
WIEDERVERWENDUNG DURCH AGGREGATION
IMPLEMENTIEREN EINES COM-OBJEKTES MIT MFC
DEFINITION DES INTERFACES
IMPLEMENTIEREN DER FUNKTIONALITÄT DES OBJEKTES
KLASSENFABRIK DES OBJEKTES
VERWENDUNG DES OBJEKTES DURCH EINEN CLIENT
AGGREGATION MIT MFC
ZUGRIFF AUF OBJEKTE MITTELS OLE AUTOMATION
DAS INTERFACE IDISPATCH
DATENTYPEN VON OLE AUTOMATION
IMPLEMENTIEREN EINES OLE AUTOMATION OBJEKTES MIT MFC
KLASSENFABRIK BEI VERWENDUNG DER DOCUMENT/VIEW ARCHITEKTUR
VERWENDEN DES OBJEKTES DURCH EINEN VISUAL BASIC CLIENT
VERWENDEN DES OBJEKTES DURCH EINEN C++ CLIENT
ZUSAMMENFASSUNG
ANHANG
BÜCHERLISTE
243
243
244
244
245
245
246
248
251
251
252
253
258
261
262
264
264
266
269
273
276
276
280
282
282
Seite 6
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Abbildungsverzeichnis
B Abbildungsverzeichnis
ABBILDUNG 1: COMPILER UND LINKER.................................................................................................13
ABBILDUNG 2: DIALOG DER RESSOURCE-DATEI .................................................................................14
ABBILDUNG 3: MENÜ FILE........................................................................................................................17
ABBILDUNG 4: MENÜ EDIT .......................................................................................................................17
ABBILDUNG 5: HELLO1.EXE .....................................................................................................................19
ABBILDUNG 6: HELLO2.EXE .....................................................................................................................19
ABBILDUNG 7: HELLO3.EXE .....................................................................................................................31
ABBILDUNG 8: DIE ENTWICKLUNGSUMGEBUNG.................................................................................39
ABBILDUNG 9: DIALOG ZUM ERZEUGEN EINER NEUEN KLASSE ......................................................40
ABBILDUNG 10: DIALOG FÜR EINE NEUE MEMBER-FUNKTION.........................................................41
ABBILDUNG 11: DIALOG FÜR EIN NEUES MEMBER-ATTRIBUT..........................................................41
ABBILDUNG 12: RESSOURCE-AUSWAHL ................................................................................................42
ABBILDUNG 13: DIE ENTWICKLUNGSUMGEBUNG MIT DEM GEÖFFNETEN DIALOG-EDITOR......43
ABBILDUNG 14: PALETTE DER STEUERELEMENTE..............................................................................43
ABBILDUNG 15: EINSTELLUNG DER DIALOG-EIGENSCHAFTEN ........................................................44
ABBILDUNG 16: TAB-REIHENFOLGE FESTLEGEN.................................................................................44
ABBILDUNG 17: MENÜ-EDITOR................................................................................................................45
ABBILDUNG 18: MENÜBEFEHL EIGENSCHAFTEN.................................................................................45
ABBILDUNG 19: WEITERE MENÜPUNKTE ..............................................................................................46
ABBILDUNG 20: EIGENSCHAFTEN FÜR EINEN MENÜPUNKT..............................................................46
ABBILDUNG 21: BITMAP-EDITOR.............................................................................................................47
ABBILDUNG 22: BITMAP EIGENSCHAFTEN............................................................................................47
ABBILDUNG 23: STRING TABLE - EDITOR ..............................................................................................48
ABBILDUNG 24: STRING TABLE EIGENSCHAFTEN ..............................................................................48
ABBILDUNG 25: DATEI-BROWSER ...........................................................................................................49
ABBILDUNG 26: DAS AUSGABE-FENSTER ..............................................................................................50
ABBILDUNG 27: SUCHEN IN DATEIEN.....................................................................................................50
ABBILDUNG 28: DER KLASSEN-ASSISTENT FÜR DIE NACHRICHTENZUORDNUNGSTABELLE.....51
ABBILDUNG 29: HINZUFÜGEN EINER NEUEN MEMBER-FUNKTION..................................................52
ABBILDUNG 30: EINE NEUE KLASSE HINZUFÜGEN..............................................................................53
ABBILDUNG 31: MEMBER-VARIABLE .....................................................................................................54
ABBILDUNG 32: MEMBER-VARIABLE HINZUFÜGEN............................................................................54
ABBILDUNG 33: INFO-VIEWER .................................................................................................................55
ABBILDUNG 34: AUSWAHL VON PROJEKTTYP, -PFAD UND –NAME..................................................60
ABBILDUNG 35: FESTLEGEN DER ART DER ANWENDUNG .................................................................61
ABBILDUNG 36: EINBINDEN VON DATENBANKUNTERSTÜTZUNG ...................................................61
ABBILDUNG 37: UNTERSTÜTZUNG FÜR VERBUNDDOKUMENTE UND ACTIVEX ...........................62
ABBILDUNG 38: FESTLEGEN VON GRAFISCHEN MERKMALEN UND WEITEREN OPTIONEN ........62
ABBILDUNG 39: KOMMENTARGENERIERUNG UND MFC-LINKER-EINSTELLUNG ..........................63
ABBILDUNG 40: ÜBERSICHT ÜBER DIE AUTOMATISCH ERSTELLTEN KLASSEN............................63
ABBILDUNG 41: ZUSAMMENFASSUNG ...................................................................................................64
ABBILDUNG 42: VOM ANWENDUNGSASSISTENTEN ERZEUGTE MFC-ANWENDUNG ....................66
ABBILDUNG 43: EIN STATISCHES TEXTFELD ........................................................................................82
ABBILDUNG 44: VERSCHIEDENE EDITFELDER (EINZEILIGES, ALS PASSWORTEINGABE,
MEHRZEILIG UND RECHTSBÜNDIG) ..............................................................................................86
ABBILDUNG 45: VERSCHIEDENE BUTTONARTEN (STANDARD, CHECKBOX UND RADIOBUTTON)
..............................................................................................................................................................91
ABBILDUNG 46: MESSAGEBOX NACH SELEKTIEREN ..........................................................................98
ABBILDUNG 47: MESSAGEBOX NACH DOPPELKLICK.........................................................................98
ABBILDUNG 48: LISTBOX MIT SORTIERTEN NAMEN ...........................................................................99
ABBILDUNG 49: EINE GEÖFFNETE COMBOBOX ..................................................................................102
ABBILDUNG 50: TREE-CONTROL ...........................................................................................................108
ABBILDUNG 51: UNTERMENÜ ................................................................................................................111
ABBILDUNG 52: POPUP-MENÜ................................................................................................................114
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 7
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Abbildungsverzeichnis
ABBILDUNG 53: MODALER DIALOG...................................................................................................... 116
ABBILDUNG 54: DER KLASSEN-ASSISTENT......................................................................................... 116
ABBILDUNG 55: NICHT MODALER DIALOG ......................................................................................... 124
ABBILDUNG 56: DIALOGBASIERTE ANWENDUNG ............................................................................. 125
ABBILDUNG 57: AUTOMATISCHE SICHERHEITSABFRAGE ............................................................... 128
ABBILDUNG 58: DIE DOKUMENT/ANSICHT-ARCHITEKTUR EINER SDI-ANWENDUNG ................ 129
ABBILDUNG 59: DIALOG „ÖFFNEN“...................................................................................................... 144
ABBILDUNG 60: KONTEXTMENÜPUNKTE „ÖFFNEN“ UND „DRUCKEN“ IM EXPLORER ............... 146
ABBILDUNG 61: ROUTING EINER AN EIN SDI-RAHMENFENSTER GESENDETEN KOMMANDONACHRICHT...................................................................................................................................... 149
ABBILDUNG 62: RECENT FILE LIST IN DER REGISTRY ...................................................................... 153
ABBILDUNG 63: SDI-ANWENUNG MIT DYNAMISCHEM SPLITTER-WINDOW................................. 157
ABBILDUNG 64: SDI-ANWENDUNG MIT STATISCHEM SPLITTER-WINDOW................................... 158
ABBILDUNG 65: SPLITTER-BAR EINES DYNAMISCHEN SPLITTER-WINDOW................................. 158
ABBILDUNG 66: PHANTOM-SPLITTER-BAR.......................................................................................... 160
ABBILDUNG 67: DIE MDI DOKUMENT/ANSICHT-ARCHITEKTUR ..................................................... 167
ABBILDUNG 68: FENSTERMENÜ EINER MDI-ANWENDUNG.............................................................. 168
ABBILDUNG 69 SERIALISIERUNG AM BEISPIEL STUDENT ............................................................... 175
ABBILDUNG 70: ZUSTANDSDIAGRAMM EINES THREADS................................................................. 182
ABBILDUNG 71: OBJEKTMODEL DES BEISPIELS MULTITHREAD1.EXE .......................................... 186
ABBILDUNG 72: KLASSE CPRIMEGENERATOR.................................................................................... 187
ABBILDUNG 73: ERSTELLEN DER BENUTZEROBERFLÄCHE FÜR MULTITHRED1.EXE ................ 190
ABBILDUNG 74: MULTITHREAD2 IN AKTION ...................................................................................... 206
ABBILDUNG 75: EINE VON CWINTHREAD ABGELEITETE KLASSE ERZEUGEN ............................. 207
ABBILDUNG 76: UMSETZUNG VON VIRTUELLEN IN PHYSISCHE ADRESSEN (INTEL CPU) ......... 211
ABBILDUNG 77: ABLAUF BEIM ZUGRIFF AUF EINE VIRTUELLE ADRESSE.................................... 212
ABBILDUNG 78: ANLEGEN DES DLL-PROJEKTS.................................................................................. 234
ABBILDUNG 79: FESTLEGEN DES DLL-TYPS........................................................................................ 235
ABBILDUNG 80: VOM CODEGENERATOR ERSTELLTES RAHMENPROJEKT DER DLL................... 235
ABBILDUNG 81: FIFOCLIENT1.EXE IN AKTION ................................................................................... 239
ABBILDUNG 82: AUFNEHMEN DER SURROGAT-BIBLIOTHEK IN DIE LINKER-LISTE ................... 240
ABBILDUNG 83: OBJEKT-NOTATION..................................................................................................... 244
ABBILDUNG 84: MÖGLICHE DARSTELLUNG EINER SOFTWAREKOMPONENTE............................ 245
ABBILDUNG 85: AUFBAU DER STANDARDISIERTEN SCHNITTSTELLE........................................... 246
ABBILDUNG 86: CLASS IDENTIFIER DER KLASSE TGAME.DOCUMENT UNTER
HKEY_CLASSES_ROOT..................................................................................................................... 249
ABBILDUNG 87: VERWEIS AUF BINÄRE PROGRAMMEINHEIT DES OBJEKTES UNTER
HKEY_CLASSES_ROOT\CLSID\7A5E3B51-9383-11D2-9C5F-BB4B45DC5501................... 249
ABBILDUNG 88: INSTANZIEREN EINES OBJEKTES ............................................................................. 250
ABBILDUNG 89: WIEDERVERWENDUNG DURCH AGGERATION ...................................................... 251
ABBILDUNG 90: THEOBJECT ................................................................................................................... 252
ABBILDUNG 91: THEOBJECTAGT ............................................................................................................ 262
ABBILDUNG 92: SPÄTES BINDEN DURCH IDISPATCH ........................................................................ 265
ABBILDUNG 93: SDI-ANWENDUNG, DIE DISPOBJECT BEINHALTET................................................ 273
ABBILDUNG 94: OBERFLÄCHE DES C++ CLIENTS............................................................................... 278
Seite 8
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Tabellenverzeichnis
C Tabellenverzeichnis
TABELLE 1: WINDOWS-DATENTYPEN
TABELLE 2: WINDOWS NACHRICHTEN FÜR DEN CLIENT-BEREICH
TABELLE 3: WINDOWS-NACHRICHTEN FÜR DEN NICHT-CLIENT BEREICH
TABELLE 4: VOM ANWENDUNGSASSISTENTEN GENERIERTE DATEIEN
TABELLE 5: STYLES DES STATIC-CONTROLS
TABELLE 6: STYLES DES EDIT-CONTROLS
TABELLE 7: BENACHRICHTIGUNGEN DES EDIT-CONTROLS
TABELLE 8: STYLES DES BUTTON-CONTROLS
TABELLE 9: BENACHRICHTIGUNGEN DES BUTTON-CONTROLS
TABELLE 10: STYLES DES LISTBOX-CONTROLS
TABELLE 11: BENACHRICHTIGUNGEN DES LISTBOX-CONTROLS
TABELLE 12: STYLES DES COMBOBOX-CONTROLS
TABELLE 13: BENACHRICHTIGUNGEN DES COMBOBOX–CONTROLS
TABELLE 14: STYLES DES TREE-CONTROLS
TABELLE 15: BENACHRICHTIGUNGEN DES TREE-CONTROLS
TABELLE 16: STYLES DES TAB-CONTROLS
TABELLE 17: BENACHRICHTIGUNGEN DES TAB-CONTROLS
TABELLE 18: WICHTIGE MEMBERFUNKTIONEN VON CDOCUMENT
TABELLE 19: DIE WICHTIGSTEN ÜBERSCHREIBAREN FUNKTIONEN VON CDOCUMENT
TABELLE 20: ABGELEITETE ANSICHTSKLASSEN DER MFC
TABELLE 21: DIE WICHTIGSTEN, ÜBERSCHREIBBAREN FUNKTIONEN
TABELLE 22: VORDEFINIERTE KOMMANDO-ID‘S UND NACHRICHTEN-HANDLER
TABELLE 23: MAKROS FÜR INTERFACES
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
12
27
28
65
80
82
83
87
88
92
93
101
102
105
105
109
110
134
136
138
139
170
256
Seite 9
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
1 Das erste Programm
1.1 Windows 3.x, Windows 95, Windows NT, Windows CE und die
Win32-API
Windows 3.x, auch 16-Bit Windows genannt,
repräsentiert eine graphische
Benutzeroberfläche mit einigen Betriebssystem-typischen Eigenschaften, die auf MSDOS aufsetzt. Die Kombination aus DOS- und Windows-System besitzt eine
angeborene Stabilitätsschwäche gegenüber sich fehlerhaft verhaltenden
Anwendungen. Win32s ist wiederum eine Erweiterung von Windows 3.x, welche die
32-Bit Schnittstelle der Win32-API auf die darrunterliegende 16-Bit Schicht von
Windows 3.x abbildet. Wobei die API auf die Leistung von 16-Bit Windows speziell
angepasst wurde.
Windows NT (New Technology) stellt das Flaggschiff von Microsofts
Betriebssystemen dar. Dabei handelt es sich um ein relativ neues, von MS-DOS
unabhängiges
32-Bit
Betriebssystem
mit
integrierter
graphischer
Benutzerschnittstelle und der Fähigkeit, als Server eingesetzt zu werden. Bei seiner
Entwicklung standen die Ziele von maximaler Portabilität, Stabilität und Sicherheit im
Vordergrund. Aufgrund seiner hohen Robustheit gegenüber fehlerträchtigen
Anwendungen eignet es sich hervorragend als Entwicklungsplattform.
Windows 95 ist der direkte Nachfolger von Windows 3.x. Es wurde mit dem
Hintergedanken der Abwärtskompatibilität zu Windows 3.x entwickelt und erbte
diesbezüglich doch eine beträchtliche Menge dessen Codes. Als 32-Bit System
bietet es einen Auszug des Besten seiner beiden Konkurrenten. Zum einen sind
dessen Hardwareanforderungen vergleichbar mit denen von Windows 3.x und seine
Stabilität kann durchaus mit der von Windows NT konkurrieren.
Windows CE ist die neueste Entwicklung der Windows Plattformen von Microsoft.
Wobei sich CE nicht auf ein bestimmtes Konzept dieses Betriebsystems bezieht,
sondern vielmehr eine Umschreibung der Windows CE Design Regeln, wie
Kompatibilität und Kompaktheit darstellt. Windows CE ist vor allem für Notepads
entwickelt worden und bietet eine 32-Bit Umgebung, die speziell an die
Hardwareanforderungen solcher Geräte angepasst wurde. Trotz der gegebenen
Hardwareanforderungen ist Windows CE leistungsstärker als Windows 3.x oder MSDOS.
Seite 10
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Win32 ist lediglich der Name einer 32-Bit basierenden Programmierschnittstelle (API
= Application Programming Interface) für die oben aufgeführten Betriebssysteme. Die
Win32-API besitzt Funktionen für die Verwaltung von Kernel-Diensten (Prozesse,
Speicher, Threads), der Benutzerschnittstelle (Fenster) und für Grafik- und
Textausgaben.
Die Win32-API wurde mehr oder weniger vollständig für alle oben genannten 32-Bit
Plattformen implementiert. So liefert beispielsweise die Funktion CreateThread für
Win32s einen Handle auf NULL zurück, da Threads von 16-Bit Windows nicht
unterstützt werden. Die vollständigste Implementierung der Win32-API ist in Windows
NT vorzufinden.
1.2 Datentypen
Die Windows-API besitzt ihre eigenen Datentypen. Um sie von den C - Datentypen
unterscheiden zu können, werden diese in großen Buchstaben geschrieben. Ein
char in der Windows-API wird demzufolge als CHAR geschrieben, ein unsigned
int als UINT. Pointer besitzen das Präfix P bzw. LP. Ein Pointer auf void wird in
der Windows-Programmierung als PVOID oder LPVOID definiert. Wobei in 32-Bit
Windows, aufgrund des unterschiedlichen Speicheraufbaus zu 16-Bit Windows, nicht
mehr unterschieden wird zwischen P* und LP* („Langer Pointer“). Die Zeiger mit
dem Präfix LP dienen rein der Abwärtskompatibilität zu 16-Bit Windows, falls der
Programmcode doch noch für unterschiedliche Windows Plattformen übersetzt
werden muss.
Generell können die Windows-API Datentypen mit den C - Datentypen gemischt
werden. Viele Windows-API Datentypen wie Character und Integer sind zudem mit
den gleichnamigen Datentypen der meisten C - Compiler identisch.
Grundsätzlich sollten die Datentypen der Windows-API verwendet werden, wenn
eine Kompatibilität zwischen 16-Bit und 32-Bit Windows existieren muss. Sollen zum
Beispiel Daten einer Diskette von 16-Bit und 32-Bit Windows lesbar sein, wäre es
falsch für eine Zahl den int – Datentypen zu verwenden, der maschinenabhängig ist
(sizeof(int) == 2 für 16-Bit, sizeof(int) == 4 für 32-Bit). Statt dessen ist
es sinnvoller WORD und DWORD (double word) zu verwenden, die eindeutig die Länge
des Datentyps auf 16-Bit bzw. 32-Bit festlegen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 11
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Die wichtigsten Windows-Datentypen sind:
Tabelle 1: Windows-Datentypen
BOOL
BYTE
COLORREF
DWORD
INT
LONG
LPARAM
LPCTSTR
LPSTR
LPVOID
LRESULT
UINT
WNDPROC
WORD
WPARAM
Boolean mit den Werten TRUE und FALSE
vorzeichenloser 8-Bit Wert
32-Bit Wert zur Darstellung von Zeichen
32-Bit breite, vorzeichenlose ganze Zahl
vorzeichenbehaftete ganze Zahl (16-Bit breit bei 16-Bit Windows,
32-Bit breit bei 32-Bit Windows)
vorzeichenbehaftete ganze Zahl von 32-Bit Breite
32-Bit breiter Übergabeparameter diverser Windows-typischer
Funktionen
Zeiger auf konstante Zeichenkette
Zeiger auf Zeichenkette
Zeiger vom Typ void
32-Bit breiter Rückgabewert diverser Windows-typischer
Funktionen
vorzeichenlose ganze Zahl (16-Bit breit bei 16-Bit Windows, 32-Bit
breit bei 32-Bit Windows)
ein Zeiger auf eine Funktion zur Nachrichtenverarbeitung
16-Bit breite, vorzeichenlose ganze Zahl
Übergabeparameter diverser Windows-typischer Funktionen. Bei
16-Bit Windows noch 16-Bit breiter Wert (WPARAM , W wie Wort).
Bei 32-Bit Windows leider 32-Bit breit und nicht 16-Bit.
Zahlreiche Datentypen in Windows werden als Handles bezeichnet. Handles sind
nichts anderes als Verweise auf Windows-Objekte, wie Fenster, Ressourcen oder
Speicherbereiche im Arbeitsspeicher. Diese Verweise erlauben aber keinen Zugriff
auf diese Datenobjekte und sind somit keine Zeiger. Ein Window-Handle (HWND)
verweist zum Beispiel auf ein Window-Objekt, das näheren Aufschluss über die
Eigenschaften und das Aussehen des Fensters gibt. Ein Window-Handle dient
zudem als eindeutiger Identifikator für ein erzeugtes Fenster innerhalb von Windows.
Grundsätzlich beginnen alle Handle-Datentypen mit einem großen H. Ein Handle für
ein Icon (bei einem Icon handelt es sich um eine Datenstruktur, die ein kleines Bild
beschreibt, das beispielsweise an der linken oberen Ecke eines Fensters erscheint)
besitzt die Bezeichnung HICON. HANDLE beschreibt ein allgemeines Objekt und ist
nicht typisiert wie HICON oder HMENU (Handle für ein Menü).
1.3 Ressource-Dateien
Mittels Ressource-Dateien können die Bedienelemente einer Benutzeroberfläche
beschrieben werden. So enthalten Ressource-Dateien die Definitionen von
Menüleisten, Dialogen, Buttons und Textfelder in einem übersichtlichen,
strukturierten und lesbaren Dateiformat. Ressource-Dateien können entweder von
Hand erstellt oder mit Hilfe von visuellen Ressource-Editoren, wie beispielsweise der
Dialog-Editor von Visual C++.
Seite 12
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Die Verwendung von Ressource-Dateien ist kein Muss, es wäre jedoch falsch und
äußerst umständlich, die Bedienelemente einer graphischen Benutzeroberfläche
durch Programmcode zu erzeugen. Der Komfort, den die graphischen bzw. visuellen
Ressource-Editoren heutzutage bieten, lässt ein manuelles Erstellen einer
Ressource-Datei völlig unnötig erscheinen. Die zusätzliche Zeiteinsparung, die mit
diesen Werkzeugen erreicht wird, ist zudem ein enormer Vorteil, der dem
Programmierer geboten wird.
Bevor die Information einer Ressource-Datei von einer Anwendung verwendet
werden kann, muss sie zuerst kompiliert werden. Für das Kompilieren der
Ressource-Datei ist ein spezieller Ressource-Compiler verantwortlich. Das Kompilat
dieses Compilers wird dann letztendlich vom Linker der EXE- oder DLL-Datei
hinzugefügt. Abbildung 1 zeigt den Entstehungsprozess einer Windows-Anwendung
unter Verwendung von Microsoft – Compilern:
Quelldateien (.c / .cpp)
Definitionsdateien (.h / .hpp)
Ressourcen-Datei (.rc)
Ressourcen (.bmp, .ico, ...)
Ressourcen-Compiler(rc.exe)
Compiler (cl.exe)
Objektdateien (.obj)
statische Bibliotheken (.lib)
Binäre Ressourcedatei (.res)
Linker (link.exe)
Ausführbare Datei (.exe, .dll, .ocx)
Abbildung 1: Compiler und Linker
Im folgenden wird nur kurz auf die Komponenten einer Ressource-Datei
eingegangen, um ein Grundverständnis für solche Dateien zu bekommen. Deren
Syntax ist sehr einfach gehalten und leicht zu erlernen. Bei der Verwendung von
Visual C++ oder anderer graphischer GUI-Builder entsteht sowieso der Eindruck,
eine Ressource-Datei sei gar nicht vorhanden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 13
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
1.3.1 Beispiel für eine Ressource-Datei
Folgendes Beispiel zeigt eine einfache Ressource-Datei die einen Dialog definiert,
der einen „OK“-Button und ein Textfeld enthält:
#include <windows.h>
DlgBox DIALOG 20, 20, 90, 50
STYLE DS_MODALFRAME | WS_SYSMENU
CAPTION "Dialog"
BEGIN
DEFPUSHBUTTON "&OK", IDOK, 19, 30, 52, 14, WS_GROUP
CTEXT "Hello, World!", -1, 0, 8, 90, 8
END
Abbildung 2: Dialog der Ressource-Datei
Auf die einzelnen Elemente der obigen Ressourcen-Datei wird später noch näher
eingegangen. Zuerst wird erläutert, wie mit einer solchen Ressourcen-Definition
innerhalb des Programmcodes umgegangen wird.
Im Programmcode wird der Bezug zur Ressource durch den Textstring „DlgBox“
hergestellt. Beim Erzeugen des Dialoges wird dieser Textstring der entsprechenden
Win32-API Funktion übergeben (die weiteren Parameter der Funktion DialogBox
sollen vorerst außer acht gelassen werden):
DialogBox( ghInstance, "DlgBox", hWnd, (DLGPROC)DlgProc );
Es besteht zusätzlich noch die Möglichkeit, die Ressource anhand eines
numerischen Wertes zu identifizieren. Mit Hilfe des Makros MAKEINTRESOURCE
erfolgt dann die Konvertierung in einen String zur Ressourcen-Identifikation.
Seite 14
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Beispiel:
Die Ressourcen-Datei:
#include <windows.h>
#define IDD_DIALOG
1000
IDD_DIALOG DIALOG 20, 20, 90, 50
......
Erzeugung des Dialoges in der Quelldatei:
#define IDD_DIALOG
1000
DialogBox( ghInstance, MAKEINTRESOURCE(IDD_DIALOG), hWnd,
(DLGPROC)DlgProc );
1.3.2 Präprozessor Anweisungen innerhalb einer Ressource-Datei
Der Ressource-Datei Präprozessor versteht nahezu dieselben Anweisungen wie der
C/C++ Präprozessor.
Es existieren folgende Anweisungen:
- für Makrodefinitionen
- zur bedingten Kompilierung
:
:
- für Header-Dateien
- und für Compiler-Fehlermeldungen
:
:
#define, #undef
#if, #ifndef, #ifdef, #else,
#endif, #elif
#include
#error
Zudem können innerhalb einer Ressource-Datei Kommentare im C und C++ Stil
verwendet werden ( /* */ und // ).
Der Ressource-Compiler definiert während seines Laufes das Symbol RC_INVOKED.
So können Compiler-Fehler vermieden werden, wie zum Beispiel die doppelte
Definition einer Klasse, wenn eine Header-Datei neben der Makros zur numerischen
Ressourcen Identifikation zusätzlich die Deklaration von Klassen enthält:
// Makro Definitionen
#define IDD_DIALOG
1000
// Klassen Deklaration
#ifndef RC_INVOKED
class MyDialog
{
....
};
#endif
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 15
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
1.3.3 Definition von Bedienelementen einer Benutzeroberfläche
Generell besteht eine Anweisung innerhalb einer Ressource-Datei aus einem
Identifikator, der entweder einen Textstring oder einen numerischen Wert darstellt
gefolgt von der Darstellungsdefinition wie zum Beispiel DIALOG,siehe Kapitel 1.3.1 .
Diese Anweisung kann entweder eine einzeilige oder mehrzeilige Anweisung
darstellen, der ein oder mehrere Parameter folgen.
Einzeilige Anweisungen werden im allgemeinen zur Definition von Bitmaps, Cursor,
Schriftarten und Icons verwendet. Ein Cursor beispielsweise wird in einer RessourceDatei wie folgt definiert:
MyCursor CURSOR cursorfile.cur
wobei MyCursor den Identifikator darstellt, CURSOR die Darstellungsdefinition ist und
cursorfile.cur als Parameter die Datei des Cursors beschreibt.
Mehrzeilige Anweisungen dienen zur Definition von Dialogen, Zeichentabellen,
Tastenkürzeltabellen, Menüleisten und Versionsinformationen. Ähnlich wie bei der
einzeiligen Anweisung ist die Reihenfolge von Identifikator, Darstellungsdefinition
und Parameter identisch. Nachfolgend können weitere Instruktionen eingeschlossen
von den Schlüsselwörtern BEGIN und END folgen.
Nun ein kleines Beispiel für eine einfache Menüleiste mit zwei Haupteinträgen
(POPUP) „File“ und „Edit“ sowie deren Menü-Unterpunkte (MENUITEM) „New“, „Exit“
sowie „Cut“, „Copy“und „Paste“:
MyMenu MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM
MENUITEM
END
POPUP "&Edit"
BEGIN
MENUITEM
MENUITEM
MENUITEM
END
END
"&New", IDM_FILE_NEW
"&Exit", IDM_FILE_EXIT
"C&ut", IDM_EDIT_CUT
"&Copy", IDM_EDIT_COPY
"&Paste", IDM_EDIT_PASTE
Seite 16
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Abbildung 3: Menü File
Abbildung 4: Menü Edit
Innerhalb einer Ressource-Datei wird oft auch das Attribut DISCARDABLE verwendet.
Dieses Attribut spezifiziert, dass eine Ressource aus dem Arbeitsspeicher entfernt
werden kann, wenn sie nicht mehr benötigt wird. Zum Beispiel:
TempBmp BITMAP DISCARDABLE "c:\\temp\\bitmap1.bmp"
Auf die Elemente der Ressource-Datei des in Abbildung 2 gezeigten Dialoges wird
im folgenden ein wenig näher eingegangen.
Nochmals die Ressourcen-Definition des Dialoges:
IDD_DIALOG DIALOG 10, 20, 90, 50
STYLE DS_MODALFRAME | WS_SYSMENU
CAPTION "Dialog"
BEGIN
DEFPUSHBUTTON "&OK", IDOK, 19, 30, 52, 14, WS_GROUP
CTEXT "Hello, World!", -1, 0, 8, 90, 8
END
Die nachfolgenden Parameter der Darstellungsdefinition DIALOG sind
folgendermaßen zu verstehen. Die vier numerischen Werte beschreiben die Position
sowie die Größe der Dialogbox. Der erste und zweite Wert bestimmt die X- und YPosition der Dialogbox bezüglich der linken oberen Ecke des aufrufenden Fensters.
Der dritte und vierte Wert beschreibt die Breite bzw. Höhe des Dialogfensters. Mit der
Anweisung STYLE kann das Aussehen des Dialogfensters festgelegt werden. Zum
Beispiel wird anhand von WS_SYSMENU ein Systemmenü erzeugt. Das heißt, der
Dialog besitzt ein kleines Kreuz in der rechten oberen Ecke der Titelleiste zum
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 17
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Beenden des Dialoges und es erscheint zudem ein kleines Popup-Menü zum
Schließen und Verschieben, wenn man die rechte Maustaste in der Titelleiste
betätigt. Der Textstring “Dialog“ nach CAPTION definiert den Text, der in der
Titelleiste erscheinen soll. Die Windows-Controls wie Buttons, Textfelder, Editfelder
und Combo-Boxen werden innerhalb der Schlüsselwörter BEGIN und END aufgelistet.
Wobei ein Control folgendermaßen definiert wird:
Control
Controltext ,Identifikator,
x, y, Breite, Hoehe
[, Stil [, erweiterter_Stil]]
Anhand von Control wird definiert, um welche Art von Control es sich handeln soll.
Controltext ist der Text, der innerhalb des Controls angezeigt werden soll. Mittels
des Identifikator wird innerhalb des Source-Codes auf das Control verwiesen. X, Y,
Breite und Hoehe dienen der Positionierung und Dimensionierung. Und Stil bzw.
erweiterter_Stil bestimmen ein Control bezogenes Aussehen oder Verhalten.
Das Control DEFPUSHBUTTON des Dialogs nach Abbildung 2 beschreibt einen Button
der nicht nur mit der Maus betätigt werden kann, sondern zusätzlich auf Drücken der
Return-Taste reagiert. Bei CTEXT (CTEXT wie Center) handelt es sich um ein
Textfeld, dessen Text innerhalb des Feldes zentriert dargestellt wird.
1.4
„Hello, World“ als Win32-API Programm
Für eine simple Textausgabe sind mit der Win32-API eigentlich auch nicht mehr
Zeilen notwendig, als mit printf unter C oder cout unter C++. So erscheint bei
den unten aufgeführten fünf Zeilen Programmcode der Text „Hello, World!“ innerhalb
eines sehr einfachen Fensters auf dem Bildschirm !
#include <windows.h>
void main (void)
{
MessageBox(NULL, "Hello, World!", "",MB_OK);
}
Zudem kann es noch von der Kommandozeile aus mit cl hello1.c user32.lib
kompiliert werden. Vorher sollte jedoch durch Aufruf der Batch-Datei vcvars32.bat
von Visual C++ die dafür entsprechenden Umgebungsvariablen gesetzt werden. Bei
geglückter und erfolgreicher Kompilierung erscheint bei einem Start von hello1.exe
ein Fenster auf dem Bildschirm wie in Abbildung 5.
Seite 18
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Abbildung 5: Hello1.exe
Zu beachten ist zudem die Funktionalität, die dieses Fenster besitzt. Es kann mit der
Maus verschoben werden, hat ein Systemmenü um es zu verschieben oder zu
schließen und kann mittels der Return-Taste geschlossen werden.
1.4.1 Die Funktion WinMain
Leider sieht die Realität in der Windowsprogrammierung doch ganz anders aus. So
benötigt die in Abbildung 6 dargestellte Variante von “Hello, World!”, Hello2.exe,
immerhin stattliche 45 Zeilen.
Abbildung 6: Hello2.exe
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 19
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Quelltext von Hello2.exe:
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam)
{
PAINTSTRUCT ps;
HDC
hDC;
RECT
rect;
switch (msg)
{
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(
hDC, "Hello, World!", -1, &rect,
DT_SINGLELINE|DT_CENTER|DT_VCENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR
lpszCmdLine, int nCmdShow)
{
MSG
msg;
HWND
hWnd;
WNDCLASS
wc;
if (!hPrevInstance)
{
memset(&wc, 0, sizeof(wc));
wc.lpszClassName = "HelloWorld";
wc.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
RegisterClass(&wc);
}
hWnd = CreateWindow("HelloWorld", "Hello, World Programm",
WS_OVERLAPPEDWINDOW, 20, 20, 300, 250,
NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
while (GetMessage(&msg, NULL, 0, 0))
DispatchMessage(&msg);
return msg.wParam;
}
Wird dieser Code nun mit der Anweisung cl hello2.c user32.lib gdi32.lib
übersetzt und das daraus resultierende Programm gestartet, erkennt man, dass es
sich hierbei nicht nur um eine simple Textausgabe in graphischer Form handelt,
sondern um eine richtige Windows-Anwendung, deren Icon und Fenstertitel innerhalb
Seite 20
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
des Task-Managers erscheint. Das Fenster kann vergrößert und verkleinert werden
und ebenfalls erscheint ein Systemmenü beim Betätigen der rechten Maustaste
innerhalb der Fenster-Titelleiste.
Auffällig sind die zwei unterschiedlichen Hauptfunktionen in Hello1.exe und
Hello2.exe. Bei Hello1.exe handelt es sich um die gewohnte Hauptfunktion
main()einer C/C++-Konsolen-Anwendungen, bei denen die Ausgabe von
Informationen für gewöhnlich mittels cout bzw. printf erfolgt. Im Vergleich dazu ist
die Hauptfunktion WinMain(...) von Hello2.exe doch etwas komplizierter gestaltet.
Neben dem Präfix WINAPI besitzt sie vier Übergabeparameter die nun näher
erläutert werden:
WinMain wird von der Startup-Funktion _WinMainCRTStartup der C/C++Laufzeitbibliothek aufgerufen, die statisch zur EXE-Datei hinzugebunden wird.
_WinMainCRTStartup wiederum wird beim Programmstart vom Lader des
Betriebssystems aufgerufen. Die Übergabeparameter von WinMain werden beim
Aufruf durch _WinMainCRTStartup übergeben.
Der Handle hInstance vom Typ HINSTANCE beschreibt in Win32 die Startadresse
der Speicherabbildung der auszuführenden EXE-Datei innerhalb des virtuellen
Arbeitsspeicher. Bei 16-Bit Windows handelt es sich hierbei nicht um HINSTANCE
sondern um HMODULE. Wird bei der Win32-API oftmals der Typ HMODULE verlangt,
kann getrost eine Variable vom Typ HINSTANCE verwendet werden, da diese in
Win32 den selben Typ darstellen. Für gewöhnlich speichert man sich den Handle
hInstance, da dieser eventuell noch zum Laden von Ressourcen Verwendung
findet.
hPrevInstance beschreibt den Handle auf die vorgehende Instanz eines
Prozesses. Dieser Parameter dient nur noch zur Gewährleistung der
Abwärtskompatibilität und besitzt für 32-Bit Windows stets den Wert NULL. In 16-Bit
Windows ist dieser Parameter der Handle einer eventuell bereits erzeugten Instanz
der selben Anwendung, um gewisse Vorkehrungen bezüglich der Initialisierung
treffen zu können, die aufgrund der unterschiedlichen Speicherverwaltung des 16-Bit
Windows gegenüber der 32-Bit Systeme notwendig sind.
lpszCmdLine ist ein Zeiger auf die Kommandozeile, wobei dieses den
Programmnamen nicht enthält.
nCmdShow legt letztendlich fest, wie das Fenster dargestellt werden soll. Zum
Beispiel, minimiert oder in der originalen Größe.
Das Präfix WINAPI ist die Aufrufkonvention der Funktion. Eine Aufrufkonvention
beschreibt das Format des Funktionsnamens innerhalb der EXE-Datei und ob die
aufrufende oder aufgerufene Funktion den Stack wieder aufräumt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 21
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
1.4.2 Registrierung einer Fensterklasse
Nach WinMain, innerhalb des Programmcodes, werden nun drei lokale Variablen
angelegt. Zum einen msg, hWnd und wc vom Typ WNDCLASS. Mit Hilfe von wc erfolgt
die Registrierung der Fensterklasse mit der API-Funktion RegisterClass.
In Windows ist jedes Fenster einer Fensterklasse zugeordnet. Dabei kann es sich um
selbst definierte Fensterklassen handeln, also um solche, die mittels
RegisterClass registriert wurden, oder um Fensterklassen, die bereits von
Windows vordefiniert sind. Vordefinierte Klassen sind zum Beispiel die Klasse
BUTTON, COMBOBOX, EDIT, SCROLLBAR und STATIC. Also Fenster, die von ihrem
Aussehen her doch recht unterschiedlich sind.
Die Registrierung einer Fensterklasse ist deshalb notwendig, da Windows über
gewisse Eigenschaften eines erzeugten Fensters Bescheid wissen muss. So zum
Beispiel braucht es Informationen, welche Art von Cursor verwendet werden muss,
wenn die Maus sich innerhalb des Fensters befindet oder welche Funktion für die
Abarbeitung der Windows-Nachrichten verantwortlich ist.
Der Typ WNDCLASS ist folgender maßen definiert:
typedef struct _WNDCLASS {
UINT
style;
WNDPROC lpfnWndProc;
int
cbClsExtra;
int
cbWndExtra;
HANDLE hInstance;
HICON
hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
Zum allgemeinen Verständnis von Hello2.exe reicht es, wenn nur einige der Felder
dieser Struktur erläutert werden:
lpfmWndProc beschreibt die Adresse der Funktion, die für die Verarbeitung der
Fenster-Nachrichten verantwortlich ist.
hInstance ist die Basisadresse des Speicherbereichs der EXE-Datei innerhalb des
virtuellen Arbeitsspeichers. Deshalb wird diesem Feld der Übergabeparameter
hInstance von WinMain zugeordnet.
hIcon ist ein Handle auf das Icon, das in der linken oberen Ecke des Fensters
erscheinen soll. In Hello2.exe wurde ein Default-Icon verwendet, das durch
IDI_APPLICATION identifiziert wird.
Seite 22
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
hCursor ist ein Handle auf den Cursor der erscheinen soll, wenn die Maus in das
Fenster eintritt. Im Fall von Hello2.exe wurde der typische „Pfeil“-Cursor verwendet.
hBackground ist der Handle auf eine GDI-Brush (Graphics Device Interface), die
verwendet wird, um den Fensterhintergrund zu zeichnen.
lpszMenuName ist der String der eine Menü-Ressource identifiziert, welche die
Menüleiste des Fensters darstellen soll. Ist dieser Wert null, so enthält das Fenster
kein Menü, wie im Falle von Hello2.exe.
lpszClassName dient zur weiteren Identifikation dieser
beispielsweise beim Erzeugen des Fensters durch CreateWindow.
Fensterklasse,
1.4.3 Erzeugen eines Fensters
Das eigentliche Erzeugen des Fensters erfolgt mit der Funktion CreateWindow. Der
Aufruf dieser Funktion erfolgt in der Regel direkt nach der Registrierung der
Fensterklasse. CreateWindow besitzt elf Übergabeparameter die mehr oder
weniger von Bedeutung sind.
HWND CreateWindow( LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HANDLE hInstance,
LPVOID lpParam);
lpClassName definiert den Namen der Fensterklasse, von der das zu erzeugende
Fenster die Eigenschaften erben soll. Dieser Namen kann entweder ein selbst
registriertes Fenster darstellen oder ein von Windows vordefiniertes Fenster. Möchte
man einen Button als Fenster erzeugen, sollte dort der Klassenname „BUTTON“
stehen. Mit lpWindowName kann ein Fenstertitel-Text definiert werden. Durch den
Parameter dwStyle wird der Stil des Fensters bestimmt. Nicht zu verwechseln ist
dieser Parameter mit dem Feld style der WNDCLASS Struktur. Während style von
WNDCLASS mehr die grundlegende Eigenschaften eines Fensters bestimmt, wird
durch dwStyle das äußere Erscheinen des Fensters definiert.
Die Definition des Fensterstiles erfolgt anhand von vordefinierten Werten die durch
die bitweise Oder-Verknüpfung (‚|‘) miteinander kombiniert werden können:
WS_MINIMIZEBOX sowie WS_MAXIMIZEBOX in Kombination mit WS_OVERLAPPED
müssen gesetzt werden, wenn das Fenster über eine Minimieren- und MaximierenSchaltfläche verfügen soll. WS_SYSMENU für ein Systemmenü und WS_OVERLAPPED
bzw. WS_POPUP sollte verwendet werden, wenn das Fenster als eigenständiges
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 23
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Fenster, nicht an ein anderes Fenster gebunden, dargestellt werden soll. Der
Fensterstil von Hello2.exe, WS_OVERLAPPEDWINDOW, ist eine Kombination aus
WS_CAPTION,
WS_OVERLAPPED,
WS_SYSMENU,
WS_THICKFRAME,
WS_MINIMIZEBOX und WS_MAXIMIZEBOX.
Die Parameter x und y beschreiben die Position des Fensters bezüglich der linken
oberen Ecke des Bildschirmes. nWith und nHeight dienen zur Bestimmung der
Fenstergröße. hWndParent ist der Handle eines eventuellen Elternfensters. hMenu
ist der Handle eines Menüs. Ist dieser Wert NULL, wird das Menü das in der
WNDCLASS Struktur definiert wurde verwendet. hInstance erklärt sich nun von
selbst. lpParam kann selbst definierte Struktur oder ein C++ Objekt zeigen, welches
von Fensterinitialisierungs-Nachrichten als Nachrichten-Parameter verwendet wird.
Als Rückgabewert liefert CreateWindow bei erfolgreich erzeugtem Fenster einen
Handle auf das Fenster zurück. Ansonsten den Wert NULL.
Der anschließende Aufruf von ShowWindow nach dem Aufruf von CreateWindow in
Hello2.exe bringt das Fenster für den Benutzer sichtbar auf den Bildschirm.
UpdateWindow erzwingt ein Neuzeichnen des Fensters.
1.4.4 Nachrichtenkonzept
Eine Windows-Anwendung reagiert standardmäßig auf äußere Ereignisse. Diese
Ereignisse können entweder von einem Benutzer ausgelöst worden sein oder direkt
vom Betriebssystem. Zur Kommunikation mit dem Betriebssystem oder anderen
Anwendungen, können Windows-Anwendungen wiederum selbst Nachrichten
versenden bzw. Ereignisse auslösen.
Sämtliche Nachrichten in Windows werden in ein oder mehrere Message-Queues
gestellt, bevor sie die entsprechende Anwendung verarbeitet. Wobei in 16-Bit
Windows nur eine einzige Message-Queue für sämtliche Anwendungen zur
Verfügung stand. Der Nachteil dieser Eigenschaft ist mitunter der, durch eine
schlecht kooperierende Anwendung oder eine sich fehlerhaft verhaltende
Anwendung das Entleeren dieser einzigen Message-Queue nicht mehr erfolgt, so ist
das gesamte System blockiert, was sich in lästigen Pieptönen bei jeder
Mausbewegung widerspiegelt.
In 32-Bit Windows besitzt jeder Prozess seine eigene Message-Queue. Entnimmt ein
Prozess aus irgendeinem Grund keine Nachrichten mehr aus seiner MessageQueue, ist nur dieser eine Prozess blockiert und nicht das gesamte System.
Wie schon erwähnt erfolgt die Nachrichtenverarbeitung innerhalb einer dafür
vorgesehen Funktion, dessen Adresse bei der Registrierung angegeben werden
muss. Diese Funktion wird auch als Windows-Prozedur bezeichnet. In Hello2.exe
handelt es sich dabei um die Funktion WndProc. WndProc bearbeitet dabei zwei
Windows-Nachrichten, die Nachricht WM_PAINT und die Nachricht WM_DESTROY. Auf
die Bedeutung dieser Nachrichten wird später eingegangen. Zuerst soll geklärt
Seite 24
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
werden, wie die Nachrichten aus der Message-Queue geholt werden und zur
Funktion WndProc gelangen.
Die Übergabeparameter der Windows-Prozedur beschreiben einen Handle hWnd auf
das Fenster, deren Nachrichten in dieser Funktion verarbeitet werden, einen
Identifikator msg, der zur Identifikation der Nachricht dient sowie zwei weitere
Parameter lParam und wParam, die eventuell nachrichtenspezifische Informationen
enthalten können. CALLBACK bezeichnet die Aufrufkonvention dieser Funktion.
Nachdem ein Fenster erzeugt worden ist und eventuell auf dem Bildschirm
dargestellt wurde, erfolgt für jedes Windows-Programm die Verarbeitung von
Nachrichten. Das heißt, Nachrichten werden aus der Message-Queue geholt und an
die Windows-Prozedur der Anwendung weitergeleitet. Dieser Vorgang wird dabei
programmtechnisch durch folgende Schleife repräsentiert:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg); // eventuell
DispatchMessage(&msg);
}
Mit
BOOL GetMessage (
LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin,
UINT wMsgFilterMax);
werden Nachrichten aus der Message-Queue geholt. Der erste Übergabeparameter
ist dabei die Adresse einer Nachrichtenstruktur vom Typ MSG, welche die
Nachrichteninformation aufnehmen soll:
typedef struct tagMSG { // msg
HWND
hwnd;
UINT
message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
hwnd von MSG identifiziert das Fenster, dessen Windows-Prozedur die Nachricht
erhalten soll. message ist die Nachrichtennummer, wie beispielsweise WM_PAINT.
Mit wParam und lParam wird, abhängig vom Wert message, der Nachricht
zusätzliche Information beigegeben. Bei time handelt es sich um den Time-Stamp
der Nachricht und pt identifiziert schließlich die Position der Maus, bevor die
Nachricht abgeschickt wurde.
Der Parameter hWnd ist der Handle auf das Fensters für das die Nachricht bestimmt
ist. Ist dieser Wert NULL ist die Nachrichten für alle Fenster des Threads vorgesehen
(in Win32 besitzt jeder Prozess mindestens einen oder mehrere Threads zur
Ausführung von Programmcode). wMsgFilterMin und wMsgFilterMax dienen,
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 25
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
wie ihr Name schon sagt zur Filterung der zu empfangenden Nachrichten.
GetMessage liefert als Rückgabewert TRUE zurück, falls nicht die Nachricht
WM_QUIT empfangen wurde. Bei Erhalt von WM_QUIT kehrt GetMessage mit FALSE
zurück, was sinnvoller weise zu einer Beendigung der Nachrichten empfangenden
Schleife führen sollte, da die Nachricht WM_QUIT darauf hinweist, dass von
irgendeiner Seite der Wunsch geäußert wurde das Programm zu beenden.
An dieser Stelle sei eine weitere Funktion erwähnt, um Nachrichten aus der
Message-Queue zu lesen. Bei PeekMessage wird im Gegensatz zu GetMessage
nicht gewartet bis eine Nachricht in die Message-Queue gestellt wird. PeekMessage
kann zum Beispiel für Polling verwendet werden.
Die nächste Funktion TranslateMessage, die zur Verarbeitung von Nachrichten
dient, übernimmt die Übersetzung von virtuellen Tastennachrichten. Eine virtuelle
Tastennachricht ist beispielsweise die Aktivierung eines Menüpunktes mittels der
ALT-Taste plus der Taste, welche innerhalb des Menünamens unterstrichen
erscheint (Beispiel: „Datei“ wird aktiviert durch ALT + D). TranslateMessage
„identifiziert“ bei einer „Taste gedrückt“-Nachricht den Charakter-Code der Taste, der
diese Nachricht auslöste. Genauer gesagt verläuft die Identifikation der gedrückten
Taste durch das Auslösen der Nachricht WM_CHAR, die den Tastencode als
zusätzliche Information in wParam mit auf den Weg bekommt.
Nach TranslateMessage wird die Nachricht an DispatchMessage übergeben.
DispatchMessage leitet die Nachricht dann an die Windows-Prozedur weiter, wo
die Nachricht ausgewertet wird. Ist die Nachricht ausgewertet und abgearbeitet, kehrt
DispatchMessage wieder zurück und die Nachrichtenverarbeitung kann von vorne
beginnen.
Bei der Windows-Prozedur handelt es sich um ein riesiges switch-case
Statement, das für jede Nachricht die ausgewertet soll eine case-Anweisung besitzt.
Nachrichten die keine anwendungsspezifische Bearbeitung erfordern, werden an
DefWindowProc weitergeleitet. DefWindowProc ruft die Default-WindowsProzedur auf, damit sichergestellt wird, dass alle Nachrichten eine Verarbeitung
finden. Würde diese Weiterleitung der Nachrichten nicht erfolgen bzw. der
Defaulteintrag des switch-case Statements nicht existieren, hätte das Fenster
nicht das Verhalten, das man von einer Windows-Anwendung gewohnt ist.
Im folgenden wird auf einige der wichtigsten Nachrichten von Windows eingegangen.
Auffallend ist, dass all diese Nachrichten das Präfix „WM_“ besitzen. Wobei „WM“ für
„Windows Message“ steht. Es handelt sich also hierbei um Nachrichten, die von
Windows gesendet werden.
Seite 26
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Tabelle 2: Windows Nachrichten für den Client-Bereich
WM_CREATE
WM_DESTROY
WM_CLOSE
WM_QUIT
WM_ACTIVATE
WM_SHOWWINDOW
WM_ENABLE
WM_MOVE
WM_SIZE
WM_PAINT
Hierbei handelt es sich um eine der ersten Nachricht, die
ein Fenster empfängt. Auslöser dieser Nachricht sind
Funktionen die Fenster erzeugen. Zum Beispiel
CreateWindow.
Diese Nachricht erhält das Fenster, wenn es bereits schon
vom Bildschirm verschwunden ist und vollends zerstört
wird.
Diese Nachricht wird ausgelöst, wenn der Benutzer
beispielsweise auf die Schaltfläche in der rechten oberen
Ecke eines Fensters klickt, die ein Kreuz beinhaltet. Diese
Nachricht wird immer dann versendet, wenn ein Fenster
geschlossen werden soll. Durch das Abfangen dieser
Nachricht, kann der Benutzer beispielsweise nochmals
darauf aufmerksam gemacht werden, ob er die
Anwendung auch wirklich beenden möchte.
WM_QUIT ist für gewöhnlich die letzte Nachricht, die das
Hauptfenster einer Anwendung empfangen kann. Nach
Empfang dieser Nachricht liefert die Funktion GetMessage
FALSE als Rückgabewert. Ausgelöst wird diese Nachricht
durch den Aufruf der Funktion PostQuitMessage.
Diese Nachricht wird an Fenster gesendet, deren Zustand
entweder auf aktiviert oder nicht aktiviert gesetzt wird.
Dabei erhält zuerst das inaktiv werdende Fenster diese
Nachricht. Im aktivierten Zustand befindet sich ein Fenster,
wenn es sich an oberster Stelle der Fensterordnung
befindet und bereit ist, Eingaben von Maus oder Tastatur
entgegenzunehmen.
WM_SHOWWINDOW deutet an, dass ein Fenster sich
entweder in den sichtbaren oder in den unsichtbaren
Zustand begibt.
WM_ENABLE wird an ein Fenster gesendet, wenn dieses
entweder in den Zustand versetzt wird, der ihm erlaubt
Tastatur- oder Mausereignisse zu empfangen oder diesen
Zustand verlässt.
Die Nachricht WM_MOVE deutet an, dass die Position des
Fensters verändert wird.
Wird die Größe eines Fensters verändert, erhält es die
Nachricht WM_SIZE.
WM_PAINT wird an ein Fenster geschickt, wenn Bereiche
des Fensters neu gezeichnet werden müssen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 27
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
WM_SETFOCUS
WM_KILLFOCUS
WM_COMMAND
WM_SETFOCUS weist darauf hin, dass ein Fenster den
Eingabefokus besitzt.
Diese Nachricht wird ausgelöst, bevor ein Fenster den
Eingabefokus verliert.
Dieses Ereignis wird ausgelöst, wenn der Anwender einen
Menüpunkt betätigt hat, wenn ein Dialogelement, wie
Button Ereignisse meldet oder wenn ein Tastenkürzel
betätigt wurde.
Einige Nachrichten betreffen den Nicht-Client Bereich des Fensters. Als Nicht-Client
Bereich eines Fensters wird die Titelleiste, das Menü oder der Rand des Fensters
bezeichnet. Somit Fensterbereiche, die in der Regel nicht von einer Anwendung
behandelt werden müssen. Muss eine Anwendung dennoch diese Bereiche
beeinflussen, kann sie zu diesem Zweck die dafür vorgesehenen WM_NC*
Nachrichten abfangen:
Tabelle 3: Windows-Nachrichten für den Nicht-Client Bereich
WM_NCCREATE
WM_NCDESTROY
WM_NCPAINT
Wird an Fenster geschickt bevor es die Nachricht
WM_CREATE erhält.
Diese Nachricht wird nach der Windows-Nachricht
WM_DESTROY an ein Fenster geschickt.
Deutet darauf hin, dass der Nicht-Client Bereich eines
Fensters neu gezeichnet werden muss.
Hello2.exe verarbeitet die Nachrichten WM_PAINT und WM_DESTROY. WM_PAINT wird
verwendet, um den Text „Hello, World!“ in den Client-Bereich des Fensters zu
zeichnen. Würde dieses Zeichnen des Textes an einer anderen Stelle im Programm
erfolgen, so würde dieser Text eventuell überhaupt nicht sichtbar sein, oder nur
solange erscheinen, bis ein Bereich des Fensters neu gezeichnet werden muss.
Die Funktionsaufrufe, die das Zeichnen von „Hello, World!“ veranlassen, werden
auch im folgenden nicht erläutert. Es ist völlig ausreichend zu wissen, dass an dieser
Stelle der Text gezeichnet wird.
Seite 28
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Die DefWindowProc besitzt eine vordefinierte Implementierung der Nachricht
WM_CLOSE. So ruft DefWindowProc nach Erhalt von WM_CLOSE die Funktion
DestroyWindow auf, die wiederum das Auslösen der Nachricht WM_DESTROY
veranlasst. Um Hello2.exe nun sauber beenden zu können, muss nach WM_DESTROY
der Aufruf von
void PostQuitMessage(int nExitCode);
erfolgen. Diese Funktion bewirkt, dass die Anwendung die Nachricht WM_QUIT erhält
und somit aus ihrer nachrichtenverarbeitenden Schleife springt. Der Parameter
nExitQuit beschreibt den Exit-Code der Anwendung. Dieser Wert wird der
Nachricht im wParam Feld der MSG-Struktur beigegeben und sollte den
Rückgabewert der Anwendung darstellen, der an Windows beim Beenden
übergeben wird.
1.4.5 Versenden von Nachrichten innerhalb einer Anwendung
Es besteht auch die Möglichkeit selbst in einer Windows-Anwendung Nachrichten zu
versenden. Die Win32-API bietet dafür die Funktionen SendMessage und
PostMessage. SendMessage und PostMessage sind zwar nicht die einzigen
Funktionen der Win32-API, die für diesen Zweck verwendet werden, aber dafür die
gebräuchlichsten.
LRESULT SendMessage(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam);
SendMessage ruft direkt die Windows-Prozedur eines Fensters auf und kehrt erst
dann zurück, nachdem die Nachricht verarbeitet wurde. Die vier Übergabeparameter
von SendMessage haben folgende Bedeutung:
hWnd ist der Handle des Fensters, an das die Nachricht geschickt werden soll.
Besitzt hWnd den Wert HWND_BROADCAST wird an sämtliche Anwendungsfenster die
zu sendende Nachricht gesendet.
msg definiert die ID der Nachricht. Somit kann eine bereits vordefinierte Nachricht,
wie beispielsweise WM_PAINT oder eine selbst definierte Nachricht versenden
werden. Bei selbst definierten Nachrichten ist zu beachten, dass deren ID im Bereich
WM_USER bis WM_USER + 0x7FFF liegen sollten. Nachrichten vor WM_USER und
nach WM_USER + 0x7FFF sind bereits für Windows reserviert.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 29
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Beispiel:
#define MyMessage
WM_USER + 0x0001
wParam
und
lParam
von
nachrichtentypische Information.
Beispiel, versenden eines Textes:
SendMessage
spezifizieren
zusätzliche
char buffer[] = “Hello”;
SendMessage(hwnd, WM_GETTEXT, sizeof(buffer), (LPARAM)buffer);
Der Rückgabewert von SendMessage liefert das Ergebnis der Nachrichtenübergabe
und ist nachrichtenspezifisch.
Der asynchrone Partner zu SendMessage bildet PostMessage. PostMessage
stellt eine Nachricht direkt in die Message-Queue eines Fensters und kehrt nach
getaner Arbeit sofort wieder zurück. Im Gegensatz zu SendMessage, wo die
Nachricht direkt an die Windows-Prozedur geschickt wird, ist der Empfänger der
Nachricht bei PostMessage entweder GetMessage oder PeekMessage.
BOOL PostMessage(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam);
Die Bedeutung der Übergabeparameter von PostMessage sind mit denen von
SendMessage identisch. Einziger Unterschied ist der Rückgabewert vom Typ BOOL.
Dieser ist nicht nachrichtenspezifisch, sondern deutet an, dass die Nachricht
erfolgreich in die Message-Queue des nachrichtenempfangenden Fensters gestellt
wurde.
1.5 Hello3.exe als Zusammenfassung der bisherigen Kapitel
Das Programm Hello3.exe dient als Zusammenfassung der bisherigen Kapitel. Es
erweitert Hello2.exe, indem Ressourcen verwendet werden, selbst definierte
Nachrichten verschickt werden und als Ergänzung modale und nicht modale Dialoge
erzeugt werden.
Der Dialog wird in Windows als Fenster gesondert gehandhabt und verdient noch
eine kurze Untersuchung und Erklärung in diesem Kapitel. Zuerst wird das
Programm Hello3.exe (siehe Abbildung 7) vorgestellt.
Hello3.exe besitzt ein kleines Hauptmenü mit drei Menü-Unterpunkten:
-
Dialog: erzeugt einen modalen Dialog,
Nachricht: erzeugt einen nicht modalen Dialog und versendet eine Nachricht mit
SendMessage an das Hauptfenster,
Beenden: beendet die Anwendung.
Seite 30
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
Senden des Inhaltes des
Editfeldes
an
das
Hauptfenster. Erscheint
als neuer Text (anstatt
„Hello, World!“)
Beenden der
Anwendung
Abbildung 7: Hello3.exe
Quelldatei hello3.c von Hello3.exe:
(Der Code für die den modalen und nicht modalen Dialog in den aufgelisteten
Programmdateien ist fett gedruckt !)
#include <windows.h>
#include "resource.h"
#include "string.h"
#define TEXTSIZE 50
// Nachricht definieren, die größer als WM_USER ist
#define MY_MESSAGE
WM_USER + 0x0005
// globaler Instanz-Handle der Anwendung
HINSTANCE ghInstance;
// globaler Window-Handle des Anwendungsfensters
HWND
ghWnd;
// globaler Window-Handle für den nicht modalen Dialog
HWND
hWndDlg;
LRESULT CALLBACK DlgProc1( HWND hWnd, UINT uMsg, WPARAM
wParam, LPARAM lParam )
{
switch( uMsg )
{
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
switch(wParam)
{
case IDOK:
EndDialog( hWnd, TRUE );
return TRUE;
default:
break;
}
}
return 0;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 31
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
LRESULT CALLBACK DlgProc2( HWND hWnd, UINT uMsg, WPARAM
wParam, LPARAM lParam )
{
// Speicher reservieren zum Überbringen der Nachricht
CHAR text[TEXTSIZE] = "";
switch( uMsg )
{
case WM_INITDIALOG:
return TRUE;
case WM_CLOSE:
// Menüpunkte aktivieren
EnableMenuItem(GetMenu(ghWnd), IDM_NACHRICHT, MF_ENABLED);
EnableMenuItem(GetMenu(ghWnd), IDM_DIALOG, MF_ENABLED);
// Dialog beseitigen
DestroyWindow(hWnd);
break;
case WM_COMMAND:
switch( LOWORD(wParam) )
{
case IDB_SENDMESSAGE:
GetDlgItemText(hWnd, IDC_NACHRICHT, text, TEXTSIZE);
SendMessage(ghWnd, MY_MESSAGE, sizeof(text), (LPARAM)text);
default:
break;
}
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam)
{
PAINTSTRUCT ps;
HDC
hDC;
RECT
rect;
static CHAR text[TEXTSIZE] = "Hello, World!";
switch (msg)
{
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(hDC, text, -1, &rect,
DT_SINGLELINE|DT_CENTER|DT_VCENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
Seite 32
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDM_DIALOG:
// Modaler Dialog erzeugen
DialogBox(ghInstance,
MAKEINTRESOURCE(IDD_DIALOG1),
hWnd, (DLGPROC)DlgProc1 );
break;
case IDM_NACHRICHT:
// Nicht modaler Dialog erzeugen
hWndDlg = CreateDialog(ghInstance,
MAKEINTRESOURCE(IDD_DIALOG2),
hWnd, (DLGPROC)DlgProc2);
// Menüpunkte deaktivieren
EnableMenuItem(GetMenu(hWnd),
IDM_NACHRICHT,
MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
EnableMenuItem(GetMenu(hWnd),
IDM_DIALOG,
MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
default:
break;
}
break;
case MY_MESSAGE:
// Empfang der Nachricht die durch SendMessage // Button
// ausgelöst wurde und kopieren des neuen //Strings
strcpy(text, (CHAR*)lParam);
// WM_PAINT Nachricht wird erzwungen
InvalidateRect(hWnd, NULL, TRUE);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR
lpszCmdLine, int nCmdShow)
{
MSG
msg;
WNDCLASS
wc;
ghInstance = hInstance;
if (!hPrevInstance)
{
memset(&wc, 0, sizeof(wc));
wc.lpszClassName = "HelloWorld";
wc.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance,
MAKEINTRESOURCE(IDI_ICON1));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU);
RegisterClass(&wc);
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 33
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
ghWnd = CreateWindow(
"HelloWorld",
"Hello, World Programm",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
300, 250,
NULL, NULL,
hInstance, NULL);
ShowWindow(ghWnd, nCmdShow);
UpdateWindow(ghWnd);
while (GetMessage(&msg, NULL, 0, 0))
{
if (!IsWindow(hWndDlg) || !IsDialogMessage(hWndDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
Ressourcen-Datei hello3.rc von Hello3.exe:
#include "resource.h"
#include <windows.h>
// Menü
IDR_MENU MENU DISCARDABLE
BEGIN
POPUP "&Demo"
BEGIN
MENUITEM "&Dialog",
MENUITEM "&Nachricht",
MENUITEM "&Beenden",
END
END
// Icon der Anwendung
IDI_ICON1
ICON
IDM_DIALOG
IDM_NACHRICHT
IDM_EXIT
DISCARDABLE
"icon1.ico"
// Dialoge
IDD_DIALOG1 DIALOG DISCARDABLE 40, 30, 100, 50
STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON
"&OK",IDOK,23,30,52,14,WS_GROUP
CTEXT
"Hello, World!",-1,0,13,100,8
END
IDD_DIALOG2 DIALOG DISCARDABLE 40, 30, 217, 36
STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP
CAPTION "Nachricht"
FONT 8, "MS Sans Serif"
BEGIN
EDITTEXT
IDC_NACHRICHT,55,10,90,12,ES_AUTOHSCROLL
PUSHBUTTON
"&SendMessage",IDB_SENDMESSAGE,155,10,55,15
LTEXT
"Nachricht:",IDC_STATIC,5,10,40,10
END
Include-Datei resource.h von Hello3.exe:
Seite 34
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
IDM_DIALOG
IDM_NACHRICHT
IDI_ICON1
IDD_DIALOG1
IDD_DIALOG2
IDR_MENU
IDM_DIALOG2
IDC_NACHRICHT
IDB_POSTMESSAGE
IDB_SENDMESSAGE
IDM_EXIT
IDC_STATIC
100
101
101
102
103
104
105
1000
1001
1001
40001
-1
Auch dieses Programm kann wieder von der Kommandozeile aus übersetzt werden.
Folgende Dateien sollten sich dabei im Verzeichnis befinden:
• resource.h (Definition der IDs für die Dialogelemente)
• hello3.c (Source-Code)
• hello3.rc (Ressourcen-Datei) und icon1.ico (Icon der Anwendung).
Kompiliert und gelinkt wird Hello3.exe mit:
rc hello3.rc
cl hello3.res hello3.c.
1.5.1 Dialoge mit der Win32-API
In Hello3.exe werden zwei Dialoge erzeugt. Ein modaler und ein nicht modaler
Dialog. Wobei modal bedeutet, dass ein solcher Dialog die Anwendung komplett für
Eingaben sperrt, mit Ausnahme des modalen Dialoges. Erst nachdem dieser
beendet wurde, kann die Anwendung Eingaben in Form von Maus- und
Tastaturereignissen entgegennehmen. Nicht modale Dialoge im Gegensatz,
bewirken kein „Sperren“ der Anwendung. Sie können zur parallelen Dateneingabe
oder Datenanzeige verwendet werden.
Die Win32-API Funktion zur Erzeugung eines modalen Dialoges lautet:
int DialogBox( HINSTANCE hInstance,
LPCTSTR lpTemplate,
HWND hWndParent,
DLGPROC lpDialogFunc)
Die Übergabeparameter von DialogBox beschreiben einen Handle auf die
Anwendung mit hInstance, einen String auf die Ressource des Dialoges, einen
Handle auf das Elternfenster bzw. das den Dialog erzeugende Fenster und einen
Funktionszeiger auf eine Dialogprozedur. Die Dialogprozedur kann mit der WindowsProzedur eines Anwendungsfensters gleichgestellt werden. Sie übernimmt die
Verarbeitung der Nachrichten, die für den Dialog bestimmt sind. Die
Übergabeparameter, der Rückgabewert sowie die Aufrufkonvention sind die selben,
wie bei der Windows-Prozedur. Der Rückgabewert vom Typ int ist jener Wert der
bei einem Beenden des Dialoges durch die Funktion:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 35
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
BOOL EndDialog(HWND hDlg, int nResult)
als nResult mitgegeben wurde. Ein modaler Dialog wird stets mit EndDialog
beendet.
Ein nicht modaler Dialog wird erzeugt mittels:
HWND CreateDialog( HINSTANCE hInstance,
LPCTSTR lpTemplate,
HWND hWndParent,
DLGPROC lpDialogFunc)
Im Unterschied zu DialogBox liefert diese Funktion einen Handle auf das
Dialogfenster zurück. Dieser kann für weitere Zwecke innerhalb der Anwendung
verwendet werden und sollte auf alle Fälle gespeichert werden. Das Beenden eines
nicht modalen Dialoges erfolgt in der Regel beim Beenden der Anwendung durch
einen Aufruf von DestroyWindow durch Windows selbst. Wird ein nicht modaler
Dialog jedoch mehrmals erzeugt und beendet, muss beim Beenden der Aufruf von
DestroyWindow durch die Anwendung selbst erfolgen. So wird gewährleistet, dass
keine Windows-Objekte im Arbeitsspeicher herumschwirren, die keine Verwendung
innerhalb der Anwendung mehr finden. Eine Besonderheit innerhalb der
nachrichtenverarbeitenden Schleife, die sich durch die Verwendung eines nicht
modalen Dialoges ergibt, ist die zusätzliche Verarbeitung von virtuellen
Tastennachrichten speziell für einen solchen Dialog. Die Schleife sieht für Hello3.exe
folgendermaßen aus:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!IsWindow(hWndDlg) || !IsDialogMessage(hWndDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
Die fett gedruckte Zeile hat folgende Wirkung: Wird eine virtuelle Tastennachricht an
den nicht modalen Dialog geschickt, so erfolgt die Umsetzung dieser Nachricht nicht
in TranslateMessage sondern in InDialogMessage. Ansonsten werden die
Nachrichten
der
übrigen
Applikation
an
TranslateMessage
bzw.
DispatchMessage übergeben.
InDialogMessage unterstützt nicht nur die virtuellen Tastennachrichten sondern
auch sämtliche Nachrichten für den nicht modalen Dialog. Ohne diese Funktion
könnte der nicht modale Dialog keine virtuellen Tastennachrichten empfangen.
Die Windows-Nachricht WM_INITDIALOG wird an die Dialogprozedur eines Dialoges
gesendet, nachdem er erzeugt wurde, jedoch noch bevor dieser auf dem Bildschirm
erscheint. Die Nachricht wird dazu verwendet um eventuell eine Initialisierung der
Bedienelemente des Dialoges vorzunehmen (z.B.: Deaktivierung von Buttons). Die
Dialogprozedur sollte TRUE als Rückgabewert liefern, bei einer Windows-Nachricht
Seite 36
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
WM_INITDIALOG. Durch die Rückgabe von TRUE wird Windows mitgeteilt, dass der
Eingabefokus dasjenige Bedienelement besitzen soll, das als erstes innerhalb der
Dialogressource aufgelistet ist. Wird FALSE zurückgegeben kann der Eingabefokus
individuell mittels der Win32-API Funktion SetFocus gesetzt werden. Soll
beispielsweise der „SendMessage“-Button des nicht modalen Dialoges den
Eingabefokus besitzen, anstatt das Editfeld, so muss in die case-Anweisung für
WM_INITDIALOG folgende Zeile eingefügt werden:
SetFocus(GetDlgItem(hwnd, IDB_SENDMESSAGE));
mit
SetFocus(HWND hwnd);
// hwnd: Handle auf das Fenster, das
// den Eingabefokus besitzen soll
und
GetDlgItem(
HWND hwnd,
int id );
// Handle auf den Dialog,
//der das Bedienelement besitzt
// ID des Bedienelements
Zusätzlich muss die Dialogprozedur in diesem Fall FALSE zurückgeben.
1.5.2 Ablaufbeschreibung von Hello3.exe
Sobald nach dem Programmstart einer der Menüpunkte betätigt wird löst dieses eine
WM_COMMAND Nachricht aus. Diese WM_COMMAND Nachrichten werden von der
Windows-Prozedur der Anwendung abgefangen. Die unteren 16-Bit des Parameters
wParam vom Typ WPARAM einer solchen Nachricht geben Aufschluss über die ID des
Bedienelementes, das diese Nachricht auslöste. So können in einer weiteren
switch-case-Anweisung die unterschiedlichen Ereignisbehandlungen für die
Bedienelemente erfolgen. In Hello3.exe wird in dieser zusätzlichen switch-caseAnweisung der modale Dialog erzeugt, der nicht modale Dialog erzeugt, mit
zusätzlicher Deaktivierung von Menüpunkten sowie die Applikation durch einen
Aufruf von DestroyWindow beendet.
Wird nun der Menüpunkt „Dialog“ betätigt, bewirkt dies ein Erzeugen des modalen
Dialoges durch DialogBox. Dieser Dialog erscheint nun auf dem Bildschirm und
kann durch Drücken des OK-Buttons wieder geschlossen werden. Durch Drücken
des OK-Buttons wird ebenfalls eine Nachricht vom Typ WM_COMMAND ausgelöst, die
in der Dialogprozedur des modalen Dialoges eine case-Anweisung für die ID IDOK
besitzt. Innerhalb dieser Anweisung wird der Dialog durch EndDialog geschlossen.
Das Erzeugen des nicht modalen Dialoges erfolgt mit der Funktion CreateDialog,
die durch die Betätigung des Menüpunktes „Nachricht“ aufgerufen wird. Nachdem
der Dialog auf dem Bildschirm erscheint, kann in das Editfeld ein Text eingegeben
werden, der im Hauptfenster als neuer Text gezeichnet werden soll. Durch Drücken
des Buttons „SendMessage“ wird die Windows-Nachricht WM_COMMAND ausgelöst,
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 37
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Das erste Programm
mit dem Wert von IDB_SENDMESSAGE in den unteren 16-Bit von wParam. In der
case-Anweisung für IDB_SENDMESSAGE wird zuerst der Text des Editfeldes mit der
Win32-API Funktion GetDlgItemText in den lokalen Textbuffer „text“ kopiert und
anschließend durch SendMessage direkt an die Windows-Prozedur der Anwendung
geschickt. Als Nachrichten-ID dient die selbstdefinierte Nachricht MY_MESSAGE, die
den Wert WM_USER + 0x0005 besitzt. In der case-Anweisung für MY_MESSAGE der
Windows-Prozedur wird der Text, dessen Adresse der Parameter lParam enthält, in
den statischen Textbuffer „text“ innerhalb der Windows-Prozedur kopiert und
anschließend im Client-Bereich der Anwendung angezeigt, indem eine WM_PAINT
Nachricht mit InvalidateRect erzwungen wird.
Der nicht modale Dialog wird beendet, indem in der case-Anweisung für die
Nachricht WM_CLOSE ein Aufruf der Funktion DestroyWindow stattfindet. Bevor der
Dialog jedoch beendet wird, werden noch vorher die Menüpunkte aktiviert, die nach
dessen erzeugen deaktiviert wurden.
Seite 38
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2 Die Entwicklungsumgebung
Die Entwicklungsumgebung von Visual C++ ist ein mächtiges Werkzeug für die
Entwicklung von Software. Sie besteht aus mehreren Fensterbereichen, die ein- oder
ausgeblendet werden können oder in ihren Größen frei veränderbar sind.
Die Fensterbereiche sind Kontrollzeile, Statuszeile, Browser, Arbeitsfläche und
Ausgabefenster. Die Kontrollzeile und die Statuszeile können vom Entwickler
definiert werden.
Werkzeugleiste
Dokument
Projektfenster
Arbeitsbereich
Ausgabefenster
Statuszeile
Abbildung 8: Die Entwicklungsumgebung
Im folgenden werden die einzelnen Fensterbereiche näher erläutert.
2.1 Projektfenster
Das Projektfenster hat unterschiedliche Aufgaben. Die erste Aufgabe ist die
Darstellung der Klassenhierarchie durch einen Klassenbrowser. Über Kontextmenüs
können Klassen verändert werden (Attribute oder Methoden hinzufügen, löschen).
Die zweite Aufgabe ist die Verwaltung der Ressourcen. Alle Ressourcentypen
(Tastenkürzel/Accelerator, Bilder/Bitmap, Dialoge, Ikonen/Icon, Menüs/Menu, TextTabellen / String Table und Versionsressource/Version) werden in baumform
dargestellt. Die dritte Aufgabe ist die Verwaltung von Dateien, die zum Projekt
gehören. Die in der Version 5 von Visual C++ ebenfalls dort eingebaute OnlineHilfe
wurde in der Version 6 ausgelagert du als HTML-Help neu gesteltet.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 39
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.1.1 Klassenbrowser
2.1.1.1 Eine neue Klasse einfügen
Wie schon erwähnt können mit Hilfe des Klassenbrowsers neue Klassen dem Projekt
hinzugefügt werden. Hierfür muss auf dem Projektnamen im Klassenbrowser die
rechte Maustaste betätigt und danach den Menüpunkt „Neue Klasse...“ ausgewählt
werden. Danach öffnet sich ein Dialog wie in Abbildung 9.
Abbildung 9: Dialog zum Erzeugen einer neuen Klasse
Beim Klassentyp kann zwischen einer MFC-Klassenhierarchie oder einer eigenen
Klassenhierarchie gewählt werden. Unter Name muss der neue Klassenname
eingetragen werden. Der Dialog schlägt selbständig einen Dateinamen vor, in der die
Klasse gespeichert werden soll. Über den Button „Ändern...“ kann hier ein anderer
Dateiname angegeben werden. Wurde als Klassentyp MFC-Klasse ausgewählt, kann
als Basisklasse, eine Klasse aus der MFC ausgewählt werden.
Nach dem Betätigen von „OK“ wird die neue Klasse automatisch im Projekt, wie auch
im Klassenbrowser eingefügt.
2.1.1.2 Eine neue Methode einfügen
Um eine neue Membermethode einer Klasse hinzuzufügen, muss eine Klasse
ausgewählt und die rechte Maustaste betätigt werden. Dort den Menüeintrag
„Member-Funktion hinzufügen...“ auswählen. Danach öffnet sich ein Dialog wie in
Abbildung 10.
Seite 40
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Abbildung 10: Dialog für eine neue Member-Funktion
Unter Funktionstyp muss der Rückgabetyp, unter Funktionsdeklaration müssen der
Funktionsname und die Übergabeparameter angegeben werden.
Zusätzlich kann im Dialog der Zugriffsstatus eingestellt werden. Weiter kann noch
festgelegt werden, ob es sich um eine statische oder eine virtuelle Funktion handelt.
Wird der Dialog mit „OK“ beendet wird die Funktion automatisch in der Klasse in der
Header-Datei deklariert und der Funktionsblock in der CPP-Datei definiert. Es muss
nun nur noch der Funktionsrumpf eingefügt werden.
Im Browser werden die Methoden in Abhängigkeit des Zugriffsstatus durch
unterschiedliche Icons dargestellt.
Durch Doppelklick auf den Funktionsnamen im Klassenbrowser oder über den
Menüeintrag „Gehe zur Definition“ des rechten Maustastenmenüs kann direkt zur
Definition der Methode gesprungen werden. Über den Menüeintrag „Gehe zur
Deklaration“wird zur Deklaration der Methode gesprungen.
Um eine Methode zu löschen muss dies von Hand gemacht werden. Hierfür muss
zum einen die Deklaration in der Header-Datei und zum anderen die Definition in der
CPP-Datei entfernt werden. Erst dann ist die Methode vollständig entfernt. Soll eine
Nachrichtenfunktion entfernt werden, so muss diese zusätzlich noch in der
Nachrichtentabelle gelöscht werden.
2.1.1.3 Ein neues Attribut einfügen
Um Attribute einzufügen muss ähnlich vorgegangen werden wie beim Einfügen von
Funktionen. Diesmal muss nur als Menüpunkt „Member-Variable hinzufügen...“
gewählt werden. Danach öffnet sich ein Dialog wie in Abbildung 11.
Abbildung 11: Dialog für ein neues Member-Attribut
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 41
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Im Feld Variablentyp muss der Typ der Variable angegeben werden. Darunter muss
der Variablenname eingetragen werden. Dabei ist zu beachten, dass alle
Membervariablen den Präfix m_ bekommen. Zusätzlich muss noch der Zugriffsstatus
ausgewählt werden.
Wird der Dialog wiederum mit „OK“ beendet, so wird das Attribut automatisch in der
Klasse deklariert. Wie die Memberfunktionen werden die Attribute in Abhängigkeit
des Zugriffsstatus mit unterschiedlichen Icons dargestellt.
Durch einen Doppelklick auf das Attribut oder über das rechte Maustastenmenü
„Gehe zur Definition“ kann direkt zur Definition des Attributs gesprungen werden.
Wie bei Methoden können auch Attribute nicht direkt im Klassenbrowser gelöscht
werden. Vielmehr muss die Definition des Attributes aus der Header-Datei entfernt
werden.
2.1.2 Ressource-Editor
Für die unterschiedlichen Ressourcen werden jeweils unterschiedliche Editoren zur
Verfügung gestellt. Soll eine neue Ressource eingefügt werden, muss die jeweilige
Kategorie ausgewählt werden. Danach muss mit der rechten Maustaste das
zugehörige Menü geöffnet und dort der Menüpunkt „Einfügen...“, „Importieren...“ oder
„Ressource einfügen“ gewählt werden. Bei „Importieren...“ kann eine vorhandene
Ressource dem Projekt hinzugefügt werden, z.B. Icons oder Bitmaps. Bei
„Ressource einfügen“ wird eine neue Ressource der ausgewählten Kategorie
eingefügt. Über „Importieren...“ erhält man einen Dialog wie in Abbildung 12, in dem
der neue Ressourcentyp ausgewählt werden kann.
Abbildung 12: Ressource-Auswahl
Wird der Dialog mit „Neu“ beendet, wird ein neue Ressource im Ressource-Browser
angelegt. Diese kann dann durch den entsprechenden Editor bearbeitet werden. Im
folgenden werden auf die Editoren näher eingegangen.
Seite 42
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.1.2.1 Dialog-Editor
Mit Hilfe des Dialog-Editors können Eingabe-Schablonen wie in Abbildung 13 für die
Oberfläche generiert werden. Dabei stellt der Dialog-Editor Funktionen wie ein
Graphikeditor zur Verfügung. Es ist möglich aus einer Palette von Steuerelementen
eines auszuwählen und im erstellenden Dialogfenster zu platzieren. Die Palette wird
in Abbildung 14 dargestellt.
Abbildung 13: Die Entwicklungsumgebung mit dem geöffneten Dialog-Editor
statisches Textfeld
Edit-Feld
Gruppe
Button
Checkbox
Radio-Button
Combobox
Listbox
horizont. Scrollbar
vertikal. Scrollbar
Tree-Control
Tab-Control
Rich-Edit
benutzerdefiniertes Steuerelement
Abbildung 14: Palette der Steuerelemente
Jedes Steuerelement bekommt nach dem Einfügen eine ID zugeordnet. Diese ID,
auch Ressource-ID genannt, ist eine Integer-Zahl, die durch ein Makro verdeckt wird.
In der Anwendung selbst wird nur über das Makro auf die ID zurückgegriffen. Alle IDs
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 43
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
der Anwendung werden in der Datei resource.h eingefügt. Der Makroname
bekommt automatisch den Präfix IDC_. Wird der Makroname nicht geändert, so baut
sich der Namen aus dem Präfix, dem Namen des Steuerelements und einer Zahl
zusammen, z.B.: IDC_BUTTON1 für einen Button.
Die Eigenschaften des gesamten Dialogs oder jedes Steuerelements, wie z.B. die
Ressource-ID oder den Windows-Stil, können durch einen Dialog eingestellt werden.
Hierfür muss das Steuerelement markiert und über das rechte Maustastenmenü der
Eintrag „Eigenschaften“ (Properties) ausgewählt werden. Es öffnet sich ein Dialog
wie in Abbildung 15.
Abbildung 15: Einstellung der Dialog-Eigenschaften
Hier können nun Voreinstellungen vorgenommen werden. Es kann festgelegt
werden, ob ein Steuerelement standardmäßig sichtbar oder aktiviert sein soll. Weiter
kann dem Steuerelement ein Rahmen gegeben werden oder spezifische Werte
zugeordnet werden, die nur für diesen Steuerelemententyp vorhanden sind, z.B.
dass ein Editfeld mehrere Zeilen besitzen soll.
In einem fertigen Dialog kann über die Tab-Taste der Fokus zwischen den einzelnen
Steuerelementen in einer gewissen Reihenfolge gesetzt werden. Diese Reihenfolge
ergibt sich durch die Eingabereihenfolge der Steuerelemente in den zu erstellenden
Dialog. Die Reihenfolge kann aber nachträglich noch geändert werden. Hierfür kann
über das Menü „Layout“ und dem Eintrag „Tabulator-Reihenfolge“ die Reihenfolge
der Steuerelemente gesetzt werden. Dabei erhält man den Dialog wie in Abbildung
16.
Abbildung 16: Tab-Reihenfolge festlegen
Die Zahlen geben dabei die Reihenfolge an. Es müssen nun die Steuerelemente in
der Reihenfolge angeklickt werden, die sie später bei der Eingabe ebenfalls besitzen
sollen. Um die Eingabe abzubrechen genügt es mit der Maus in einen freien Bereich
zu klicken oder die ESC-Taste zu drücken. Beim Festlegen der Reihenfolge ist es
Seite 44
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
sinnvoll, die Eingabefelder direkt hinter die zugehörigen statischen Felder zu legen.
So kann durch einen Shortcut des statischen Feldes der Fokus in das Eingabefeld
gesetzt werden. Der Shortcut wird durch ein Kaufsmannsund vor dem Buchstaben,
der als Shortcut dienen soll, im statischen Namen eingefügt.
Damit der Dialog ohne Anwendung getestet werden kann, gibt es die Möglichkeit im
Dialog-Editor den Dialog zu testen. Es muss wiederum im Menü „Layout“ der Eintrag
„Testen“ ausgewählt werden. Danach wird der Dialog als modaler Dialog geöffnet, in
dem Eingaben getätigt werden können. Um den Dialog zu beenden muss „OK“ oder
„Abbrechen“ gedrückt werden. Eine andere Möglichkeit den Dialog zu verlassen ist
die ESC-Taste.
Um den Dialog einer Klasse zuzuordnen, muss ein Doppelklick auf den Dialog
ausgeführt oder über das rechte Maustasten-Menü der Eintrag „Klassen-Assistent ...“
gewählt werden. Näheres wird in Kapitel 6 beschrieben.
2.1.2.2 Menü-Editor
Jede Anwendung besitzt ein Menü über das die Anwendung gesteuert werden kann.
Zusätzlich besitzen viele Fenster ein Kontext-Menü, das sich auf das jeweilige
Eingabefenster anpasst, z.B.: das rechte Maustasten-Menü. Beide Menüs können
mit dem Menü-Editor erstellt werden. Soll ein neues Menü erstellt werden, muss im
Ressource-Editor ein neues Menü hinzugefügt werden. Dieses Menü kann dann
wiederum über eine Ressource-ID oder besser gesagt über ein Makro angesprochen
werden. Wurde ein neues Menü erstellt, öffnet sich der Menü-Editor wie in Abbildung
17.
Abbildung 17: Menü-Editor
Um einen neuen Menüpunkt einzufügen, muss das gestrichelte Rechteck markiert
werden. Es kann nun der Menüname eingegeben werden. Dabei öffnet sich
automatisch der Dialog „Menübefehl Eigenschaften“ wie in Abbildung 18.
Abbildung 18: Menübefehl Eigenschaften
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 45
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Hier kann nun die Art und der Stil eingestellt werden. Damit das Menü durch einen
Hotkey geöffnet werden kann, muss vor dem Buchstaben, der als Hotkey dienen soll,
ein Kaufsmannsund (&) gesetzt werden. Das bedeutet, dass das Hilfe-Menü sich
durch die Tastenkombination ALT + H öffnen lässt .
Abbildung 19: weitere Menüpunkte
Sollen dem Menü weitere Menüpunkte hinzugefügt werden, muss einfach das Menü
und das rechteckig markierte Feld ausgewählt werden. Dann muss wiederum nur der
Name des Menüpunkts eingegeben werden. Bei der Namensvergabe ist darauf zu
achten, dass die Konvention eingehalten wird. D.h. wird durch den Menüpunkt keine
sofortige Aktion ausgeführt, müssen dem Name drei Punkte („...“) folgen.
Abbildung 20: Eigenschaften für einen Menüpunkt
Der Editor erzeugt selbständig eine ID, die durch den Programmierer geändert
werden kann.
Soll der Menüpunkt als Wurzel für ein neues Popup-Menü verwendet werden, muss
die Check-Box „Popup“ angewählt werden. Somit hat der Programmierer die
Möglichkeit Untermenüs zu erstellen.
Sollen zwei Menüpunkte durch eine Linie getrennt werden, muss dem dazwischen
liegenden Feld die Eigenschaft einer „Trennlinie“ zugeordnet werden.
Zusätzlich kann über „Aktiviert“ und „Grau“ ein Menüpunkt standardmäßig aktiv oder
als nicht auswählbar (greyed) dargestellt werden. Dieser Menüpunkt muss dann
durch die Anwendung in Abhängigkeit des Zustandes der Anwendung aktiviert
werden.
Besitzt die Anwendung eine Statuszeile, so kann zu jedem Menüpunkt eine Info in
der Statuszeile ausgegeben werden. Diese Info muss in dem Eingabefeld
„Statuszeilentext“ eingetragen werden. Dabei wird der Infotext nicht direkt dem
Menüpunkt zugeordnet, sondern der Infotext wird in der string table abgelegt und
über die ID des Menüpunkts darauf zugegriffen.
Seite 46
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.1.2.3 Bitmap-Editor
Mit dem Bitmap-Editor können Bitmaps gezeichnet und von der Anwendung über
eine ID angesprochen werden.
Abbildung 21: Bitmap-Editor
Um ein neues Bitmap zu erzeugen, muss im Ressource-Editor ein Bitmap generiert
werden. Danach erhält man einen leeren Bitmap-Editor. Der Bitmap-Editor setzt sich
aus drei Fenstern, wie in Abbildung 21 zusammen. Das linke Fenster, ist das
Zeichen-Fenster, im linken Bereich wird die Originalgröße des Bitmaps dargestellt, im
rechten Bereich eine vergrößerte Darstellung. Das zweite Fenster beinhaltet die
Werkzeugpalette, das dritte Fenster die Farbpalette.
Mit den Werkzeugen, z.B. Stift, Lineal oder Zirkel können die Bitmaps gezeichnet
werden. Ein Werkzeug muss ausgewählt und innerhalb des Bitmaps platziert
werden.
Um das Bitmap der gewünschten Größe anzupassen, müssen einfach die
Markierungspunkte des Bitmaps auf die richtige Größe verschoben werden. Die
Bitmapgröße wird dabei in der Statuszeile angezeigt.
Wird im Ressource-Editor ein Bitmap markiert und über die rechte Taste den
Menüpunkt „Eigenschaften“ aufgerufen, öffnet sich ein Dialog wie in Abbildung 22.
Hier kann nun die ID des Bitmaps und die Bitmapdatei geändert werden.
Abbildung 22: Bitmap Eigenschaften
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 47
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Das Einbinden der Bitmaps in einer Anwendung kann nun durch die Klasse
CImageList geschehen. Hierfür muss im Create-Aufruf die ID des Bitmaps
angegeben werden.
2.1.2.4 String Table - Editor
Im string table - Editor können Strings abgelegt werden, die über eine ID
angesprochen werden können. Als Beispiel dient der Infotext eines Menüpunkts, der
in der Statuszeile dargestellt wird. So muss eine Anwendung, die in mehreren
Sprachen zur Verfügung stehen soll, nur unterschiedliche string tables enthalten, in
denen die Strings in der jeweiligen Sprache stehen. Der string table - Editor wird in
Abbildung 23 dargestellt.
Abbildung 23: string table - Editor
Soll ein neuer String in die Tabelle eingefügt werden, muss über das rechte
Maustastenmenü der Menüpunkt „neue Zeichenfolge“ ausgewählt werden. Danach
öffnet sich ein Dialog wie in Abbildung 24.
Abbildung 24: string table Eigenschaften
Hier kann nun die ID für den String und der String (Beschriftung) geändert werden.
Soll in der Anwendung auf einen String zugegriffen werden, so geschieht dies mit
Hilfe der Klasse CString. Dabei wird ein Objekt der Klasse CString angelegt und
diesem mit der Methode LoadString der String zugewiesen. Als Parameter besitzt
LoadString die ID des Strings, der geladen werden soll.
...
CString str;
str.LoadString(ID_DIALOG_MODALERDIALOGOEFFNEN);
Write(str);
...
Seite 48
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.1.3 Der Datei-Browser
Mit Hilfe des Datei-Browsers (siehe Abbildung 25) können neue Dateien dem Projekt
zugeordnet werden. Dabei werden die cpp- und h-Dateien auf eine
Klassendeklaration geparst. Konnte eine Klasse ausfindig gemacht werden, so wird
sie automatisch im Klassenbrowser eingefügt. Hierfür muss das Projekt markiert sein
und über das rechte Maustastenmenü der Eintrag „Dateien zu Projekt hinzufügen...“
gewählt werden. Es öffnet sich ein Öffnen-Dialog, in dem eine oder mehrere Dateien
ausgewählt werden können, die dem Projekt zugeordnet werden.
Wird auf einer Datei ein Doppelklick ausgeführt oder über die rechte Maustaste den
Menüpunkt „Öffnen“ ausgewählt, wird die Datei geöffnet und im Arbeitsbereich in
einem Fenster ausgegeben. Handelt es sich bei der Datei um ein Bitmap, so wird
automatisch der Bitmap-Editor geöffnet.
Abbildung 25: Datei-Browser
Ebenfalls können hier Dateien aus dem Projekt entfernt werden. Hierfür muss die
Datei markiert werden. Danach kann durch die „Delete“-Taste die Datei entfernt
werden.
Es ist ebenfalls möglich in einem Arbeitsbereich mehrere Projekte gleichzeitig
geöffnet zu halten. Hierfür muss dem Arbeitsbereich nur ein zweites Projekt
zugewiesen werden. Dies geschieht über das rechte Maustastenmenü „Neues
Projekt zu Arbeitsbereich hinzufügen...“ oder „Projekt in den Arbeitsbereich
einfügen...“. Durch das erste Menü wird ein neues Projekt angelegt, das zweite lädt
ein bereits vorhandenes Projekt in den Arbeitsbereich.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 49
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.2 Ausgabe-Fenster
Das Ausgabe-Fenster besteht aus vier Bereichen. Im Bereich „Erstellen“ werden
Compiler- und Linker-Ausgaben wie in Abbildung 26 ausgegeben.
Abbildung 26: Das Ausgabe-Fenster
Im Bereich „Debug“ werden Ausgaben durchgeführt, die die ausgeführte Anwendung
im Debug-Modus ausgibt. Hierfür stehen die Makros TRACE0, TRACE1 usw.
Ebenfalls wird hier aufgeführt, welche DLLs beim Starten der Anwendung geladen
werden müssen.
In den Bereichen „Suchen in Dateien 1“ und „Suchen in Dateien 2“ werden die
gefundenen Suchbegriffe mit Datei und Zeile ausgegeben. Der Suche-Dialog wird
über das Menü „Bearbeiten“ und dem Menüpunkt „Suchen in Dateien...“ gestartet. Es
öffnet sich ein Dialog wie in Abbildung 27.
Abbildung 27: Suchen in Dateien
Hier kann unter „Suchen nach“ der Suchbegriff eingegeben werden. Zusätzlich
können die Dateien und das Verzeichnis, in dem gesucht werden soll, angegeben
werden. Durch markieren von „Ausgabe in Bereich 2“ kann der Ausgabebereich von
„Suchen in Dateien 1“ auf „Suchen in Dateien 2“ umgeschaltet werden. Des weiteren
kann die Suche konkretisiert werden, z.B. nach ganzen Wörtern suchen oder auf
Groß- und Kleinschreibung achten.
2.3 Der Application-Wizard
Auf den Application-Wizard wird in diesem Kapitel nicht näher eingegangen. Er wird
vielmehr in einem späteren Kapitel an Hand eines Beispiels besprochen.
2.4 Klassen-Assistent
Seite 50
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Der Klassen-Assistent ist ein nützliches Hilfsmittel für abgeleitete MFCFensterklassen. Mit ihm ist es möglich die Deklaration und die Definition von
Antwortfunktionen auf die Windowsnachrichten zu implementieren. Weiter kann man
den Steuerelement-IDs eine Membervariable zuordnen.
Der Klassen-Assistent teilt sich in 5 Bereiche auf. Im weiteren wird nun auf drei der
Bereiche näher eingegangen. Die Bereiche „Automatisierung“ und „ActiveXEreignisse“ werden nicht besprochen.
2.4.1 Nachrichtenzuordnungstabelle
Wie weiter oben erwähnt können hier Methoden den Windowsnachrichten
zugeordnet werden. In Abbildung 28 kann der Eingabe-Dialog für die
Nachrichtenzuordnungstabelle betrachtet werden.
Abbildung 28: Der Klassen-Assistent für die Nachrichtenzuordnungstabelle
In der linken Combo-Box kann das zu bearbeitende Projekt eingestellt werden, falls
dem Arbeitsbereich mehrere Projekte zugeordnet sind.
In der daneben liegenden Combo-Box muss die Klasse eingestellt werden, die
erweitert werden soll. Bei Objekt-ID kann das Objekt ausgewählt werden, das die
Nachricht sendet. In Abhängigkeit des Objektes stehen unterschiedliche Nachrichten
in der daneben liegenden Listbox zur Verfügung. Wird als Objekt die Klasse gewählt,
können auf alle Fensternachrichten, die Windows sendet, Methoden gebildet werden.
In der darrunterliegenden Listbox sind alle Nachrichten und die zugewiesenen
Methoden, die in dieser Klasse schon implementiert sind, aufgeführt.
Wird eine Nachricht ausgewählt, zeigt der Dialog eine Beschreibung der jeweiligen
Nachricht an. Um eine Methode zu erzeugen, muss ein Doppelklick auf die Nachricht
erfolgen oder die Nachricht ausgewählt und der Button „Funktion hinzufügen“
gedrückt werden. Bei Nachrichten von Steuerelementen oder bei Menünachrichten
öffnet sich ein neuer Dialog wie in Abbildung 29, in dem der Name der Methode
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 51
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
eingegeben werden kann. Bei Standardnachrichten wird der Methodenname
automatisch erzeugt.
Abbildung 29: Hinzufügen einer neuen Member-Funktion
Durch einen Doppelklick auf eine Member-Funktion oder nach dem Anklicken des
Buttons „Code bearbeiten“ wird der Klassen-Assistent geschlossen und an die
entsprechende Codezeile gesprungen.
Um eine Funktion löschen zu können, muss die Funktion im Klassen-Assistent
ausgewählt sein. Danach kann über die Delete-Taste oder über den Button „Funktion
löschen“ die Funktion gelöscht werden. Dabei wird die Funktionsdeklaration und die
Zuordnung in der Nachrichtentabelle gelöscht, nicht aber die Funktionsdefinition.
Diese muss vom Programmierer selbständig gelöscht werden. Die Funktion kann
auch auf herkömmliche Weise gelöscht werden, es muss nur daran gedacht werden,
dass die Deklaration, die Definition und die Zuordnung in der Nachrichtentabelle
gelöscht wird.
Zusätzlich kann über das Button-Menü „Klasse hinzufügen...“ und den Eintrag
„Neu...“ Klassen hinzugefügt werden. Es öffnet sich ein Dialog wie in Abbildung 30.
Unter Name muss der neue Klassenname eingetragen werden. Der Dialog bildet
dann aus dem Klassennamen automatisch den Dateinamen, unter dem die Klasse
gespeichert werden soll. Der Dateiname kann durch Drücken des Buttons „Ändern...“
geändert werden.
Aus der Combo-Box Basisklasse, muss die Basisklasse ausgewählt werden. Hier
können Probleme auftreten, da nicht alle MFC-Klassen in der Combo-Box enthalten
sind. Z.B. fehlt die Klasse CObject.
Wurde als Basisklasse ein CDialog ausgewählt, kann unter Dialogfeld-ID, die
Ressource-ID des Dialogs eingegeben werden.
Seite 52
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Abbildung 30: Eine neue Klasse hinzufügen
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 53
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
2.4.2 Membervariablen
Im Dialog „Membervariablen“ können, wie der Name schon sagt, Membervariablen
einer Klasse zugewiesen werden. Diese Member-Variable, die hier eingefügt werden
können, stehen in Verbindung zu einem Steuerelement. Die Zuordnung geschieht
über die ID des Steuerelements. Abbildung 31 zeigt den Dialog.
Abbildung 31: Member-Variable
Die Voreinstellungen für Projekt und Klassennamen müssen, wie schon im
vorgehenden Kapitel beschrieben, eingestellt werden. In der darrunterliegenden ListBox sind alle IDs der Steuerelemente aufgeführt, die in dieser Klasse vorhanden
sind.
Um eine Member-Variable hinzuzufügen, muss auf diese doppelt geklickt oder
markiert und den Button „Variable hinzufügen“ gedrückt werden. Danach öffnet sich
ein Dialog wie in Abbildung 32.
Abbildung 32: Member-Variable hinzufügen
Seite 54
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Die Entwicklungsumgebung
Hier kann nun der Name der Member-Variable, die Kategorie und der Variablentyp
eingetragen werden. Wird dieser Dialog mit „OK“ verlassen, so wird die Variable in
der oben eingestellten Klasse deklariert. Zusätzlich wird in der Methode
DoDataExchange der DDX_Text Aufruf für diese Variable eingetragen.
Nun kann im Membervariablen-Dialog die Variable genauer spezifiziert werden. So
kann für eine Zahl der kleinste zulässige Wert und der größte zulässige Wert
angegeben werden. Bei einem CString kann die Anzahl der maximalen Zeichen
festgelegt werden. Die genaue Funktionalität wird in Kapitel 6.1.1 besprochen.
Soll eine Member-Variable gelöscht werden, muss sie markiert und der Button
„Variable löschen“ angeklickt werden.
Um den Typ der Variable zu verändern, muss diese von Hand gemacht werden oder
die Variable muss zuerst gelöscht und dann neu eingefügt werden.
2.5 Die Online-Hilfe
Die Online-Hilfe ist eine der wichtgsten referenzen beim Programmieren mit Visal
C++. Niemand kann sich tausende von WIN32-Funktionen und über Hundert MFCKlassen mitsamt allen Methdoen und Mebern auswendig merken. Am schnellsten
gelangt man zur Hilfe über das Menü „?“. Mit den Subemnü’s Inhalt, Suche und
INdex kann man entweder zur Inhalts-Darstellung, zur Index-Suche oder zur
Freitextsuche gelangen. Für die programmierarbeit sehr wichtig ist die Taste „F1“.
Steht man im Sourcetext (im Editor-Fenster) auf einem Wort und drückt F1, wird die
online-Hilfe gestartet und nach einem Eintrag gesucht, der diesem Wort entspricht.
Der Online-Hilfe wird in Abbildung 33 dargestellt.
Abbildung 33: Info-Viewer
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 55
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
3 Einführung in das MFC-Anwendungsgerüst
3.1 Was ist MFC ?
Das Microsoft-Foundation-Class-Application-Framework (kurz: MFC-Framework oder
MFC-Anwendungsgerüst) erleichtert das Programmieren für die Microsoft Windows –
Plattformen erheblich.
Das MFC-Framework umfasst :
• eine Definition der Gliederung der Quelltextmodule,
• einen übersichtlichen und standardisierten Weg der Kommunikation und des
Datenaustauschs zwischen Teilen der Anwendung,
• Hilfsfunktionen und Makros, die dem Programmierer lästigen Routinecode
vereinfachen helfen
und schließlich als wichtigsten Bestandteil
• die MFC-Klassenbibliothek.
MFC als Standard
Man kann die MFC als eine Art Windows-API für die objektorientierte
Programmierung bezeichnen. Sie wird heute allgemein als Industriestandard für die
Windowsprogrammierung angesehen und mittlerweile sogar von anderen
Compilerherstellern, wie z.B. Symantec verwendet.
MFC-Projekte verfügen über eine Standardstruktur
Jeder Programmierer hat sich im laufe seiner Programmierpraxis eine Art der
Strukturierung seines Quellcodes zurechtgelegt. Da aber größere Softwareprojekte
kaum von Einzelkämpfern zu bewältigen sind, sondern von Teams zwischen 5 und
50 Programmieren gemeinsam bearbeitet werden, ist es hilfreich, eine projektweit
einheitliche Struktur festzulegen. Die MFC verfügt über eine eigene
Anwendungsstruktur, die sich in vielen großen wie kleinen Softwareprojekten
bewährt hat. Ein Programm, das mit Hilfe der MFC entwickelt wurde, kann von jedem
anderen MFC-Programmierer weiterentwickelt und gepflegt werden.
Optimale Ressourcenausnutzung
Die MFC hilft auch dabei, die Ressourcen des Computers optimal zu nutzen.
Darunter versteht man heute unter anderem die konsequente Verwendung von
Softwaremodulen, die nicht statisch zum eigenen Programm gelinkt werden, sondern
von allen Programmen, die das Modul benötigen, zur Laufzeit dynamisch gebunden
werden (siehe später im Kapitel über dynamische Bibliotheken).
Zu Zeiten der 16-Bit-Windowsprogramme hatte ein Minimalprogramm etwa 20 KB.
Ein Minimalprogramm, das mit C++ für eine Win-32-Plattform geschrieben wurde,
umfasst ungefähr 200 KB Maschinencode. Diese Größenzunahme kommt von der
ausschließlichen Verwendung von 32-Bit Zeigern, 32-Bit Ganzzahl-Variablen wo
immer möglich, vom C++-Exception-Handling und natürlich von vielen neuen
Seite 56
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
„Komfort-Merkmalen“ der neuen Win95/WinNT-Oberfläche, wie andockbare Menüs
u.v.a.m.
Durch die Auslagerung von gemeinsam verwendbarem Code in dynamische
Bibliotheken ist jedoch die Größe der EXE des Minimalprogramms auch mit der MFC
kaum größer als 20 KB.
3.2 Ein kurzer Überblick über die Revisionsgeschichte der MFC
Die Version 1.0 der MFC war zunächst eine reine Klassenbibliothek (ohne
Anwendungsgerüst), wurde zusammen mit Microsoft C/C++ 7.0 ausgeliefert und
verfügte damals über folgende Merkmale:
• Allgemeine, nicht Windows-spezifische Klassen für Strings, Listen, Arrays, Maps,
Datums- und Zeitklassen
• Die ganze Hierarchie war abgeleitet von einem gemeinsamen Stammobjekt
„CObject“
• Unterstützung von Multi-Document-Interface-Anwendungen (MDI, siehe Kapitel
7.3)
• Unterstützung von OLE1.0 (object-linking and –embedding, heute ActiveX, siehe
Kapitel 12)
Mit der MFC-Version 2.0 , die mit Visual C++ 1.0 ausgeliefert wurde, ist das
Anwendungsgerüst eingeführt worden. Weitere Neuerungen sind:
• Unterstützung für die Befehle Öffnen, Speichern und Speicher unter ... , sowie der
Liste der letzten Dokumente im Menü Datei.
• Seitenansicht und Drucken
• Symbol- und Statusleisten
• Unterstützung von dynamischen Bibliotheken
• u.v.a.
Mit MFC 2.5 kamen folgende Merkmale hinzu:
• Unterstützung von ODBC (Open Database Connectivity), einer Schnittstelle für
Datenbankzugriffe, die unabhängig vom verwendeten DBMS ist. Es gibt heute
ODBC-Treiber für praktisch jedes DBMS, z.B. für MS Access, MS SQL-Server,
MS FoxPro, Oracle, Sybase, dBase, Paradox, usw.
• Unterstützung für OLE2.0 mit Drag&Drop und OLE-Automation.
Mit MFC 3.0 kam die erste 32-Bit-Version auf den Markt. Neu war unter anderem:
• Unterstützung von Eigenschaftsdialogfeldern (s. später in diesem Kapitel)
• Andockbare Symbolleisten
Mit MFC 3.1 kamen:
• die neuen Windows-95-Steuerelemente
• verbesserte ODBC-Unterstützung
• Winsock-Klassen für die TCP/IP-Kommunikation
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 57
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Mit Visual C++ 4.0 stellte Microsoft MFC 4.0 vor:
• Data-Access-Objects
(DAO),
eine
Alternative
zu
Datenbankprogrammierung
• Klassen für die Threadsynchronisation (s. Kapitel 9.3.3)
• Unterstützung für OLE-(ActiveX-) Steuerelementcontainer
• u.v.a.
ODBC
für
die
Mit Visual C++ 4.2 kam die MFC Version 4.2 heraus: Neu war:
• WinInet-Klassen für die einfache Programmierung von HTTP-, FTP- und GopherClients
• Viele Verbesserungen rund um ActiveX
• verbesserte ODBC-Unterstützung
Mit VC++ 5.0 wurde Version 4.21 der MFC ausgeliefert, die einige Bugs der 4.2Version beseitigt und bietet darüber hinaus:
• Erleichterungen für die Programmierung von ActiveX-Komponenten (ATL –
Active-Template-Library)
• Component-Object-Modell: #import-Anweisung für die Einbindung von
Typbibliotheken
Stand heute ist die Version 4.22 der MFC, die mit VC++ 6 ausgeliefet wird. Hier sind
wiederum einige Bugs der Vorgängerversion behoben und folgende neuen Features
implementiert worden:
• Active Document Containment ermöglicht einer Applikaton sich nahezu
vollständig in einem OLE-Container darzustellen ein beispiel hierfür ist die
Applikation Binder.exe (zu deutsch Sammelmappe) in Microsofts Office-Paket.
• CHtmlView ist eine Klasse die das HTML-Control kapselt. Das HTML-Control
ist letztendlich der Internet-Explorer. DAmit wird dem programmierer ein sehr
mächtiges Werkzeug zur verfügung gestellt. Mit wenigen Handgriffen läßt sich
nun ein eigener Browser zusammennageln.
• Klassen für die Internet Explorer 4.0 Common Controls. Mit dem
Internetexplorer 4.x wird ein Teil des betriebsystems erweitert: Die Common
Controls erhalten Zuwachs um eine ganze Zahl von neuen Contols. Die MFC
wurde um die entsprechenden Klassen CComboBoxEx, CDateTimeCtrl,
CIPAddressCtrl und CMonthCalCtrl erweitert.
• OLE-DB ist eine neue Technik für den Datenbank-Zugriff über die OLETechnlogie. MFC wurde um einige OLE-DB Klassen erweitert.
Eine detaillierte Übersicht über die Historie von MFC ist in der Online-Hilfe von VC++
enthalten.
3.3 Das erste MFC-Programm: „Hello MFC !!“
In diesem Kapitel soll anhand eines konkreten Beispiels gezeigt werden, wie der C++
- Quellcode einer MFC-Anwendung aussieht. Als Beispiel dient wie schon im Kapitel
zuvor, ein kleines Programm, dass eine Zeichenkette („Hello MFC !!“) auf dem
Bildschirm ausgibt.
Wichtig: Die Entwicklungsumgebung „Visual Studio 97“, unter der die MFC-4.21Programme entwickelt werden, enthält einen Code-Generator der dem
Programmierer das lästige Schreiben der immer gleichen generischen
Seite 58
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Standardstrukturen abnimmt, dabei die eingangs erwähnte Projektstruktur erzeugt
und in den Code immer dort entsprechende Bemerkungen einfügt, wo Sie als
Programmierer tätig werden sollen. Damit Sie zwischen dem C++-Code, den der
Code-Generator erstellt, und dem, den der Autor hinzugefügt hat, unterscheiden
können, sind alle nachträglich vom Autor veränderten oder hinzugefügten Zeilen fett
gesetzt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 59
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
3.3.1 Schritt 1: Anlegen des Projekts
Bevor Sie diesen Schritt ausführen (also bevor sie den Rechner überhaupt
anschalten) sollten Sie Ihr Softwareprojekt soweit analysiert haben, dass Sie Antwort
auf folgende Fragen geben können:
(Es macht nichts, wenn Sie einige Begriffe noch nicht verstehen. Sie können diese
Checkliste aber später als eine Art Mindestanforderung an Ihre Projektanalyse
hernehmen.)
• Wollen Sie eine SDI -, eine MDI- oder eine dialogfeldbasierte Anwendung
erstellen?
• Wollen Sie auf Datenbanken zugreifen ?
• Wollen Sie Verbunddokumente, Automatisierung oder ActiveX-Steuerelemente
unterstützen ?
• Wollen Sie auf die Messaging- (Mail-) Schnittstelle zugreifen oder WindowsSockets benutzen ?
Sie werden gleich zu Beginn, wenn Sie ihr Projekt neu erstellen nach all diesen
Merkmale gefragt werden, und es ist ziemlich umständlich, nachträglich etwas zu
ändern. Daher ist eine gründliche Analyse vorher sehr zu empfehlen.
Auf den folgenden Seiten werden Sie die Dialoge sehen, mit denen Sie die oben
genannten Merkmale festlegen können, dabei ist die gezeigte Einstellung jeweils die,
mit der das „Hello MFC !!“-Programm erstellt wurde.
Zuerst geben Sie an, um welche Art von Projekt es sich handelt (hier: MFC –
Anwendung als EXE). Außerdem wählen Sie einen Pfad und einen Namen für das
Projekt. Das Projektverzeichnis wird automatisch erstellt.
Abbildung 34: Auswahl von Projekttyp, -pfad und –name
Seite 60
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Im nächsten Dialog wird der Typ der Anwendung eingestellt. Nähere Informationen
zu Single Document Interface (SDI) und Multiple Document Interface (MDI) finden
Sie im Kapitel 7. Für unser Beispiel wählen wir SDI.
Abbildung 35: Festlegen der Art der Anwendung
Nun können Sie den Anwendungsassistenten anweisen, unterstützenden Code für
die Programmierung von Datenbankzugriffen einzubinden.
Abbildung 36: Einbinden von Datenbankunterstützung
Das nächste Dialogfeld fragt nach Unterstützungsfunktionen für Verbunddokumente
und ActiveX-Elemente. Für unser Beispiel benötigen wir nichts dergleichen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 61
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Abbildung 37: Unterstützung für Verbunddokumente und ActiveX
Nun können Sie ein paar Äußerlichkeiten und Komfortmerkmale festlegen, die Ihr
Programm haben soll.
Abbildung 38: Festlegen von grafischen Merkmalen und weiteren Optionen
Seite 62
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Endspurt: Hier können Sie angeben, ob der Code-Generator Kommentare an den
Stellen einfügen soll, an denen Sie nachher weiter programmieren sollen. Außerdem
weisen Sie den Linker an, die MFC-Bibliothek statisch oder dynamisch zu linken.
Abbildung 39: Kommentargenerierung und MFC-Linker-Einstellung
Der letzte Dialog zeigt eine Übersicht über die Klassen und Dateien, die der CodeGenerator erstellen wird. Sie könnten hier den Klassennamen, den Dateinamen für
die Quellcodedatei oder die vorgeschlagenen Basisklassen ändern. Für das „Hello
MFC!!“-Beispiel belassen wir alles, wie es ist.
Abbildung 40: Übersicht über die automatisch erstellten Klassen
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 63
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Hinweis: Gemäß allgemeiner Konvention beginnen alle Klassennamen mit „C“.
Die Klasse CHelloMFCApp legt die Hauptklasse der Anwendung fest. In der
zugehörigen Datei wird dann eine einzige globale Instanz dieser Klasse erzeugt:
theApp, Ihre Anwendung.
Die Klasse CMainFrame ist die Klasse des Rahmenfensters. Sie wurde von
CFrameWnd abgeleitet. Die Vaterklasse steuert unter anderem das Hauptmenü und
die Position der Toolbars und des Ansichtsfensters (s. unten) innerhalb der
sogenannten client area des Rahmenfensters.
Die Klasse CHelloMFCDoc ist die Dokumentenklasse unserer Anwendung. Sie
wurde von CDocument abgeleitet. Dokumentenklassen kapseln die Daten einer
Anwendung zusammen mit den Methoden zu ihrer Bearbeitung, zum Speichern und
Laden usw.
Dokumentenklassen sind stets mit einer (oder mehreren) Ansichten verknüpft.
Die Klasse CHelloMFCView ist die Ansichtsklasse der Anwendung. In dieser Klasse
wird alles implementiert, was mit der Darstellung der Daten auf dem Bildschirm zu
tun hat. Viele Standardmethoden, wie z.B. das Scrollen von Fensterinhalten erbt
unsere Ansichtsklasse von ihrer Vaterklasse CView oder von deren Vaterklasse
CWnd. Man bezeichnet die Ansichtsklasse oft als die „Schnittstelle“ zwischen dem
Dokument (den Daten) und dem Anwender.
Mehr zu der engen Verknüpfung zwischen Dokumenten und Ansichten erfahren Sie
in Kapitel Quer!.
Zum Schluss fasst das Visual Studio nochmals alle Einstellungen zusammen:
Abbildung 41: Zusammenfassung
Seite 64
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Nachdem Sie diesen Dialog mit OK bestätigt haben erstellt der Anwendungsassistent
sage und schreibe insgesamt 22 Dateien in ihrem Projektverzeichnis. Die wichtigsten
sollen nachfolgend kurz beschrieben werden.
Tabelle 4: Vom Anwendungsassistenten generierte Dateien
Dateiname
HelloMFC.dsp
HelloMFC.dsw
HelloMFC.rc
Inhalt
Projektdatei
mir
Compilerund
Linkereinstellungen für alle QuellcodeDateien
Die „Workspace-“ (Arbeitsbereichs-)Datei
kann mehrere Projekte zusammenfassen
(enthält in diesem Fall nur einen Eintrag für
HelloMFC.dsp)
Die Ressourcendatei im ASCII-Format
Bemerkung
Nicht editieren !!
Nicht editieren !!
i. d. Regel nicht
direkt editieren !!
Resource.h
Die Header-Datei für die Ressourcen; enthält i. d. Regel nicht
die Konstantendefinitionen (als #define) für direkt editieren !!
die Klartextnamen der Steuerelemente
HelloMFC.h
Haupt-Headerdatei,
deklariert
CHelloMFCApp
HelloMFC.cpp
Haupt-Implementationsdatei,
definiert
(implementiert) CHelloMFCApp
MainFrm.h
Header-Datei
des
Rahmenfensters,
deklariert CMainFrame
MainFrm.cpp
Implementationsdatei des Rahmenfensters,
definiert (implementiert) CMainFrame
HelloMFCDoc.h
Header-Datei
der
Dokumentenklasse,
deklariert CHelloMFCDoc
HelloMFCDoc.cpp Implementationsdatei
der
Dokumentenklasse, definiert (implementiert)
CHelloMFCDoc
HelloMFCView.h
Header-Datei der Ansichtsklasse, deklariert
CHelloMFCView
HelloMFCView.cpp Implementationsdatei der Ansichtsklasse,
definiert (implementiert) CHelloMFCView
HelloMFC.opt
Die
„Options“-Datei
(binär)
enthält Nicht editieren !!
Informationen über das Aussehen der
Entwicklungsumgebung
(Fensterlayout,
Benutzermenüs ...)
HelloMFC.clw
Enthält Informationen für den Class-Wizard Nicht editieren !!
(Klassenassistent)
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 65
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
3.3.2 Schritt 2: Funktionalität hinzufügen – Ausgabe in das Ansichtsfenster
Sie können zu diesem Zeitpunkt – ohne eine Zeile C++-Code selbst geschrieben zu
haben – Ihre Anwendung zum ersten mal kompilieren. Das Visual Studio erstellt ein
Verzeichnis .\Debug in Ihrem Projektverzeichnis und erstellt darin die Anwendung
HelloMFC.exe. Wenn Sie diese Datei ausführen, erscheint folgendes Fenster auf
dem Bildschirm:
Titelleiste
Hauptmenü
Toolbar
Statusleiste
Das ist das Fenster, das durch
die Ansichtsklasse definiert
wird. Es ist in die client area
des Rahmenfensters
flächendeckend eingebettet
Abbildung 42: Vom Anwendungsassistenten erzeugte MFC-Anwendung
Wie Sie sehen, hat diese Anwendung schon einen beachtlichen Leistungsumfang.
Es gibt ein Hauptmenü, eine Toolbar und eine Statusleiste. Wenn Sie im Menü Datei
auf Öffnen ... oder das entsprechende Toolbar-Icon klicken, erscheint der bekannte
„Öffnen“-Dialog, wenn Sie im Menü Datei auf Seitenansicht ... klicken, sehen Sie das
leere Blatt Papier vor sich, das ausgedruckt würde, wenn sie auf Drucken... klicken
würden.
Aber etwas sinnvolles tut diese Anwendung noch nicht. Das MFCAnwendungsgerüst stellt uns also zusammen mit dem Codegenerator des
Anwendungsassistenten eine funktionierende „Leer-Anwendung“ zur Verfügung,
aber es liegt an Ihnen, das Fenster mit Leben zu füllen.
Seite 66
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Wir wollten ursprünglich mit Hilfe der MFC den Text „Hello MFC !!“ auf dem
Bildschirm ausgeben. Wir werden dazu die Ansichtsklasse CHelloMFCView so
erweitern, dass gleich beim Start der Anwendung der Text in die Fenstermitte
ausgegeben wird.
Die Klasse CView, von der unsere Ansichtsklasse CHelloMFCView abgeleitet ist,
deklariert eine rein virtuelle Methode OnDraw() :
virtual void OnDraw( CDC* pDC ) = 0;
Der Parameter pDC ist ein Zeiger auf ein Objekt der Klasse CDC. „DC“ steht dabei für
device context. Die Klasse CDC kapselt alle Daten und Methoden rund um das
Thema „Graphics Device Interface“ (GDI). Sie enthält Methoden zum Zeichnen von
Text, Linien, geometrischen Formen, zum Verändern der Zeichen- und Textfarbe
usw. Die von der Win32-API her bekannte Funktion BeginPaint() wird durch den
Konstruktor CDC::CDC() ersetzt, die API-Funktion EndPaint() durch den
Destruktor CDC::~CDC(). Man muss zur Ausgabe in ein Fenster also nur noch ein
temporäres CDC-Objekt erzeugen, seine Methoden benutzen und es danach wieder
zerstören (lassen).
Wir verwenden in unserem Beispiel jedoch eine zweite Möglichkeit: Das
Anwendungsgerüst ruft nämlich immer, wenn ein Teil der Ansicht neu gezeichnet
werden muss, die Funktion OnDraw() auf. Da die Funktion in der Vaterklasse rein
virtuell definiert ist, muss sie in unserer abgeleiteten Klasse überschrieben werden.
Der Anwendungsassistent hat dies bereits vorbereitet. Im Übergabeparameter pDC
erhalten wir einen Zeiger auf einen device context, den das Anwendungsgerüst für
diesen Zweck erzeugt hat. Wir wollen einen Text ausgeben, daher rufen wir die
Methode TextOut() auf. Sie ist überladen und in der Klasse CDC wie folgt
deklariert:
virtual BOOL TextOut(
int x, int y,
LPCTSTR lpszString, int nCount );
BOOL TextOut( int x, int y, const CString& str );
x und y sind die Koordinaten (Ursprung links oben, positive x-Richtung nach rechts,
positive y-Richtung nach unten), in den anderen Parametern wird der Text
übergeben (als Zeiger mit Längenangabe oder als CString-Objekt)
Hier nun der entsprechende Ausschnitt aus der Date HelloMFCView.cpp:
(Hinweis: Die vom Autor hinzugefügten Zeilen sind fett gesetzt.)
/////////////////////////////////////////////////////////////////////////////
// CHelloMFCView Zeichnen
void CHelloMFCView::OnDraw(CDC* pDC)
{
CHelloMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// Textausgabe eingefügt :
pDC->TextOut(0,0,”Hello MFC !!”);
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 67
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
Damit wird der Text in die linke obere Ecke geschrieben, wir wollten aber den Text in
der Mitte des Fensters platzieren.
Dazu benötigen wir zuerst eine Information über die Größe des Ausgabebereiches.
Da unsere Vaterklasse CView von CWnd abgeleitet ist, können wir die Funktion
CWnd::GetClientRect() verwenden:
void GetClientRect( LPRECT lpRect ) const;
Die Funktion erhält einen Zeiger auf eine RECT-Struktur übergeben, in welche die
Koordinaten der linken oberen Ecke und der rechten unteren Ecken kopiert werden.
Damit lässt sich die Mitte des Fensters berechnen:
x = Rect.right / 2;
y = Rect.bottom / 2;
Fehlt nur noch die halbe Breite und Höhe des auszugebenden Textes, um sie von
den Mittelpunktskoordinaten abziehen zu können.
Die GDI-Funktion CDC::GetTextExtent
CSize GetTextExtent( LPCTSTR lpszString, int nCount ) const;
CSize GetTextExtent( const CString& str ) const;
liefert ein Objekt der Klasse CSize zurück, dessen public-Attribute cx und cy die
horizontale bzw. vertikale Ausdehnung des Strings wiedergibt, welcher der Funktion
übergeben wurde.
Die Funktion OnDraw() sieht nun folgendermaßen aus:
/////////////////////////////////////////////////////////////////////////////
// CHelloMFCView Zeichnen
void CHelloMFCView::OnDraw(CDC* pDC)
{
CString strHello = “Hello MFC !!”;
RECT Rect;
CSize size;
int x,y;
CHelloMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
size = pDC->GetTextExtent(strHello);
GetClientRect(&Rect);
x = Rect.right / 2 – size.cx / 2;
y = Rect.bottom / 2 – size.cy / 2;
pDC->TextOut(x,y,strHello);
}
Noch eine Bemerkung zum device context:
Das Anwendungsgerüst übergibt uns für die Ausgabe auf dem Bildschirm den pDCZeiger. Wir können uns darauf verlassen, dass alle Methodenaufrufe, die wir mit
diesem Zeiger machen, vom Betriebssystem in Pixel umgesetzt und dann an den
entsprechenden Hardwaretreiber (also hier den Treiber der Grafikkarte)
weitergeleitet werden. Die Funktion OnDraw() wird aber nicht nur aufgerufen, wenn
Seite 68
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in das MFC-Anwendungsgerüst
die Bildschirmausgabe aktualisiert werden muss, sondern auch, wenn der Benutzer
die Seitenansicht oder Drucken... wählt. Und jedes Mal wird unser Text „Hello MFC
!!“ richtig ausgegeben, auch auf dem Drucker ! Das ist deshalb möglich, weil das
Anwendungsgerüst jedes Mal einen passenden device context erzeugt und uns den
Zeiger darauf übergibt. Die Funktionen zum Zeichnen usw. sind weitestgehend
unabhängig davon auf welchem Gerät die Ausgabe erfolgt !
Ein Problem stellt sich jedoch: Weil das Blatt Papier, das aus dem Drucker kommt,
kein Objekt einer Klasse ist, die von CWnd abgeleitet ist, funktioniert die
Koordinatenberechnung nicht. Es gibt aber eine Funktion CDC::IsPrinting(), mit
der man ermitteln kann, ob es sich bei dem DC um einen Drucker-DC handelt, oder
nicht. Falls ja wird in unserem Beispiel mit Hilfe der Funktion CDC::GetDeviceCaps
die Ausdehnung des eingestellten Papiers ermittelt:
/////////////////////////////////////////////////////////////////////////////
// CHelloMFCView Zeichnen
void CHelloMFCView::OnDraw(CDC* pDC)
{
CString strHello = “Hello MFC !!”;
RECT Rect;
CSize size;
int x,y;
CHelloMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
size = pDC->GetTextExtent(strHello);
if (pDC->IsPrinting())
{
x = pDC->GetDeviceCaps(HORZRES) / 2 – size.cx / 2;
y = pDC->GetDeviceCaps(VERTRES) / 2 – size.cy / 2;
}
else
{
GetClientRect(&Rect);
x = Rect.right / 2 – size.cx / 2;
y = Rect.bottom / 2 – size.cy / 2;
}
pDC->TextOut(x,y,strHello);
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 69
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
4 Nachrichtenverarbeitung mit der MFC
Im Vergleich zur Nachrichtenverarbeitung in einem herkömmlichen C-Programm, ist
die Nachrichtenverarbeitung mit Hilfe der MFC übersichtlicher und einfacher.
Durch die Objektorientierung könnten die Windowsnachrichten als virtuelle
Funktionen in einer Oberklasse definiert werden. Um auf eine Nachricht reagieren zu
können, müsste in einer abgeleiteten Klasse diese Funktion virtuelle überschrieben
werden. Dies hätte zur Folge, dass für jede Fensterklasse alle Methoden für die
Nachrichtenbehandlung in der virtuellen Methodentabelle eingetragen werden
müssen. Außerdem könnten Methoden, die auf ein Menü reagieren sollen, nicht
definiert werden, da es für jede Anwendung andere Menüs gibt. Deswegen stellt die
MFC vordefinierte Makros zur Verfügung, die die Umsetzung zwischen WindowsNachricht und einer Methode realisieren.
In Windows werden zwischen drei Nachrichtenkategorien unterschieden. Die erste
Kategorie sind die Fensternachrichten, die mit dem Präfix WM_ beginnen. Sie werden
von Fensterklassen (CWnd) oder Ansichtklassen (CView) verarbeitet. Diese
Nachrichten besitzen oftmals Parameter, die in Abhängigkeit der Nachricht
interpretiert werden müssen. Als Ausnahme ist die Nachricht WM_COMMAND, die
teilweise zur zweiten und dritten Kategorie gehört.
Zur zweiten Kategorie, den Steuerelement-Nachrichten, gehören die WM_COMMANDNachrichten, die von Steuerelementen oder untergeordneten Fenstern gesendet
werden. Werden Änderungen an einem Steuerelement vorgenommen, sendet dieses
an sein Vaterfenster eine WM_COMMAND-Nachricht, in der die Art der Änderung als
Parameter übergeben wird, z.B.: EN_CHANGED von einem Editfeld. Eine Ausnahme
ist die Benachrichtigungsnachricht BN_CLICKED. Diese Nachricht wird von
Schaltflächen gesendet, wenn ein Benutzer sie anklickt. Sie wird gesondert als
Befehlsnachricht behandelt und weitergeleitet.
Als letzte Kategorie gibt es die Befehlsnachrichten. Zu ihnen gehören die
WM_COMMAND-Nachrichten, die von Menüs, Schaltflächen der Symbolleiste und oder
Zugriffstasten gesendet werden.
Die Nachrichten von Kategorie eins und zwei können von Fenstern der Klasse CWnd
und deren abgeleiteten Klassen wie CFrameWnd, CMDIFrameWnd, CView und
CDialog behandelt werden. Nachrichten der Kategorie drei können von mehreren
Objekten empfangen werden. Zu ihnen gehören einmal Fenster, Dokumente
(CDocument) und die Anwendungsklasse selber (CWinApp).
Ganz allgemein gilt: Nachrichten können von allen Klassen empfangen und
bearbeitet werden, die von der Klasse CCmdTarget abgeleitet wurden.
Seite 70
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
4.1 Makros zur Nachrichtenverarbeitung
Für die Nachrichtenverarbeitung mit der MFC werden Makros verwendet. In
Abhängigkeit des Nachrichtentyps wird zwischen den Makros unterschieden. Alle
Nachrichten der Kategorie 1 besitzen ein vordefiniertes Makro. Für jede Nachricht,
d.h. für jedes Makro gibt es eine Methode, bei der die Signatur (Name und
Parameter) gegeben ist. Nachrichtentypen sind als UINT definiert.
Als Beispiel diene die Windowsnachricht WM_LBUTTONDOWN. Diese Nachricht wird
vom Windowssystem gesendet, wenn die linke Maustaste gedrückt wurde. Sie kann
von einem Fenster nur dann empfangen werden, wenn in der Fensterklasse das
Makro ON_WM_LBUTTONDOWN definiert wurde. Zusätzlich muss die Methode
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
implementiert werden.
Der Ausdruck afx_msg ist nur zur Verdeutlichung, dass es sich bei dem Prototyp um
eine Nachrichtenbehandlungsroutine handelt. Als Parameter besitzt die Methode
nFlags und point. In nFlags stehende Werte geben an, ob noch weitere virtuelle
Tasten, wie die Steuerungstaste, die Shift-Taste oder ein Mausbutton gedrückt
wurden. Ist das Flag MK_CONTROL gesetzt, kann erkannt werden, dass die
Steuerungstaste gleichzeitig mit der linken Maustaste betätigt wurde.
Es ist aber nicht ausreichend nur das Makro und die Funktion zu implementieren. Es
muss in der Klassendeklaration das Makro DECLARE_MESSAGE_MAP() aufgeführt
werden. Dies geschieht in einem protected-Bereich. Durch dieses Makro werden
gewisse Methoden deklariert, die die Windowsprozedur der C-Programmierung
realisiert.
Werden nach diesem Makro weitere Membervariablen deklariert muss vor diesen ein
neuer Zugriffsmodifizierer stehen.
Zusätzlich muss in der Quelltextdatei
Nachrichtenzuordnung enthalten sein.
ein
weiteres
Makropaar
für
die
BEGIN_MESSAGE_MAP(CNewClass, CParentClass)
.......
END_MESSAGE_MAP()
Innerhalb dieses Makropaars müssen nun alle Nachrichtenzuordnungen stehen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 71
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
Beispiel für eine Nachrichtenzuordnungstabelle:
BEGIN_MESSAGE_MAP(CMyView, CView)
ON_COMMAND(ID_BEARBEITEN_COPY, OnBearbeitenCopy)
ON_UPDATE_COMMAND_UI(ID_BEARBEITEN_COPY, OnUpdateBearbeitenCopy)
ON_WM_PAINT()
ON_WM_TIMER()
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
Zuerst wird eine message map für die Klasse CMyView, die von CView abgeleitet
wurde, angelegt.
Durch das Makro ON_COMMAND werden alle Nachrichten, die durch Auswählen eines
Menüpunktes erzeugt werden, abgefangen. Als erster Parameter muss hierbei die ID
des Menüeintrags angegeben werden. Als zweiten Parameter wird der Name der
Methode angegeben, die durch die Auswahl des Menüeintrags ausgeführt werden
soll. Diese Methode hat den Rückgabewert void und keine Übergabeparameter.
Das zweite Makro ON_UPDATE_COMMAND_UI ist für das Ein- und Ausschalten von
Menüeinträgen notwendig. Hierfür muss wiederum als erster Parameter die ID des
Menüeintrags angegeben werden. Der zweite Parameter ist der Funktionsname. Der
Rückgabewert dieser Funktion ist void. Als Parameter wird ein Pointer auf ein Objekt
der Klasse CCmdUI übergeben. Durch die Methode Enable kann der Menüeintrag
ein- oder ausgeschaltet werden. Mit SetCheck ist es möglich den Menüeintrag mit
einem Häkchen zu markieren.
Mit dem Makro ON_WM_PAINT() wird die WM_PAINT Nachricht für den View
abgefangen. Durch dieses Makro wird automatisch die Methode OnPaint
aufgerufen, in der alle graphischen Ausgaben für die View ausgeführt werden.
Einem Fenster können mehrere Timer zugewiesen werden. Sind die Timer
abgelaufen senden sie die Nachricht WM_TIMER. Durch das Makro ON_WM_TIMER()
wird diese Nachricht abgefangen und die Funktion OnTimer(UINT nIDEvent)
aufgerufen. nIDEvent ist dabei der Identifier des Timers, der abgelaufen ist.
Mit dem Makro ON_WM_LBUTTONDOWN() wird wie vorhin erwähnt, die Nachricht für
das Drücken der linken Maustaste abgefangen.
Um die Zuordnungstabelle zu implementieren müssen Sie diese nicht von Hand
eingegeben. Vielmehr werden Sie durch den Klassen-Assistent unterstützt, der die
Zuordnungstabelle
automatisch
generiert
und
die
gewünschten
Nachrichtenzuordnungen einfügt. Näheres finden Sie hierzu unter dem Kapitel „Die
Entwicklungsumgebung“.
Seite 72
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
4.1.1 Benutzerdefinierte Nachrichten
Unter Windows können auch benutzerdefinierte Nachrichten versendet werden. D.h.
jeder Programmierer kann seine eigenen Nachrichten erzeugen. Hierfür stehen unter
Windows zwei Methoden zur Verfügung. Einmal die Methode
LRESULT SendMessage( UINT message,
WPARAM wParam = 0,
LPARAM lParam = 0);
die erst wieder zurückkehrt, wenn die hierfür zugeordnete Methode abgearbeitet ist.
Zum anderen die Methode
BOOL PostMessage(
UINT message,
WPARAM wParam = 0,
LPARAM lParam = 0 );
die sofort zurückkehrt und nicht auf die Abarbeitung der zugeordneten Methode
wartet.
Beide Methoden sind Memberfunktionen der Klasse CWnd, d.h. es können nur
Nachrichten an Fenster, Objekte der Klasse CWnd oder davon abgeleiteten Klassen,
gesendet werden.
Als Parameter besitzen beide Methoden ein UINT message, in dem der
Nachrichtentyp festgelegt ist. Der Nachrichtentyp muss einen Wert größer WM_APP
(0x8000) besitzen, darrunterliegende Werte sind für das Windowssystem
reserviert. Es reicht nicht aus den Nachrichtentyp über dem Wert WM_USER zu
verwenden, da auch die MFC benutzerdefinierte Nachrichten versendet, die in
diesem Bereich liegen. Zusätzlich können dem Nachrichtentyp zwei weitere
Parameter zugewiesen werden (WPARAM, LPARAM). Unter Windows 3.1 war
WPARAM ein 16 Bit und der LPARAM ein 32 Bit Wert. Unter Windows 95 und NT sind
beide Parameter 32 Bit Werte. Es ist somit möglich 32 Bit Zahlenwerte oder Pointer
auf Objekte oder Funktionen mit der Windowsnachricht zu übergeben.
Die Methode SendMessage hat als Rückgabetyp einen LRESULT, was einem 32 Bit
Wert entspricht. Der Rückgabewert ist der Returnwert der zugeordneten Funktion.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 73
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
Die Methode PostMessage hat als Rückgabetyp ein BOOL, dieser ist TRUE, wenn
die Nachricht abgeschickt wurde, sonst ist er FALSE.
Um benutzerdefinierte Nachrichten abfangen zu können, wird ein weiteres Makro
benötigt. Die MFC stellt hierfür das Makro ON_MESSAGE zur Verfügung. Als
Parameter muss der Nachrichtentyp und der Methodenname angegeben werden.
Die zugeordnete Methode muss als Rückgabewert ein LRESULT und als
Übergabeparameter die Werte WPARAM und LPARAM besitzen.
Das Senden einer Nachricht könnte folgendermaßen aussehen:
...
#define MY_MESSAGE (WM_APP + 10)
...
void CMyWnd::SendPerson()
{
CPerson person;
// Bearbeitung der Variable person
person.SetName(„Hugo“);
SendMessage(MY_MESSAGE, 10, (LPARAM) & person)
...
}
Zuerst wird ein benutzerdefinierter Nachrichtentyp (MY_MESSAGE) mit der ID WM_APP
+ 10 bestimmt. Danach wird ein Objekt der Klasse CPerson angelegt. Diesem
Objekt wird mit der Methode SetName einen Namen übergeben. Mit dem Aufruf
SendMessage(MY_MESSAGE, 10, (LPARAM) & person) wird die Nachricht
gesendet. Als WPARAM wird der Wert 10, als LPARAM wird die Adresse des Objekts
person übergeben. Dieser Aufruf kehrt erst dann zurück, wenn die zugeordnete
Methode ausgeführt wurde.
Seite 74
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Nachrichtenverarbeitung mit der MFC
Für diese Nachricht könnte die Nachrichtentabelle und Methodendeklaration und
-definition folgendermaßen aussehen:
class CMyWnd : public CWnd
{
...
protected:
LRESULT MyMessage(WPARAM, LPARAM);
...
protected:
DECLARE_MESSAGE_MAP()
};
...
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
...
ON_MESSAGE(MY_MESSAGE, MyMessage)
...
END_MESSAGE_MAP()
...
...
LRESULT CMyWnd::MyMessage(WPARAM wParam, LPARAM lParam)
{
...
LRESULT result;
int value = (int) wParam;
CPerson* pPerson = (CPerson*) lParam;
...
if(pPerson->GetName() == “”)
{
...
}
else
{
...
}
result = (value == 10);
return result;
}
Als erstes muss die Methode (MyMessage), die die Nachricht behandeln soll, in einer
abgeleiteten Klasse (CMyClass) von CWnd deklariert werden. Danach muss in der
Klassendeklaration die Nachrichtentabelle (DECLARE_MESSAGE_MAP()) deklariert
werden. Die Zuordnung zwischen Nachrichtentyp und Methode geschieht über das
Makro ON_MESSAGE(MY_MESSAGE, MyMessage). Zum Schluss muss die Methode
MyMessage definiert werden. In der Methode selbst kann auf die
Nachrichtenparameter zugegriffen werden. Die Methode muss mit einem
Rückgabewert (return result) enden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 75
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
5 Controls
Controls sind Steuerelemente, von denen es in der MFC sehr viele gibt, so dass
nicht auf alle eingegangen werden kann. Wird ein Control benötigt, das hier nicht
beschrieben ist, kann immer noch die Online-Dokumentation von VisualC++
herangezogen werden, ohne die – wie hier erwähnt sei – kaum ein MFCProgrammierer auskommen kann.
Alle Controls oder Steuerelemente sind von der Klasse CWnd abgeleitet. Das
bedeutet, alle Controls sind Fenster, welche Windows-Nachrichten empfangen
können. Ebenfalls erben sie sehr viele Methoden, die von CWnd zur Verfügung
gestellt werden.
Steuerelemente werden in zwei Schritten erstellt. Als erstes muss der Konstruktor
der Klasse für das eigentliche Steuerelement-Objekt, dann die Create-Funktion zum
Erzeugen eines Fensters aufgerufen werden. Der Create-Funktion muss der
Windows-Style kombiniert mit den Control-Styles, ein CRect-Objekt für die Größe
des Elements, einen Zeiger auf das Vaterfenster und die Steuerelement-ID
übergeben werden. Abhängig vom Steuerelement kann zusätzlich noch ein LPCTSTR
für einen Text übergeben werden. Dieser Text erscheint dann bei einem Button als
Buttonname, bei einem Static-Control als statischer Text.
Nachträglich kann jedem Steuerelement ein Text zugeordnet oder der zugeordnete
Text wieder geändert werden. Dies geschieht über die geerbte CWnd-Funktion
SetWindowText, der einfach ein nullterminierter LPCTSTR übergeben wird. Diese
Methode löst dabei eine WM_SETTEXT Nachricht an das eigentliche Control-Fenster
aus. Dem lParam der Nachricht wird dabei der Pointer auf den char-String
übergeben.
Um Steuerelemente für die Eingabe zu sperren oder freizugeben, muss die Methode
EnableWindow des jeweiligen Controls mit den Werten FALSE oder TRUE
aufgerufen werden. Diese wurde ebenfalls von CWnd geerbt. Wie schon bei
SetWindowText wird ebenfalls eine Windowsnachricht an das Fenster gesendet.
Bei dieser Methode handelt es sich um die Nachricht WM_ENABLE, der im wParam
der boolsche Wert übergeben wird.
Seite 76
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
5.1 Grundgerüst
Im folgenden soll jedes Steuerelement in einer einzelnen Anwendung analysiert
werden. Dadurch, das das Anwendungsgerüst immer dasselbe ist, wird dieses im
voraus vorgestellt und im weiteren immer auf dieses zurückgegriffen.
Für das Hauptfenster wird eine neue Klasse (CMainFrame) von CFrameWnd
abgeleitet. Die Deklaration der Klasse CMainFrame wird wie folgt geschrieben:
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
// Attribute
protected:
// Attribute für Steuerelemente
// Generierte Message-Map-Funktionen
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
Im folgenden wird die Implementierung der Klasse CMainFrame aufgeführt.
#include “stdafx.h”
#include “MainFrm.h”
#include “Control.h”
/////////////////////////////////////////
// CMainFrame
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
END_MESSAGE_MAP()
/////////////////////////////////////////
// CMainFrame Nachrichten-Handler
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung
// einfügen
}
CMainFrame::~CMainFrame()
{
// Heap wieder freigeben
}
/////////////////////////////////////////
// CMainFrame Konstruktion/Destruktion
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 77
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
// Hier müssen die Steuerelemente kreiert werden !!!
return 0;
}
Die Klasse für das Hauptfenster benötigt eine message map, um auf die
Nachrichten, die die jeweiligen Steuerelemente senden können, zu reagieren. Im
Konstruktor werden dynamische Objekte der Steuerelemente angelegt. Im Destruktor
müssen die dynamisch erzeugten Objekte wieder frei gegeben werden. Um ein
Steuerelement zu erzeugen, d.h. ein Fenster für das Steuerelement zu erhalten,
muss die Nachricht WM_CREATE empfangen werden. In der Methode OnCreate
kann dann das Steuerelement kreiert werden. Davor ist dies nicht möglich, da erst ab
diesem Zeitpunkt das Vaterfenster, hier CMainFrame, ein gültiges Fensterhandle
besitzt, der durch den Pointer auf ein CWnd-Objekt übergeben wird. In der Methode
OnCreate wird zuerst die Methode der Vaterklasse aufgerufen, damit alle Attribute
der Vaterklasse initialisiert werden.
Als Anwendungsklasse wird eine eigene Klasse (CControlApp) erzeugt, die von
CWinApp abgeleitet wurde. Die Deklaration hat folgendes Aussehen:
class CControlApp : public CWinApp
{
public:
CControlApp();
~CControlApp();
virtual BOOL InitInstance();
};
Es wird der Default-Konstruktor, der Destruktor und die virtuelle Methode InitInstance
deklariert.
CControlApp::CControlApp()
{
// ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen
// Alle wichtigen Initialisierungen in InitInstance
// platzieren
}
CControlApp::~CControlApp()
{
// ZU ERLEDIGEN: Hier Code zur Destruktion einfügen
}
////////////////////////////////////////////
// Das einzige CControlApp-Objekt
CControlApp theApp;
Seite 78
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
////////////////////////////////////////////
// CControlApp Initialisierung
BOOL CControlApp::InitInstance()
{
CMainFrame* pMainFrame = new CMainFrame();
pMainFrame->Create(NULL,”Control-Window”);
m_pMainWnd = pMainFrame;
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
Hier ist wichtig, dass ein globales Objekt der Klasse CControlApp angelegt wird.
Durch dieses Objekt wird die gesamte Anwendung gestartet. In der virtuellen
Methode InitInstance muss die Anwendung initialisiert werden. Hierfür wird ein
dynamisches Fensterobjekt von der Klasse CMainFrame erzeugt. Durch den Aufruf
der Methode Create wird das Hauptfenster kreiert. Dabei bekommt das Fenster den
Titel
„Control-Window“.
Zusätzlich
wird
im
Fenster
das
Flag
WS_OVERLAPPEDWINDOW gesetzt. Dies geschieht durch den internen Aufruf von
Create. Danach wird das Objekt dem Attribut m_pMainWnd zugewiesen, sichtbar
gemacht (ShowWindow) und neu gezeichnet (UpdateWindow).
Im folgenden wird nun in die Klasse CMainFrame das jeweilige Attribut für das
Steuerelement, welches in dem Kapitel behandelt werden soll, eingefügt. Die
Methode Create von CMainFrame muss jeweils für das Steuerelement angepasst
werden.
In den Beispielen für die einzelnen Steuerelemente werden nicht auf alle
Steuerelementnachrichten eingegangen. Es werden nur exemplarisch einige
herausgegriffen, die die Arbeitsweise verdeutlichen sollen. Die Steuerelemente treten
häufig in Dialogen auf. In den Beispielen werden die Steuerelemente dynamisch
erzeugt und nicht durch den Ressource-Editor in einem Dialog platziert und erzeugt.
Somit kann der Programmierer besser erkennen, wie er mit den einzelnen ControlStyles die Funktionalität eines Controls ändern kann.
5.2 CStatic
Die Klasse CStatic verwaltet statische Elemente, d.h. sie ist nicht nur für statische
Textfelder verantwortlich, sondern kann ebenfalls Bitmaps oder Icons verwalten.
In Abhängigkeit für welches statische Element das Objekt generiert werden soll,
muss im Windows-Style beim Aufruf der Create-Methode ein anderes Flag gesetzt
werden. Für ein statisches Bitmap muss das Flag SS_BITMAP, für ein statisches Icon
SS_ICON gesetzt werden. Für ein statisches Textfeld muss kein weiteres Flag
angegeben werden. Alle Static-Flags werden in Tabelle 5 erläutert. Sie besitzen als
Präfix SS_ (Static-Styles).
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 79
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Tabelle 5: Styles des Static-Controls
Style
SS_BLACKFRAME
SS_BLACKRECT
SS_CENTER
SS_GRAYFRAME
SS_GRAYRECT
SS_ICON
SS_LEFT
SS_LEFTNOWORDWRAP
SS_NOPREFIX
SS_RIGHT
SS_SIMPLE
SS_USERITEM
SS_WHITEFRAME
SS_WHITERECT
Beschreibung
Bestimmt ein Rechteck, dessen Rahmen mit derselben
Rahmenfarbe wie die Rahmen der Fenster gezeichnet
wird. Defaultfarbe ist schwarz
Bestimmt ein Rechteck, das mit der Rahmenfarbe von
Fenstern ausgefüllt wird. Defaultfarbe ist schwarz.
Zentriert den Text in einem Rechteck. Ist der Text zu groß,
wird automatisch der Text von Beginn an ausgegeben und
der überstehende Rest in einer neuen Zeile zentriert.
Bestimmt ein Rechteck, dessen Rahmen mit derselben
Hintergrundfarbe
des
Desktops
gezeichnet
wird.
Defaultfarbe ist grau.
Bestimmt ein Rechteck, das mit der Hintergrundfarbe des
Desktops ausgefüllt wird. Defaultfarbe ist grau.
Bestimmt ein Icon, das ausgegeben wird. Der Text ist der
Name des Icons in den Ressourcen.
Gibt den Text linksbündig aus. Ist der Text zu groß, wird
automatisch der Text von Beginn an ausgegeben und der
überstehende Rest in einer neuen Zeile linksbündig
ausgegeben.
Gibt den Text linksbündig aus. Tabs werden vergrößert,
der Text wird aber nicht in eine neue Zeile umgebrochen
sondern abgeschnitten.
Tritt in einem Text ein „&“ auf, so wird dieses nicht
dargestellt, sondern das darauffolgende Zeichen
unterstrichen ausgegeben. Soll diese Funktionalität
unterdrückt werden, muss SS_NOPREFIX gesetzt werden.
Gibt den Text rechtsbündig aus. Ist der Text zu groß, wird
automatisch der Text von Beginn an ausgegeben und der
überstehende Rest in einer neuen Zeile rechtsbündig
ausgegeben.
Bestimmt ein Rechteck und gibt einen einzeiligen Text
linksbündig aus. Die Zeile kann nicht verkürzt oder
geändert werden.
Bestimmt ein benutzerdefiniertes Element
Bestimmt ein Rechteck, dessen Rahmen mit derselben
Fensterhintergrundfarbe gezeichnet wird. Defaultfarbe ist
weiß.
Bestimmt ein Rechteck, das mit der Fensterhintergrundfarbe ausgefüllt wird. Defaultfarbe ist weiß.
Seite 80
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
In der Klasse CMainFrame muss das Attribut m_pStaticText, das ein Pointer auf
die Klasse CStatic hält, eingefügt werden.
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
...
// Attribute
protected:
CStatic* m_pStaticText;
};
Der Konstruktor und der Destruktor werden wie folgt implementiert:
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pStaticText = new CStatic();
}
CMainFrame::~CMainFrame()
{
delete m_pStaticText;
}
Im Konstruktor wird auf dem Heap ein dynamisches Objekt der Klasse CStatic zur
Laufzeit erzeugt und dem Attribut m_pStaticText zugewiesen. Im Destruktor wird
dieser allokierte Speicherbereich wieder frei gegeben.
Die Create-Methode muss wie folgt implementiert werden:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect rect(10,10,400,30);
m_pStaticText->Create(“ein statisches Textelement”,
WS_BORDER | WS_VISIBLE | SS_CENTER,
rect,this);
return 0;
}
In der Create-Methode wird zuerst die Create-Methode der Vaterklasse
aufgerufen. Konnte diese nicht erfolgreich ausgeführt werden, wird aus der Methode
mit –1 zurückgesprungen und das Fenster konnte nicht erzeugt werden. Danach wird
ein Objekt von CRect angelegt. Die Zahlenwerte sind die Koordinaten für das
statische Textfeld, die in dem darauffolgenden Aufruf als Referenz übergeben
werden. Zusätzlich muss in der Create-Methode der statische Text, den WindowsStyle und einen Pointer auf das Vaterfenster übergeben werden. Durch das Flag
WS_BORDER erhält das statische Textfeld einen Rahmen. Damit das Textfeld nach
dem Erzeugen sofort angezeigt wird, muss das Flag WS_VISIBLE ebenfalls gesetzt
sein. Nun ist es noch möglich die Ausrichtung des Textes festzulegen. Mit
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 81
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
SS_CENTER wird der Text zentriert, weiter gibt es noch SS_LEFT und SS_RIGHT.
Der Defaultwert ist SS_LEFT.
Wird die Anwendung gestartet, erhält man ein Fenster wie in Abbildung 43.
Abbildung 43: Ein statisches Textfeld
Für diese Steuerelement gibt es keine Nachrichten.
5.3 CEdit
Die Klasse CEdit hat die Aufgabe Eingabefelder zu verwalten. Es können
unterschiedliche Styles angegeben werden, die das Aussehen des Edit-Feldes
verändern. So kann ein Editfeld aus einer Zeile oder aus mehreren Zeilen bestehen.
Es kann die Eingabe gesperrt werden oder das Eingabefeld als Passworteingabe
genutzt werden, bei dem die eingegebenen Zeichen als „ * “dargestellt werden.
Alle Edit-Flags werden in Tabelle 6 aufgeführt. Sie besitzen als Präfix ES_ (EditStyles).
Tabelle 6: Styles des Edit-Controls
Style
ES_AUTOHSCROLL
ES_AUTOVSCROLL
ES_CENTER
ES_LEFT
ES_LOWERCASE
ES_MULTILINE
ES_NOHIDESEL
ES_OEMCONVERT
Beschreibung
Verschiebt bis auf 10 Zeichen den Text nach links, wenn
der Anwender eine Eingabe macht. Wird die Eingabe
mit Enter abgeschlossen, wird der Text von der Position
0 aufgeführt.
Setzt nach der Eingabe von Enter den Text auf die erste
Seite.
Richtet den Text in einem mehrzeiligen Editfeld zentriert
aus.
Richtet den Text in einem mehrzeiligen Editfeld links
aus.
Wandelt alle eingegebenen Zeichen in Kleinbuchstaben
um.
Das Editfeld soll mehrzeilig sein.
Im Editfeld wird weiterhin der Text selektiert, auch wenn
es nicht mehr den Fokus besitzt.
Alle Eingaben werden vom ANSI-Zeichensatz in den
OEM-Zeichensatz und wieder zurückgewandelt. Sinnvoll
bei Dateinamen.
Seite 82
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
ES_PASSWORD
ES_RIGHT
ES_UPPERCASE
ES_READONLY
ES_WANTRETURN
Die eingegebenen Zeichen werden als „ * “ dargestellt.
Richtet den Text in einem mehrzeiligen Editfeld rechts
aus.
Wandelt alle eingegebenen Zeichen in Großbuchstaben
um.
Im Editfeld können keine Eingaben vorgenommen
werden.
In einem Editfeld innerhalb eines Dialogs wird bei der
Eingabe von Enter ein Carriage Return eingefügt. Ist
das Flag nicht gesetzt, wird automatisch der DefaultButton des Dialogs ausgeführt.
Alle Edit-Nachrichten werden in Tabelle 7 aufgeführt. Sie besitzen als Präfix EN_
(Edit-Notifications).
Tabelle 7: Benachrichtigungen des Edit-Controls
Nachricht
EN_CHANGE
EN_ERRSPACE
EN_HSCROLL
EN_KILLFOCUS
EN_MAXTEXT
EN_SETFOCUS
EN_UPDATE
EN_VSCROLL
Beschreibung
Zeigt an, dass der Text im Editfeld sich geändert hat.
Zeigt an, dass das Editfeld nicht mehr Speicher für einen
Request allokieren kann.
Zeigt an, dass der Benutzer auf die horizontale Scrollbar des
Edit-Feldes geklickt hat.
Zeigt an, dass das Editfeld den Eingabefokus verloren hat.
Zeigt an, dass die maximale Anzahl an Zeichen für das Editfeld
überschritten wurde.
Zeigt an, dass das Editfeld den Eingabefokus erhalten hat.
Zeigt an, dass das Editfeld den geänderten Text ausgibt. Die
Nachricht wird nach dem Formatieren des Textes und vor dem
Darstellen des Textes gesendet.
Zeigt an, dass der Benutzer auf die vertikale Scrollbar des EditFeldes geklickt hat.
Im Beispiel sollen drei unterschiedliche Editfelder erzeugt werden. Das erste soll ein
normales einzeiliges Editfeld sein, das zweite soll für eine Passworteingabe dienen
und das dritte soll ein mehrzeiliges Editfeld sein, das den Text rechtsbündig ausgibt.
Für alle drei Editfelder wird ein Attribut in der Klassendeklaration angelegt.
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
...
// Attribute
protected:
CEdit* m_pSingleEdit;
CEdit* m_pPassword;
CEdit* m_pMultiEdit;
};
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 83
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Im Konstruktor werden dynamisch die drei Editfelder auf dem Heap erzeugt und im
Destruktor wieder zerstört und freigegeben.
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pSingleEdit = new CEdit;
m_pPassword = new CEdit;
m_pMultiEdit = new CEdit;
}
CMainFrame::~CMainFrame()
{
delete m_pSingleEdit;
delete m_pPassword;
delete m_pMultiEdit;
}
Um eine Nachricht einem Steuerelement, hier einem Editfeld, zuordnen zu können,
wird in der jeweiligen Nachricht die ID des Steuerelements mitgesendet. Die IDs
werden einfach durch #define´s am Anfang der Datei, in der die Klasse
implementiert ist, definiert.
#define IDC_SINGLEEDIT 100
#define IDC_PASSWORD
101
#define IDC_MULTIEDIT 102
Soll bei allen drei Edit-Feldern auf die Nachricht EN_CHANGE reagiert werden, muss
für all drei Edit-Feldern hierfür eine Methode geschrieben werden.
Um auf eine Nachricht reagieren zu können, muss diese in der Nachrichtentabelle
auf eine Methode gemappt werden. Die Nachrichtentabelle für das Beispiel ist wie
folgt:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_EN_CHANGE(IDC_SINGLEEDIT, OnChangeSingleEdit)
ON_EN_MAXTEXT(IDC_PASSWORD, OnMaxTextPassword)
ON_EN_KILLFOCUS(IDC_MULTIEDIT, OnKillFocusMultiEdit)
END_MESSAGE_MAP()
Durch das Makro ON_WM_CREATE wird automatisch die Nachricht WM_CREATE auf
die Methode OnCreate umgesetzt. In der OnCreate-Methode von CMainFrame
werden wiederum die einzelnen Edit-Fenster erzeugt.
Seite 84
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect singleRect(10,10,400,34);
m_pSingleEdit->Create(WS_BORDER | WS_VISIBLE,singleRect,
this,IDC_SINGLEEDIT);
CRect passRect(10,40,400,64);
m_pPassword->Create(WS_BORDER | WS_VISIBLE | ES_PASSWORD,passRect,
this,IDC_PASSWORD);
CRect multiRect(10,70,400,140);
m_pMultiEdit->Create(WS_BORDER | WS_VISIBLE | ES_MULTILINE |
ES_RIGHT,multiRect,this,IDC_MULTIEDIT);
return 0;
}
m_pSingleEdit ist ein Editfeld, das einen Rahmen besitzt und sichtbar ist. Es
erhält die ID IDC_SINGLEEDIT. m_pPassword erhält noch zusätzlich das
ES_PASSWORD Flag und bekommt die ID IDC_PASSWORD. Das dritte Editfeld wird in
m_pMultiEdit gespeichert. Es besitzt das ES_MULTILINE und ES_RIGHT-Flag.
Die ID ist IDC_MULTIEDIT.
Durch das Makro ON_EN_CHANGE wird die Nachricht EN_CHANGE für die ID
IDC_SINGLEEDIT auf die Methode , OnChangeSingleEdit umgesetzt.
void CMainFrame::OnChangeSingleEdit()
{
MessageBeep(0);
}
In der Methode kann nun auf jede Zeicheneingabe reagiert und wie hier durch die
Methode MessageBeep ein Piepton auf dem PC-Lautsprecher ausgegeben werden.
Das Makro ON_EN_MAXTEXT setzt die Nachricht EN_MAXTEXT für das Editfeld mit
der ID IDC_PASSWORD auf die Methode OnMaxTextPassword um.
void CMainFrame::OnMaxTextPassword()
{
MessageBox("Es können keine weiteren Zeichen eingegeben werden !",
"Edit - Password",MB_OK);
}
In der Methode wird eine MessageBox ausgegeben, in der darauf hingewiesen wird,
dass keine weiteren Zeichen eingegeben werden können.
Die Nachricht EN_KILLFOCUS wird durch das Makro ON_EN_KILLFOCUS auf die
Methode OnKillFocusMultiEdit für die Steuerelement-ID IDC_MULTIEDIT
weitergeleitet.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 85
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
void CMainFrame::OnKillFocusMultiEdit()
{
MessageBox("Das mehrzeilige Editfeld hat den Fokus verloren !",
"Edit - Multiline",MB_OK);
}
In der Methode wird eine MessageBox geöffnet, die darauf hinweist, dass die
mehrzeilige Eingabe den Fokus verloren hat.
Zusätzlich müssen die Methoden noch in der Klassendeklaration deklariert werden:
class CMainFrame : public CFrameWnd
{
...
// Operationen
protected:
void OnChangeSingleEdit();
void OnMaxTextPassword();
void OnKillFocusMultiEdit();
...
DECLARE_MESSAGE_MAP()
};
Methoden, die auf Nachrichten reagieren werden immer als protected deklariert.
Wird die Anwendung gestartet, erhält man ein Fenster wie in Abbildung 44.
Abbildung 44: Verschiedene Editfelder (einzeiliges, als Passworteingabe,
mehrzeilig und rechtsbündig)
Seite 86
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
5.4 CButton
Die Klasse CButton kapselt die Funktionalität von unterschiedlichen Knöpfen. Hier
wird zwischen einem normalen Button, einem Radio-Button und einer CheckBox
unterschieden.
Der normale Button verändert sein Aussehen, wenn die linke Maustaste auf ihm
gedrückt wird. Erst beim Loslassen bekommt er sein altes Aussehen und sendet
dann die Nachricht BN_CLICKED.
Der Radio-Button kann angewählt werden, aber nicht durch Drücken zurückgesetzt
werden. Ein Radio-Button kann nur durch Anklicken eines anderen Radio-Button, der
in derselben Gruppe liegt, zurückgesetzt werden. D.h. in einer Gruppe von RadioButtons kann immer nur einer ausgewählt sein (wie beim Radio, es kann nur ein
Sender gewählt sein).
Eine CheckBox kann markiert und wieder demarkiert werden. Jedes Mal sendet sie
die Nachricht BN_CLICKED.
Die Buttons werden durch unterschiedliche Flags erzeugt. Alle Button-Flags werden
in Tabelle 8 aufgeführt. Sie besitzen als Präfix BS_ (Button-Styles).
Tabelle 8: Styles des Button-Controls
Style
BS_AUTOCHECKBOX
BS_AUTORADIOBUTTON
BS_AUTO3STATE
BS_CHECKBOX
BS_DEFPUSHBUTTON
BS_GROUPBOX
BS_LEFTTEXT
Beschreibung
Wie BS_CHECKBOX. Es wird ein Häkchen in der
CheckBox gesetzt wenn der Benutzer diese anklickt.
Beim nächsten Anklicken verschwindet das Häkchen
wieder.
Wie BS_RADIOBUTTON. Es wird der RadioButton
markiert, alle andere RadioButtons in derselben Gruppe
verlieren die Markierung
Wie BS_3STATE. Die Zustände werden unterschiedlich
dargestellt. Markiert, abgedunkelt und nicht markiert.
Erzeugt ein kleines Quadrat mit einem rechts
danebenstehenden Text (Ausnahme wenn zusätzlich
BS_LEFTTEXT gesetzt ist).
Definiert den Defaultbutton, der bei der Eingabe von
Enter automatisch angewählt wird. Er besitzt einen
breiteren Rahmen.
Definiert eine GroupBox. Enthaltene Buttons werden in
einer Gruppe zusammengefaßt. Der zugeordnete Text
erscheint in der linken oberen Ecke.
Wird der Style mit einem RadioButton oder einer
CheckBox kombiniert, erscheint der Text links von dem
RadioButton oder von der CheckBox.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 87
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
BS_OWNERDRAW
BS_PUSHBUTTON
BS_RADIOBUTTON
BS_3STATE
Definiert einen vom Benutzer gezeichneten Button. Das
Rahmenfenster ruft dabei die DrawItem Methode auf
wenn der Button sich darstellen soll. Der Style muss
beim Verwenden der Klasse CBitmapButton gesetzt
werden.
Erzeugt einen Button, der eine WM_COMMAND Nachricht
sendet, wenn er gedrückt wird.
Erzeugt einen kleinen Kreis mit einem rechts
danebenstehenden Text (Ausnahme wenn zusätzlich
BS_LEFTTEXT gesetzt ist).
Wie BS_CHECKBOX. Die CheckBox kann drei Zustände
einnehmen. Der dritte Zustand wird hauptsächlich für
Disablen verwendet.
Alle Button-Nachrichten werden in Tabelle 9 aufgeführt. Sie besitzen als Präfix BN_
(Button-Notifications).
Tabelle 9: Benachrichtigungen des Button-Controls
Nachricht
BN_CLICKED
BN_DOUBLECLICKED
Beschreibung
Zeigt an, dass der Benutzer auf eine Schaltfläche geklickt
hat.
Zeigt an, dass der Benutzer einen Doppelklick auf eine
Schaltfläche gemacht hat.
Im Beispiel sollen unterschiedliche Buttons getestet werden. Ein Standard-Button,
der ein Piepton erzeugt, zwei Radio-Buttons die jeweils eine MessageBox öffnen,
sobald sie markiert werden und eine CheckBox, mit der der StandardButton disabled
werden kann. In der Klassendeklaration wird für jeden Button ein Attribut gesetzt.
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
// Attribute
protected:
CButton* m_pButton;
CButton* m_pRadioButton1;
CButton* m_pRadioButton2;
CButton* m_pCheckBox;
};
Seite 88
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Im Konstruktor wird für jeden Button einen Speicherbereich auf dem Heap erzeugt
und später im Destruktor wieder freigegeben.
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pButton = new CButton;
m_pRadioButton1 = new CButton;
m_pRadioButton2 = new CButton;
m_pCheckBox = new CButton;
}
CMainFrame::~CMainFrame()
{
delete m_pButton;
delete m_pRadioButton1;
delete m_pRadioButton2;
delete m_pCheckBox;
}
Für jeden Button wird eine eigene ID definiert, die bei jeder Nachricht mitgesendet
wird:
#define
#define
#define
#define
IDC_BUTTON
IDC_RADIOBUTTON1
IDC_RADIOBUTTON2
IDC_CHECKBOX
100
101
102
103
Alle Nachrichten, die bearbeitet werden müssen, werden in der Nachrichtentabelle
den zu bearbeitenden Methoden zugeordnet:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_BN_CLICKED(IDC_BUTTON, OnButton)
ON_BN_CLICKED(IDC_CHECKBOX, OnCheckBox)
ON_BN_CLICKED(IDC_RADIOBUTTON1, OnRadioButton1)
ON_BN_CLICKED(IDC_RADIOBUTTON2, OnRadioButton2)
END_MESSAGE_MAP()
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 89
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Die Methode OnCreate wird dann aufgerufen, wenn das Vaterfenster erzeugt und
die Nachricht WM_CREATE gesendet wurde.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect rectButton(10,10,170,38);
m_pButton->Create(“Standard-Button”,WS_BORDER | WS_VISIBLE,
rectButton,this,IDC_BUTTON);
CRect rectRadio1(10,50,100,78);
m_pRadioButton1->Create(“Radio 1”,WS_VISIBLE | BS_AUTORADIOBUTTON,
rectRadio1,this,IDC_RADIOBUTTON1);
CRect rectRadio2(10,80,100,108);
m_pRadioButton2->Create(“Radio 2”,WS_VISIBLE | BS_AUTORADIOBUTTON,
rectRadio2,this,IDC_RADIOBUTTON2);
CRect rectCheck(180,10,400,38);
m_pCheckBox->Create(“Disable Standard-Button”,WS_VISIBLE |
BS_AUTOCHECKBOX,rectCheck,this,IDC_CHECKBOX);
return 0;
}
In der Methode OnCreate werden die einzelnen Buttons generiert. Dem Attribut
m_pButton wird der Standardbutton zugeordnet. Er erhält die ID IDC_BUTTON und
die Beschriftung „Standard-Button“. Den Attributen m_pRadioButton1 und
m_pRadioButton2 wird durch das Flag BS_AUTORADIOBUTTON ein RadioButton
zugewiesen. Sie besitzen die ID’s IDC_RADIOBUTTON1 und IDC_RADIOBUTTON2.
Als Text besitzen die RadioButtons „Radio 1“ und „Radio 2“. Zum Schluß wird mit
dem Flag BS_AUTOCHECKBOX eine CheckBox erzeugt, die die ID IDC_CHECKBOX
besitzt. Als Text erhält die CheckBox „Disable Standard-Button“.
Die BN_CLICKED-Nachricht des Standardbuttons wird in der Methode OnButton
bearbeitet.
void CMainFrame::OnButton()
{
MessageBeep(0);
}
Es wird, sobald der Button gedrückt wurde, ein Piepton ausgegeben.
Wird die CheckBox selektiert oder deselektiert, wird die Methode OnCheckBox
aufgerufen.
void CMainFrame::OnCheckBox()
{
m_pButton->EnableWindow(!m_pCheckBox->GetCheck());
}
Seite 90
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Durch die Methode GetCheck wird der Zustand der CheckBox abgefragt. Der
invertierte Wert wird dann an die Methode EnableWindow übergeben, die das
Fenster (den Button) aktiv (TRUE) oder inaktiv (FALSE) darstellt.
Wird einer der beiden RadioButtons selektiert, so wird die zugeordnete Methode
OnRadioButton1 oder OnRadioButton2 aufgerufen.
void CMainFrame::OnRadioButton1()
{
MessageBox(“Es wurde Radio-Button 1 gedrückt.”,”Button”,MB_OK);
}
void CMainFrame::OnRadioButton2()
{
MessageBox(“Es wurde Radio-Button 2 gedrückt.”,”Button”,MB_OK);
}
In den Methoden wird jeweils eine MessageBox geöffnet, in der ausgegeben wird,
welcher RadioButton gedrückt wurde.
Die Methoden sind in der Klassendeklaration wie folgt deklariert:
class CMainFrame : public CFrameWnd
{
...
// Operationen
protected:
int OnCreate(LPCREATESTRUCT lpCreateStruct);
void OnButton();
void OnCheckBox();
void OnRadioButton1();
void OnRadioButton2();
...
DECLARE_MESSAGE_MAP()
};
Wird die Anwendung gestartet, erhält man ein Fenster wie in Abbildung 45.
Abbildung 45: Verschiedene Buttonarten (Standard, CheckBox und
RadioButton)
Die Klasse CButton wird auch als GroupBox verwendet. Hierfür muss beim
Generieren das Flag BS_GROUPBOX gesetzt werden. In einer GroupBox können
mehrere RadioButtons zusammengefaßt werden. So ist es möglich mehrere
Gruppen von RadioButtons zu erzeugen, ohne dass sich die Gruppen beeinflussen.
D.h. in jeder Gruppe muss dann ein RadioButton selektiert sein.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 91
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
5.5 CListBox
5.5.1 Ganz allgemein
Die Klasse CListBox stellt die Schnittstelle zu einer ListBox dar. Sie besitzt
Methoden, mit denen es möglich ist, die ListBox mit Einträgen zu füllen oder Einträge
zu löschen. Zusätzlich kann jedem Eintrag ein long-Wert zugewiesen werden.
Dieser long-Wert kann dann z.B. einen Pointer auf ein Objekt einer Klasse oder
einer Struktur sein, das mit dem Eintrag eine Beziehung besitzt. Sollen z.B. in einer
ListBox (siehe späteres Beispiel) Personennamen aufgelistet werden, so kann jedem
Eintrag ein Objekt der Klasse CPerson zugewiesen werden. Die Klasse CPerson
besitzt Attribute wie Name, Vorname und Straße.
Alle ListBox-Flags werden in Tabelle 10 aufgeführt. Sie besitzen als Präfix LBS_
(ListBox-Styles).
Tabelle 10: Styles des ListBox-Controls
Style
LBS_EXTENDEDSEL
Beschreibung
Der Benutzer kann mehrere Zeilen gleichzeitig
auswählen. Er muss hierfür zusätzlich die SHIFT-Taste
gedrückt halten.
LBS_HASSTRINGS
Den Elementen einer benutzergezeichneten ListBox
wird ein Text zugeordnet, der über GetText abgefragt
werden kann.
LBS_MULTICOLUMN
Bestimmt eine mehrspaltige ListBox. Die Breite der
Spalten kann durch die Methode SetColumnWidth
gesetzt werden.
LBS_MULTIPLESEL
Es können mehrere Einträge selektiert werden. Ein
selektierter
Eintrag
kann
durch
nochmaliges
Auswählen deselektiert werden.
LBS_NOINTEGRALHEIGHT Die ListBox besitzt die Größe, die durch die
Anwendung gegeben ist. Normalerweise setzt
Windows die Größe so, dass keine Einträge
abgeschnitten werden.
LBS_NOREDRAW
Die ListBox wird bei einer Änderung nicht neu
ausgegeben.
LBS_NOTIFY
Das Vaterfenster erhält eine Eingabenachricht wenn
immer der Benutzer einen Klick oder Doppelklick auf
einen Eintrag ausführt.
LBS_OWNERDRAWFIXED
Der Besitzer der ListBox ist für die Ausgabe der
ListBox selbst verantwortlich. Alle Einträge besitzen
dieselbe Höhe.
LBS_OWNERDRAWVARIABLE Der Besitzer der ListBox ist für die Ausgabe der
ListBox selbst verantwortlich. Alle Einträge können
unterschiedliche Höhen besitzen.
LBS_SORT
Die Elemente werden alphabetisch sortiert aufgelistet.
Seite 92
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Die Elemente werden alphabetisch sortiert, das
Vaterfenster erhält eine Eingabenachricht wenn der
Benutzer einen Klick oder Doppelklick auf einen
Eintrag ausführt und die ListBox erhält einen Rahmen
an allen vier Seiten.
LBS_USETABSTOPS
Erlaubt einer ListBox Tabs, die in einem String
vorhanden sind, auszugeben.
LBS_WANTKEYBOARDINPUT Der Besitzer der ListBox erhält, solange sie den Fokus
besitzt, die Nachrichten WM_VKEYTOITEM oder
WM_CHARTOITEM wann immer eine Taste gedrückt
wird.
LBS_DISABLENOSCROLL
Besitzt eine ListBox nicht genügend Einträge, wird eine
disablete vertikaler Scrollbar angezeigt. Sonst
verschwindet die Scrollbar.
LBS_STANDARD
Alle ListBox-Nachrichten werden in Tabelle 11 aufgeführt. Sie besitzen als Präfix
LBN_ (ListBox-Notifications).
Tabelle 11: Benachrichtigungen des ListBox-Controls
Nachricht
LBN_DBCLK
LBN_ERRSPACE
LBN_KILLFOCUS
LBN_SELCANCEL
LBN_SELCHANGE
LBN_SETFOCUS
Beschreibung
Zeigt an, dass ein Eintag in einer ListBox doppelt geklickt
wurde.
Zeigt an, dass die ListBox nicht mehr Speicher für einen
Request allokieren kann.
Zeigt an, dass die ListBox den Eingabefokus verloren hat.
Zeigt an, dass der Benutzer einen Eintrag cancelt.
Zeigt an, dass sich der selektierte Eintrag geändert hat.
Zeigt an, dass die ListBox den Eingabefokus erhalten hat.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 93
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Im folgenden Beispiel wird eine ListBox für das Auflisten von Personennamen
verwendet. Die Personendaten werden in der Klasse CPerson gekapselt, die für
jedes Attribut eine lesende Methode besitzt. Die Klasse CPerson ist wie folgt
implementiert:
Die Deklaration der Klasse CPerson:
#include “stdafx.h”
class CPerson : public CObject
{
public:
CString GetStrasse();
CString GetVornamen();
CString GetNamen();
CPerson(CString name,CString vorname,CString strasse);
virtual ~CPerson();
protected:
CString m_Strasse;
CString m_Vornamen;
CString m_Namen;
};
Die Klasse hält die Attribute m_Strasse, m_Vornamen und m_Namen. Sie sind
jeweils vom Typ CString. Zusätzlich besitzt die Klasse für jedes Attribut eine
Lesemethode, die den Wert des Attributs zurückgibt.
Die Definition der Klasse CPerson:
#include “Person.h”
CPerson::CPerson(CString namen, CString vornamen, CString strasse)
{
m_Namen = namen;
m_Vornamen = vornamen;
m_Strasse = strasse;
}
CPerson::~CPerson()
{
}
CString CPerson::GetNamen()
{
return m_Namen;
}
CString CPerson::GetVornamen()
{
return m_Vornamen;
}
CString CPerson::GetStrasse()
{
return m_Strasse;
}
Damit die Klasse in einem überschaubaren Bereich bleibt, wurde der Konstruktor so
gewählt, dass alle Attribute mit ihm gesetzt werden können. Die Schreibmethode
aber weggelassen wurden. Es kann also nie eine Person in eine andere Straße
umziehen.
Seite 94
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Die Klasse CMainFrame wird wie folgt deklariert:
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
void ListBoxAufraeumen();
void FuelleListBox();
protected:
int OnCreate(LPCREATESTRUCT lpCreateStruct);
void OnClose();
void OnSelchangeListBox();
void OnDblclkListBox();
// Attribute
protected:
CListBox* m_pListBox;
DECLARE_MESSAGE_MAP()
};
Als Attribut besitzt sie einen Pointer (m_pListBox) auf eine ListBox. Die
Operationen sind einmal für die Nachrichten und zum anderen für das Füllen
(FuelleListBox) und das Aufräumen (ListBoxAufraeumen) der ListBox.
Im Konstruktor wird ein Objekt der Klasse CListBox auf dem Heap erzeugt und der
Membervariable m_pListBox zugewiesen.
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pListBox = new CListBox;
}
CMainFrame::~CMainFrame()
{
delete m_pListBox;
}
Im Destruktor wird das ListBox-Objekt wieder auf dem Heap freigegeben.
Nachdem die Nachricht WM_CREATE gesendet wurde, wird die zugeordnete Methode
OnCreate aufgerufen und ausgeführt.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect rect(10,10,260,260);
m_pListBox->Create(WS_VISIBLE | WS_BORDER |
LBS_NOTIFY | LBS_HASSTRINGS | LBS_SORT,
rect,this,IDC_LISTBOX);
FuelleListBox();
return 0;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 95
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Es wird ein Rechteck erzeugt, das die Koordinaten besitzt, die später die ListBox
erhalten soll. In der Create-Methode der ListBox wird der Style, das Rechteck, das
Vaterfenster und die ID, hier IDC_LISTBOX, übergeben. Die ID ist wiederum durch
#define IDC_LISTBOX
100
definiert. Durch den Style ist die ListBox nach dem Erstellen sichtbar (WS_VISIBLE)
und erhält einen Rahmen (WS_BORDER). Die Einträge enthalten einen String
(LBS_HASSTRINGS) und werden sortiert (LBS_SORT) ausgegeben. Durch
LBS_NOTIFY wird das Vaterfenster benachrichtigt wenn der Benutzer einen Eintrag
ausgewählt hat.
Danach wird die Methode FuelleListBox aufgerufen.
void CMainFrame::FuelleListBox()
{
CPerson* pPerson;
int index;
CString nachVorNamen;
pPerson = new CPerson(“Mayer”,”Karl”,”Stuttgarter Str”);
nachVorNamen = pPerson->GetNamen();
nachVorNamen += “, “;
nachVorNamen += pPerson->GetVornamen();
index = m_pListBox->AddString(nachVorNamen);
m_pListBox->SetItemDataPtr(index,pPerson);
pPerson = new CPerson(“Schmitt”,”Heidi”,”Frankfurter Str”);
nachVorNamen = pPerson->GetNamen();
nachVorNamen += “, “;
nachVorNamen += pPerson->GetVornamen();
index = m_pListBox->AddString(nachVorNamen);
m_pListBox->SetItemDataPtr(index,pPerson);
pPerson = new CPerson(“Nullblicker”,”Hugo”,”Flandernstr”);
nachVorNamen = pPerson->GetNamen();
nachVorNamen += “, “;
nachVorNamen += pPerson->GetVornamen();
index = m_pListBox->AddString(nachVorNamen);
m_pListBox->SetItemDataPtr(index,pPerson);
}
Der fett dargestellte Block wiederholt sich für jeden Eintrag. Es werden insgesamt
drei Personen eingefügt. Zuerst wird ein CPerson-Objekt auf dem Heap erzeugt und
mit den Werten initialisiert. Danach wird der String nachVorNamen, der vom Typ
CString ist, aus Namen und Vornamen zusammen gesetzt. Der zusammengesetzte
String wird nun dem Objekt m_pListBox mit der Methode AddString übergeben.
Die Methode hat als Rückgabewert einen int-Wert, der als Index dient. Über den
Index kann man durch die Methode SetItemDataPtr einen Pointer dem Eintrag
zuordnen.
Seite 96
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Damit der Heap nach dem Beenden der Anwendung wieder völlig geleert wird, muss
jedes CPerson-Objekt gelöscht werden. Dies geschieht in der Methode
ListBoxAufraeumen.
void CMainFrame::ListBoxAufraeumen()
{
CPerson* pPerson;
int index;
int anzahl = m_pListBox->GetCount();
for(index = 0;index < anzahl;index++)
{
pPerson = (CPerson*)m_pListBox->GetItemDataPtr(0);
m_pListBox->DeleteString(0);
delete pPerson;
}
}
Es wird durch die Methode GetCount festgestellt, wieviel Elemente in der ListBox
enthalten sind. Es wird danach in einer for-Schleife jedes Element einzeln aus der
ListBox gelöscht. Zuerst wird von der Position 0 der Datenpointer geholt und in der
lokalen Variable pPerson abgespeichert. Danach wird der Eintrag an der Position 0
durch die Methode DeleteString gelöscht. Die Methode löscht nur den Eintrag
aus der ListBox, den zugeordneten Pointer aber nicht !!! Es muss deshalb noch mit
delete pPerson das Objekt vom Heap gelöscht werden. Es ist kein Fehler immer
das erste Element (Position 0) in der ListBox zu löschen. Wurde das erste gelöscht,
rückt automatisch das zweite Element an die Stelle des ersten. Die Zählweise hängt
mit der Array-Indizierung von C zusammen.
Die Methode ListBoxAufraeumen wird in der Methode OnClose aufgerufen.
OnClose ist der Nachricht WM_CLOSE zugeordnet und wird dann aufgerufen, wenn
ein Fenster geschlossen wird. Hier können dann noch die letzten Aufräumarbeiten
gemacht werden, bei denen noch gültige Fenster zur Verfügung stehen müssen. Wie
hier kann die ListBox nur noch geleert werden, solange die ListBox ein gültiges
Fensterhandle besitzt. Möchte man die ListBox im Destruktor leeren, ist dies nicht
mehr möglich, da dort der Fensterhandle nicht mehr gültig ist. D.h. das Fensterobjekt
nicht mehr existiert.
void CMainFrame::OnClose()
{
ListBoxAufraeumen();
CWnd::OnClose();
}
Damit auf die Nachrichten reagiert werden
Nachrichtentabelle wie folgt erzeugt werden.
kann,
muss
wiederum
die
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_WM_CLOSE()
ON_LBN_SELCHANGE(IDC_LISTBOX, OnSelchangeListBox)
ON_LBN_DBLCLK(IDC_LISTBOX, OnDblclkListBox)
END_MESSAGE_MAP()
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 97
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Wie aus der Nachrichtentabelle zu erkennen ist, muss auf die Nachrichten
LBN_SELCHANGE und LBN_DBCLK reagiert werden. Diese werden auf die Methoden
OnSelchangeListBox und OnDblclkListBox umgesetzt.
void CMainFrame::OnSelchangeListBox()
{
CPerson* pPerson;
int index;
index = m_pListBox->GetCurSel();
pPerson = (CPerson*)m_pListBox->GetItemDataPtr(index);
CString str;
str = “Es wurde “;
str += pPerson->GetVornamen();
str += “ “;
str += pPerson->GetNamen();
str += “ ausgewählt !”;
MessageBox(str,”ListBox”,MB_OK);
}
void CMainFrame::OnDblclkListBox()
{
CPerson* pPerson;
int index;
index = m_pListBox->GetCurSel();
pPerson = (CPerson*)m_pListBox->GetItemDataPtr(index);
CString str;
str = pPerson->GetNamen();
str += “, “;
str += pPerson->GetVornamen();
str += “ wohnt in der “;
str += pPerson->GetStrasse();
str += “.”;
MessageBox(str,”ListBox”,MB_OK);
}
In beiden Methoden wird zuerst mit der Methode GetCurSel der Index des
selektierten Eintrags bestimmt. Danach wird durch die Methode GetItemDataPtr
das zugeordnete Datum geholt und in der Variable pPerson gespeichert. Nun wird
ein String aus Vornamen und Namen zusammengesetzt und eine MessageBox wie
in Abbildung 46 geöffnet. Bei der anderen Methode wird ein String aus Namen,
Vornamen und Straße gebildet und in einer MessageBox wie in Abbildung 47
ausgegeben.
Abbildung 46: MessageBox nach
Selektieren
Abbildung 47: MessageBox nach
Doppelklick
Seite 98
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Beim Testen erscheint immer nur die MessageBox aus Abbildung 46. Dies liegt
daran, dass vor einem Doppelklick immer die Nachricht EN_SELCHANGE gesendet
wird. Um die MessageBox aus Abbildung 47 zu erhalten, müssen die Anweisungen
der Methode OnSelchangeListBox auskommentiert werden.
Wird die Anwendung gestartet, erhält man ein Fenster wie in Abbildung 48.
Abbildung 48: ListBox mit sortierten Namen
5.5.2 Benutzerdefinierte ListBox
Bei benutzerdefinierten Elementen hat der Programmierer die Aufgabe, die Elemente
selbständig auszugeben. Dies soll hier Ansatzweise mit einer benutzerdefinierten
ListBox geschehen.
Eine benutzerdefinierte ListBox wird beim Generieren durch die Flags
LBS_OWNERDRAWFIXED oder LBS_OWNERDRAWVARIABLE festgelegt. Wird das erste
Flag verwendet, besitzen alle Elemente, die in der ListBox ausgegeben werden
sollen, eine feste Höhe. Bei dem anderen Flag kann jedes Element seine eigene
Höhe besitzen. Die Höhe der Elemente muss nun in der virtuellen Methode
MeasureItem gesetzt werden. Bei der festen Höhe, wird die Methode nur einmal
aufgerufen und steht danach fest. Bei der variablen wird vor jeder Ausgabe eines
Elements die Höhe abgefragt. Es ist deshalb notwendig, dass eine neue, von
CListBox abgeleitet Klasse erstellt und die Methode MeasureItem überschrieben
wird.
void CMyListBox::MeasureItem( LPMEASUREITEMSTRUCT lpMeasureItemStruct )
{
lpMeasureItemStruct->itemHeight = 20;
}
In der MEASUREITEMSTRUCT-Struktur wird der Control-Typ, die Control-ID, die ItemID, bei einer ListBox ist dies der Index, die Item-Höhe und Weite und die Item-Daten,
die dem Element zugeordnet wurden, übergeben. Es muss nun der Item-Höhe
itemHeight die Höhe der Elemente in Pixel angegeben werden. Der Control-Typ
enthält eine Konstante, um welches Control es sich handelt (ODT_BUTTON,
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 99
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
ODT_COMBOBOX, ODT_LISTBOX, ODT_MENU, ODT_LISTVIEW, ODT_STATIC,
ODT_TAB).
Zusätzlich muss in der neuen Klasse auch die Methode DrawItem überschrieben
werden. In ihr muss jedes Element einzeln ausgegeben werden.
void CMyListBox::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct )
{
CPerson* pPerson = (CPerson*) lpDrawItemStruct->itemData;
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);
dc.TextOut(lpDrawItemStruct->rcItem.left,
lpDrawItemStruct->rcItem.top,
pPerson->GetNamen());
}
Die Struktur DRAWITEMSTRUCT enthält den Control-Typ, die Control-ID, die Item-ID
(Index bei ListBox und ComboBox), die Item-Aktion, der Item-Status, ein Item,Handle, einen Handle auf einen Device-Context, ein Rechteck, das die
Ausgabenkoordinaten besitzt und die Item-Daten. Die Item-Aktion teilt mit, was mit
dem Eintrag getätigt wurde (ODA_DRAWENTIRE, ODA_FOCUS, ODA_SELECT). Aus
dem Item-Status wird bestimmt, wie der Eintrag dargestellt werden soll
(ODS_GRAYED, ODS_CHECKED, ODS_DISABLED, ODS_FOCUS, ODS_GRAYED,
ODS_SELECTED, ODS_COMBOBOXEDIT, ODS_DEFAULT). Das Rechteck besitzt die
gültigen Koordinaten, innerhalb diesen ist der Device-Context gültig und es können
graphische Elemente wie Text oder Linien ausgegeben werden.
Im Beispiel soll eine ListBox ausgegeben werden, die wie im vorhergehenden
Beispiel eine Liste von Personen enthält. Zuerst wird das Datenelement itemData
der Struktur auf einen Pointer von CPerson gecastet. Danach wird ein DeviceContext-Objekt mit Hilfe der Methode Attach angelegt, der ein Handle auf einen
Device-Context übergeben werden muss. Zum Schluß kann auf dem Device-Context
mit der Methode TextOut ein Text ausgegeben werden. Die Koordinaten werden
durch die linke obere Ecke des Rechteckes, das in der Struktur übergeben wurde,
festgelegt. Der Text besteht aus dem Namen der Person, dieser wird über die
Methode GetNamen erfragt.
In dem Beispiel wird nun noch nicht berücksichtigt, dass ein Element den Fokus hat
oder markiert dargestellt werden müßte. Hierfür müßte man die einzelnen Zustände
abfangen und je nach dem einen anderen Hintergrund setzen.
5.6 CComboBox
Eine ComboBox hat drei unterschiedliche Darstellungsarten. Eine einfache
ComboBox die aus einem Eingabefeld und einer ListBox besteht. Eine DropdownComboBox, die ebenfalls aus einem Eingabefeld und zusätzlich aus einer
aufklappbaren ListBox besteht. Als drittes gibt es die Dropdown-Liste, die nur ein
statisches Feld besitzt, indem der ausgewählte Eintrag angezeigt wird und zusätzlich
aus einer aufklappbaren ListBox. Alle ComboBoxen werden von der Klasse
CComboBox verwaltet.
Seite 100
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Alle ComboBox-Flags werden in Tabelle 12 aufgeführt. Sie besitzen als Präfix CBS_
(ComboBox-Styles).
Tabelle 12: Styles des ComboBox-Controls
Style
CBS_AUTOHSCROLL
Beschreibung
Verschiebt den Text im Eingabefeld an das Ende,
sobald der Benutzer eine Eingabe macht. Ist dieses
Flag nicht gesetzt, sind nur die Eingaben zulässig, die
in das Feld passen.
CBS_DROPDOWN
Ähnlich wie CBS_SIMPLE, die ListBox wird aber erst
dann angezeigt, wenn der Benutzer das Icon neben
dem Editfeld anklickt.
CBS_DROPDOWNLIST
Ähnlich wie CBS_DROPDOWN, das Editfeld wurde aber
durch ein statisches Textfeld ersetzt, in dem der
ausgewählte Eintrag angezeigt wird.
CBS_HASSTRINGS
Den
Elementen
einer
benutzergezeichneten
ComboBox wird ein Text zugeordnet, der über
GetText abgefragt werden kann.
CBS_OEMCONVERT
Der, in dem ComboBox Editfeld eingegebene Text,
wird von ANSI-Zeichen in OEM-Zeichen und wieder zu
ANSI-Zeichen konvertiert. Dieses Flag ist sehr nützlich,
wenn die ComboBox Dateinamen enthält. Ist nur mit
dem Flag CBS_SIMPLE oder CBS_DROPDOWN zulässig.
CBS_OWNERDRAWFIXED
Der Besitzer der ComboBox ist für die Ausgabe der
ComboBox selbst verantwortlich. Alle Einträge
besitzen dieselbe Höhe.
CBS_OWNERDRAWVARIABLE Der Besitzer der ComboBox ist für die Ausgabe der
ComboBox selbst verantwortlich. Alle Einträge können
unterschiedliche Höhen besitzen.
CBS_SIMPLE
Die ListBox wird immer angezeigt. Der ausgewählte
Eintrag in der ListBox wird im Editfeld angezeigt.
CBS_SORT
Die Elemente werden alphabetisch sortiert aufgelistet.
CBS_DISABLENOSCROLL
Besitzt eine ComboBox nicht genügend Einträge, wird
eine disablete vertikale Scrollbar angezeigt. Sonst
verschwindet die Scrollbar.
CBS_NOINTEGRALHEIGHT Die ComboBox besitzt die Größe, die durch die
Anwendung gegeben ist. Normalerweise setzt
Windows die Größe so, dass keine Einträge
abgeschnitten werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 101
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Alle ComboBox-Nachrichten werden in Tabelle 13 aufgeführt. Sie besitzen als Präfix
CBN_ (ComboBox-Notifications).
Tabelle 13: Benachrichtigungen des ComboBox–Controls
Nachricht
CBN_CLOSEUP
CBN_DBCLK
CBN_DROPDOWN
CBN_EDITCHANGE
CBN_EDITUPDATE
CBN_ERRSPACE
CBN_KILLFOCUS
CBN_SELCHANGE
CBN_SELENDCANCEL
CBN_SELENDOK
CBN_SETFOCUS
Beschreibung
Zeigt an, dass die ListBox geschlossen wurde.
Zeigt an, dass auf ein Eintag in einer ComboBox doppelt
geklickt wurde.
Zeigt an, dass die ListBox sichtbar gemacht wird.
Zeigt an, dass der Benutzer eine Aktion ausgeführt hat, die
den Text im Editfeld änderte. Die Nachricht wird nach dem
Ausgeben gesendet.
Zeigt an, dass das Editfeld den geänderten Text ausgibt. Die
Nachricht wird nach dem Formatieren des Textes und vor
dem Darstellen des Textes gesendet.
Zeigt an, dass die ComboBox nicht mehr Speicher für einen
Request allokieren kann.
Zeigt an, dass die ComboBox den Eingabefokus verloren
hat.
Zeigt an, dass sich der selektierte Eintrag geändert hat.
Zeigt an, dass der Benutzer ein Eintrag angewählt hat, dann
aber ein anderes Steuerelement selektiert oder den Dialog,
in dem sich die ComboBox befindet, schließt.
Zeigt an, das der Benutzer einen Eintrag aus der Liste
markiert hat oder einen Eintrag gewählt hat und danach die
ListBox geschlossen wurde.
Zeigt an, dass die ComboBox den Eingabefokus erhalten
hat.
Einer ComboBox kann, wie bei einer ListBox auch, ein long-Wert jedem Eintrag
zugeordnet werden. Auf dies wird hier aber nicht weiters eingegangen. Und kann im
Kapitel 5.5 – CListBox - nachgelesen werden.
Im Beispiel soll durch eine ComboBox die Auswahl von Farben ermöglicht werden.
Das hierfür erstellte Fenster kann dann wie in Abbildung 49 aussehen.
Abbildung 49: Eine geöffnete ComboBox
Seite 102
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Für das Erstellen einer ComboBox wird in der Klasse CMainFrame ein Attribut vom
Typ CComboBox angelegt.
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
void FuelleComboBox();
protected:
int OnCreate(LPCREATESTRUCT lpCreateStruct);
void OnSelchangeComboBox();
void OnDropdownComboBox();
// Attribute
protected:
CComboBox* m_pComboBox;
DECLARE_MESSAGE_MAP()
};
Zusätzlich werden alle notwendigen Methoden deklariert. Im Konstruktor wird ein
dynamisches Objekt von der Klasse CComboBox auf dem Heap erzeugt.
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pComboBox = new CComboBox;
}
CMainFrame::~CMainFrame()
{
delete m_pComboBox;
}
Im Destruktor wird das dynamische Objekt wieder zerstört und freigegeben. Die
OnCreate-Methode wird aufgerufen nachdem die Nachricht WM_CREATE gesendet
wurde.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect rect(10,10,160,130);
m_pComboBox->Create(WS_BORDER | WS_VISIBLE |
CBS_HASSTRINGS | CBS_DROPDOWNLIST,
rect,this,IDC_COMBOBOX);
FuelleComboBox();
return 0;
}
void CMainFrame::FuelleComboBox()
{
m_pComboBox->AddString(“rot”);
m_pComboBox->AddString(“grün”);
m_pComboBox->AddString(“blau”);
m_pComboBox->AddString(“gelb”);
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 103
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
In der OnCreate-Methode wird die ComboBox durch den Aufruf Create erzeugt.
Als Style bekommt die ComboBox einen Rahmen (WS_BORDER) und wird sichtbar
dargestellt (WS_VISIBLE). Zusätzlich wird das Flag CBS_HASSTRINGS gesetzt und
der ComboBox-Typ CBS_DROPDOWNLIST gewählt. Weiter muss ein Rechteck mit
den Koordinaten der ComboBox, das Vaterfenster und die ID, hier IDC_COMBOBOX,
übergeben werden. Die ID wird mit
#define IDC_COMBOBOX
100
definiert.
Danach wird die Methode FuelleComboBox aufgerufen. In dieser Methode werden
durch die Aufrufe AddString jeweils ein neuer Eintrag in die ComboBox
eingetragen.
Um auf Nachrichten reagieren zu können, muss wieder eine Nachrichtentabelle
eingerichtet werden:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_CBN_SELCHANGE(IDC_COMBOBOX, OnSelchangeComboBox)
ON_CBN_DROPDOWN(IDC_COMBOBOX, OnDropdownComboBox)
END_MESSAGE_MAP()
Durch das Makro ON_CBN_SELCHANGE wird die Methode OnSelchangeComboBox
des Vaterfenster aufgerufen und mitgeteilt, dass der Benutzer ein Eintrag ausgewählt
hat. Mit dem Makro ON_CBN_DROPDOWN wird das Anklicken auf den DropdownButton der ComboBox an die Methode OnDropdownComboBox weitergeleitet.
void CMainFrame::OnSelchangeComboBox()
{
CString farbe;
m_pComboBox->GetWindowText(farbe);
CString str;
str = “Es wurde die Farbe “;
str += farbe;
str += “ gewählt.”;
MessageBox(str,”ComboBox”,MB_OK);
}
void CMainFrame::OnDropdownComboBox()
{
MessageBeep(0);
}
In der Methode OnSelchangeComboBox wird der ausgewählte Eintrag durch die
Methode GetWindowText von der ComboBox geholt. Danach wird ein String
zusammengestellt und in einer MessageBox ausgegeben.
In der Methode OnDropdownComboBox wird nur ein Piepton ausgegeben, der dem
Benutzer mitteilen soll, dass die ListBox geöffnet wurde.
Seite 104
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Beim Beenden der Anwendung muss nicht wie im vorhergehenden Beispiel in Kapitel
5.5 die Item-Daten gelöscht werden, da gar keine erzeugt und zugeordnet wurden.
Wird eine benutzerdefinierte ComboBox benötigt, so ist die Vorgehensweise gleich
wie bei der ListBox.
5.7 CTreeCtrl
Alle TreeCtrl-Flags werden in Tabelle 14 aufgeführt. Sie besitzen als Präfix TVS_
(Tree View-Styles).
Tabelle 14: Styles des Tree-Controls
Style
TVS_HASLINES
TVS_LINESATROOT
TVS_HASBUTTONS
TVS_EDITLABELS
TVS_SHOWSELALWAYS
TVS_DISABLEDRAGDROP
Beschreibung
Der Baum besitzt Kanten, die die Kinderknoten mit
dem Vaterknoten verbinden
Die Kinderknoten besitzen Kanten, die mit der Wurzel
verbunden sind.
Jeder Vaterknoten besitzt auf der linken Seite vom
Eintrag einen Button.
Der Namen des Eintrags kann geändert werden.
Der ausgewählte Knoten wird weiterhin markiert, auch
wenn der TreeCtrl den Fokus verliert.
Der
TreeCtrl
sendet
keine
TVN_BEGINDRAG
Nachrichten.
Alle TreeCtrl-Nachrichten werden in Tabelle 15 aufgeführt. Sie besitzen als Präfix
TVN_ (Tree View-Notifications).
Tabelle 15: Benachrichtigungen des Tree-Controls
Nachricht
TVN_BEGINDRAG
TVN_BEGINLABELEDIT
TVN_BEGINRDRAG
TVN_DELETEITEM
TVN_ENDLABELEDIT
TVN_GETDISPINFO
TVN_ITEMEXPANDED
TVN_ITEMEXPANDING
TVN_KEYDOWN
Beschreibung
Zeigt an, dass durch die linke Maustaste eine Drag &
Drop Aktion initiiert wurde.
Zeigt an, dass der Namen eines Eintrags editiert wird.
Zeigt an, dass durch die rechte Maustaste eine Drag &
Drop Aktion initiiert wurde.
Zeigt an, dass ein Eintrag gelöscht werden soll.
Zeigt an, dass der Namen eines Eintrags editiert wurde.
Das Vaterfenster wird gefragt, wie der Eintrag dargestellt
oder sortiert werden soll.
Zeigt an, dass die Kinderknoten eines Vaterknoten
dargestellt oder versteckt wurden.
Zeigt an, dass die Kinderknoten eines Vaterknoten
dargestellt oder versteckt werden sollen.
Zeigt an, dass der Benutzer eine Taste gedrückt hat und
der TreeCtrl den Eingabefokus besitzt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 105
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
TVN_SELCHANGED
TVN_SELCHANGING
TVN_SETDISPINFO
Zeigt an, dass die Auswahl von einem Konten auf einen
anderen Knoten stattgefunden hat.
Zeigt an, dass die Auswahl von einem Konten auf einen
anderen Knoten stattfinden wird.
Zeigt an, dass das Vaterfenster die Information für einen
Knoten erneuern muss.
Im folgenden Beispiel werden in einem Baum unterschiedliche Fortbewegungsmittel
aufgeführt. Dabei wird vor jeden Eintrag ein Icon gesetzt.
Um ein Tree-Control mit Icons zu erzeugen, werden in der Klasse CMainFrame die
Membervariablen m_pTreeCtrl vom Typ CTreeCtrl und m_ImageList vom Typ
CImageList deklariert.
class CMainFrame : public CFrameWnd
{
// Operationen
public:
CMainFrame();
virtual ~CMainFrame();
void FuelleTreeCtrl();
int OnCreate(LPCREATESTRUCT lpCreateStruct);
// Attribute
protected:
CImageList m_ImageList;
CTreeCtrl* m_pTreeCtrl;
DECLARE_MESSAGE_MAP()
};
Im Konstruktor wird ein neues CTreeCtrl-Objekt auf dem Heap erzeugt, dass dann
im Destruktor wieder freigegeben wird.
CMainFrame::CMainFrame()
{
// ZU ERLEDIGEN: Hier Code zur Member-Initialisierung einfügen
m_pTreeCtrl = new CTreeCtrl;
}
CMainFrame::~CMainFrame()
{
delete m_pTreeCtrl;
}
Seite 106
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Um den Tree-Control zu generieren wird die WM_CREATE-Nachricht an CMainFrame
abgefangen und die Generierung in der Methode OnCreate implementiert.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
CRect rect(10,10,260,260);
m_pTreeCtrl->Create(WS_BORDER | WS_VISIBLE | TVS_DISABLEDRAGDROP |
TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS
,rect,this,IDC_TREECTRL);
m_ImageList.Create(IDB_BITMAP,10,3,ILC_COLORDDB);
m_pTreeCtrl->SetImageList(&m_ImageList,TVSIL_NORMAL);
FuelleTreeCtrl();
return 0;
}
In der Create-Methode muss der Style des Tree-Controls, die Koordinaten, das
Vaterfenster und die ID angegeben werden. Die ID ist IDC_TREECTRL und wird als
#define IDC_TREECTRL
100
definiert.
Das Tree-Fenster erhält einen Rahmen (WS_BORDER) und wird nach dem Erzeugen
sichtbar dargestellt (WS_VISIBLE). Die Elemente sind durch Linien verbunden
(TVS_HASLINES) und haben einen Button auf der linken Seite (TVS_HASBUTTONS)
sobald sie Kinder besitzen. Die Wurzel wird ebenfalls durch eine Linie verbunden
(TVS_LINESATROOT). Die Elemente können nicht durch „Drag and Drop“
verschoben werden (TVS_DISABLEDRAGDROP).
Danach wird eine ImageList erzeugt. Eine ImageList ist eine Liste von
aneinandergehängten Bitmaps, die dann über einen Index angesprochen werden
können. In der Methode Create kann eine Bitmap-Ressource (IDB_BITMAP)
angegeben werden. Zusätzlich muss die Breite eines einzelnen Bitmaps und eine
Farbenkonstante (ILC_COLORDDB) angegeben werden.
Die erzeugte ImageList muss nun mit dem Tree mit der Methode SetImageList
verbunden werden. Somit ist es möglich, dass beim Erzeugen von Einträgen nur der
Index eines Bitmaps angegeben werden muss.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 107
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Das Füllen des Baumes geschieht durch den Aufruf von FuelleTreeCtrl. In der
Methode werden einzelne Elemente eingetragen.
void CMainFrame::FuelleTreeCtrl()
{
HTREEITEM vaterItem;
HTREEITEM kindItem1;
HTREEITEM kindItem2;
HTREEITEM kindItem3;
vaterItem = m_pTreeCtrl->InsertItem(“Fortbewegungsmittel”,
0,0,TVI_ROOT,TVI_LAST);
kindItem1 = m_pTreeCtrl->InsertItem(“Luft”,1,1,vaterItem,TVI_LAST);
m_pTreeCtrl->InsertItem(“Flugzeug”,2,2,kindItem1,TVI_LAST);
kindItem2 = m_pTreeCtrl->InsertItem(“Wasser”,1,1,vaterItem,TVI_LAST);
m_pTreeCtrl->InsertItem(“Dampfer”,2,2,kindItem2,TVI_LAST);
m_pTreeCtrl->InsertItem(“Segelboot”,2,2,kindItem2,TVI_LAST);
kindItem3 = m_pTreeCtrl->InsertItem(“Land”,1,1,vaterItem,TVI_LAST);
m_pTreeCtrl->InsertItem(“PKW”,2,2,kindItem3,TVI_LAST);
m_pTreeCtrl->InsertItem(“LKW”,2,2,kindItem3,TVI_LAST);
m_pTreeCtrl->InsertItem(“Motorrad”,2,2,kindItem3,TVI_LAST);
m_pTreeCtrl->InsertItem(“Fahrrad”,2,2,kindItem3,TVI_LAST);
}
Mit InsertItem wird jeweils ein neues Element im Baum eingetragen. Hierfür muss
ein Name, der Index für das Bitmap im selektierten Zustand und der Index für das
Bitmap im nicht selektierten Zustand, den Vaterknoten und den Vorgängerknoten als
HTREEITEM angegeben werden. Es können wie hier auch Konstanten wie
TVI_ROOT oder TVI_LAST verwendet werden. Bei TVI_ROOT gibt es kein
Vaterknoten, da der Eintrag die Wurzel darstellen soll. Bei TVI_LAST wird der neue
Knoten an den Schluß angehängt. Weiter gibt es die Konstanten TVI_FIRST und
TVI_SORT. Mit TVI_FIRST werden die Knoten an den Anfang eingefügt. Bei
TVI_SORT werden die Knoten sortiert. Die Methode gibt ein HTREEITEM für den
gültigen Knoten zurück oder NULL wenn der Knoten nicht erzeugt werden konnte.
Wird die Anwendung gestartet, erhält man ein Fenster wie in Abbildung 50.
Abbildung 50: Tree-Control
Seite 108
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
5.8 CTabCtrl
Alle TabCtrl-Flags werden in Tabelle 16 aufgeführt. Sie besitzen als Präfix TCS_
(TabCtrl-Styles).
Tabelle 16: Styles des Tab-Controls
Style
TCS_BUTTONS
TCS_FIXEDWIDTH
Beschreibung
Die Tabs sehen aus wie Buttons.
Alle Tabs besitzen dieselbe Größe. Default wird die
Größe von jedem Tab angepaßt. Das Flag kann nicht
mit dem Flag TCS_RIGHTJUSTIFY verwendet werden.
TCS_FOCUSNEVER
Die Tabs erhalten nie den Eingabefokus.
TCS_FOCUSONBUTTONDOWN Ein Tab erhält den Eingabefokus wenn er gedrückt
wurde. Dieses Flag wird normalerweise mit dem Flag
TCS_BUTTONS verwendet.
TCS_FORCEICONLEFT
Das Icon wird im Tab auf der linken Seite ausgegeben,
der Namen bleibt zentriert. Default wird das Icon und
der Name zusammen zentriert ausgegeben. Dabei
erscheint das Icon links vom Namen.
TCS_FORCELABELLEFT
Das Icon und der Namen werden links ausgerichtet.
TCS_MULTILINE
Die Tabs werden in mehrere Reihen aufgeteilt. Alle
Tabs werden gleichzeitig angezeigt. Default wird eine
einzeilige Tabreihe erzeugt.
TCS_OWNERDRAWFIXED
Das Vaterfenster hat die Aufgabe die Tabs zu
zeichnen.
TCS_RIGHTJUSTIFY
Die Tabs werden auf die rechte Seite gesetzt. Default
erscheinen die Tabs auf der linken Seite.
TCS_SHAREIMAGELISTS
Die Imagelist vom TabCtrl wird nicht zerstört, wenn das
TabCtrl zerstört wird.
TCS_TOOLTIPS
Zu jedem Tab erscheint ein Tool Tip.
TCS_TABS
Tab ist neben Tab. Jeder Tab besitzt einen Rahmen.
Defaultstyle
TCS_SINGLELINE
Es wird eine Tab-Zeile angezeigt. Der Benutzer kann
durch die Tabs durchscrollen. Defaultstyle
TCS_RAGGEDRIGHT
Die Tab-Zeile wird nicht auf Breite des Controls
ausgeweitet. Defaultstyle.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 109
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Alle TabCtrl-Nachrichten werden in Tabelle 17 aufgeführt. Sie besitzen als Präfix
TCN_ (TabCtrl-Notifications).
Tabelle 17: Benachrichtigungen des Tab-Controls
Nachricht
TCN_KEYDOWN
TCN_SELCHANGE
TCN_SELCHANGING
Beschreibung
Zeigt an, dass eine Taste gedrückt wurde.
Zeigt an, dass das momentan gewählte Tab sich geändert
hat.
Zeigt an, dass das momentan gewählte Tab sich ändern
wird.
5.9 Menüs
Jede Windows-Anwendung besitzt mindestens ein Menü, das Hauptmenü. Dieses
Hauptmenü wird automatisch generiert, sobald die Anwendung mit dem ApplicationWizard erstellt wurde. Zusätzlich besitzen viele Anwendungen Popup-Menüs, die
durch das Betätigen der rechten Maustaste geöffnet werden. Menüs können, wie in
Kapitel 2.1.2.2 – Menü-Editor beschrieben, bearbeitet werden.
5.9.1 Das Hauptmenü
Das Hauptmenü erhält die Ressource-ID IDR_MAINFRAME. Um in einer Anwendung
das Hauptmenü einzubinden, muss die Zeile, in der das Hauptfenster
(Framewindow) generiert wird, durch die folgende Zeile ersetzt werden:
BOOL CControlApp::InitInstance()
{
...
pMainFrame->Create(NULL,
“Control”,
WS_OVERLAPPEDWINDOW,
CFrameWnd::rectDefault,
NULL,
MAKEINTRESOURCE(IDR_MAINFRAME));
...
}
Für das Laden des Menüs ist nur der 6. Parameter erforderlich. Als Parameter muss
hier der Name des Menüs als String angegeben werden. Durch das Makro
MAKEINTRESOURCE() wird die Ressource-ID IDR_MAINFRAME in den String
gewandelt.
Um auf eine Menü-Nachricht reagieren zu können muss nun für jeden Menüpunkt
eine Zuordnung zu einer Methode in der Nachrichtentabelle hergestellt werden. Für
das Beispiel wurde das Hauptmenü um folgendes Untermenü, wie in Abbildung 51
erweitert.
Seite 110
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Abbildung 51: Untermenü
Soll auf den Menüpunkt „MessageBox öffnen“ reagiert werden, so muss der
zugeordneten ID ID_MENU_MESSAGEBOX_OEFFNEN eine Methode zugeordnet
werden, hier OnMenuMessageBoxOeffnen. Dies geschieht, wie schon in Kapitel 4
erwähnt, in der Nachrichtentabelle.
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_MENU_MESSAGEBOX_OEFFNEN, OnMenuMessageBoxOeffnen)
END_MESSAGE_MAP()
Zusätzlich muss die Methode in der Header-Datei von CMainFrame deklariert und in
der cpp-Datei von CMainFrame definiert werden. In der Methode kann dann eine
MessageBox ausgegeben werden.
void CMainFrame::OnMenuMessageBoxOeffnen ()
{
// TODO: Code für Befehlsbehandlungsroutine hier einfügen
MessageBox(“Kommentar”,”MessageBox”,MB_OK);
}
Das Anlegen der Methoden wird natürlich nicht von Hand ausgeführt. Vielmehr wird
hier auf die Funktionalität des Klassen-Assistenten zurückgegriffen.
Bevor ein Menü geöffnet wird, wird für jeden einzelnen Menüpunkt das Hauptfenster
gefragt, wie der Menüpunkt dargestellt werden soll. D.h. der Programmierer hat hier
die Möglichkeit zu entscheiden, ob der Menüpunkt markiert oder grau dargestellt
werden soll. Um diese Nachricht abfangen zu können muss in der Nachrichtentabelle
folgende Zeile eingefügt werden:
ON_UPDATE_COMMAND_UI(ID_MENU_MESSAGEBOX_OEFFNEN,
OnUpdateMenuMessageBoxOeffnen)
Wie schon bei der bearbeitenden Methode der Nachricht muss auch hierfür eine
Methode eingefügt werden. Dieser Methode wird ein Objekt der Klasse CCmdUI
übergeben. Durch dieses Objekt kann der Zustand des Menüpunkts beeinflußt
werden. Um den Menüpunkt grau darzustellen muss für das Objekt die Methode
Enable(m_Enable) aufgerufen werden.
void CMainFrame::OnUpdateMenuMessageBoxOeffnen(CCmdUI* pCmdUI)
{
// TODO: Code für die Befehlsbehandlungsroutine zum
// Aktualisieren der Benutzeroberfläche hier einfügen
pCmdUI->SetEnable(m_Enable);
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 111
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
m_Enable ist eine boolsche Membervariable, die den Zustand des Menüpunktes
„MessageBox öffnen“ speichert. Sie kann z.B. durch die Menüpunkte „Disable“ und
„Enable“ manipuliert werden. Hierfür muss wieder in der Nachrichtentabelle die
Zuordnung zwischen ID und Methode stattfinden.
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_MENU_DISABLE, OnMenuDisable)
ON_COMMAND(ID_MENU_ENABLE, OnMenuEnable)
ON_COMMAND(ID_MENU_MESSAGEBOX_OEFFNEN, OnMenuMessageBoxOeffnen)
ON_UPDATE_COMMAND_UI(ID_MENU_MESSAGEBOX_OEFFNEN,
OnUpdateMenuMessageBoxOeffnen)
END_MESSAGE_MAP()
In der Methode OnMenuDisable kann nun die Membervariable m_Enable auf FALSE
gesetzt werden, um den Menupunkt zu disablen. Um den Menüpunkt wieder aktiv zu
schalten, kann in der Methode OnMenuEnable die Variable auf TRUE gesetzt
werden.
void CMainFrame::OnMenuDisable()
{
m_Enable = FALSE;
}
void CMainFrame::OnMenuEnable()
{
m_Enable = TRUE;
}
Weiter gibt es die Möglichkeit durch die Methode SetCheck den Menüpunkt mit
einem Häkchen oder durch die Methode SetRadio den Menüpunkt mit einen Punkt
zu markieren. Hierfür wird die Nachrichtentabelle wie folgt erweitert:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
...
ON_COMMAND(ID_MENU_HAEKCHEN, OnMenuHaekchen)
ON_UPDATE_COMMAND_UI(ID_MENU_HAEKCHEN, OnUpdateMenuHaekchen)
ON_COMMAND(ID_MENU_PUNKT, OnMenuPunkt)
ON_UPDATE_COMMAND_UI(ID_MENU_PUNKT, OnUpdateMenuPunkt)
...
END_MESSAGE_MAP()
In den Update-Methoden wird nun der Zustand für die Menüpunkte gesetzt.
void CMainFrame::OnUpdateMenuHaekchen(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_Haekchen);
}
void CMainFrame::OnUpdateMenuPunkt(CCmdUI* pCmdUI)
{
pCmdUI->SetRadio(m_Punkt);
}
In der Methode OnUpdateMenuHaekchen wird dem Menüpunkt „Häkchen“ in
Abhängigkeit der Membervariable m_Haekchen, die vom Typ BOOL ist, mit der
Anweisung pCmdUI->SetCheck(m_Haekchen) ein Häkchen gesetzt.
Seite 112
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
In der Methode OnUpdateMenuHaekchen wird der Menüpunkt „Punkt“ in
Abhängigkeit der Membervariable m_Punkt, die ebenfalls vom Typ BOOL ist, mit der
Anweisung pCmdUI->SetRadio(m_Punkt) ein Punkt gesetzt.
Die Membervariablen werden wiederum in den Methoden der Menüpunkte gesetzt:
void CMainFrame::OnMenuHaekchen()
{
// TODO: Code für Befehlsbehandlungsroutine hier einfügen
m_Haekchen = !m_Haekchen;
}
void CMainFrame::OnMenuPunkt()
{
// TODO: Code für Befehlsbehandlungsroutine hier einfügen
m_Punkt = !m_Punkt;
}
Hier wird bei jedem Aufruf des Menüpunktes die jeweilige Membervariable invertiert.
5.9.2 Ein Popup-Menü
Um ein Popup-Menü mit der rechten Maustaste zu öffnen, muss die Nachricht
WM_RBUTTONDOWN abgefangen werden. In der zugeordneten Methode kann dann
das Menü geöffnet werden.
Die Nachrichtentabelle muss nun wie folgt erweitert werden:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
...
ON_WM_RBUTTONDOWN()
...
END_MESSAGE_MAP()
Das Öffnen eines Popup-Menüs könnte dann wie folgt implementiert werden.
void CMainFrame::OnRButtonDown(UINT nFlags, CPoint point)
{
// TODO: Code für die Behandlungsroutine für Nachrichten hier
// einfügen und/oder Standard aufrufen
ClientToScreen(&point);
CMenu menuListe;
menuListe.LoadMenu(IDR_RIGHTBUTTON_MENU);
CMenu* pPopupMenu;
pPopupMenu = menuListe.GetSubMenu(0);
pPopupMenu->TrackPopupMenu(TPM_LEFTALIGN |
TPM_LEFTBUTTON,
point.x,point.y,this);
menuListe.DestroyMenu();
}
Durch
die
Methode
ClientToScreen wird
die
Mausposition
von
Fensterkoordinaten auf Bildschirmkoordinaten umgerechnet. Danach wird ein
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 113
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Controls
Menüobjekt erzeugt. Für das Menüobjekt wird ein Menü aus den Ressourcen mit der
Methode LoadMenu geladen. Als Parameter muss die ID der Menü-Ressource
angegeben werden und ist hier IDR_RIGHTBUTTON_MENU. Die Menü-Ressource
wird in Abbildung 52 dargestellt.
Abbildung 52: Popup-Menü
Mit LoadMenu wird eine ganze Menüleiste geladen. Um nun ein Untermenü zu
erhalten, wird ein Pointer auf ein Menüobjekt angelegt. Diesem Pointer wird durch
die Anweisung menuListe.GetSubMenu(0) ein Pointer auf das Untermenü
zugewiesen, das an der Position 0 steht. Durch die Methode TrackPopupMenu wird
nun das Popup-Menü geöffnet. Als Parameter muss hier die Ausrichtung des Menüs
(TPM_CENTERALIGN, TPM_LEFTALIGN, TPM_LEFTALIGN) und die Art der
Menüpunktauswahl (TPM_LEFTBUTTON, TPM_RIGHTBUTTON) übergeben werden,
d.h. mit welchem Mausbutton muss der Menüpunkt ausgewählt werden. Danach
müssen die Koordinaten des Menüs und ein Zeiger auf das Vaterfenster übergeben
werden. Zum Schluß muss das Menüobjekt wieder zerstört werden.
Wurde für den Menüpunkt „MessageBox öffnen...“ die gleiche ID, wie im
vorangegangenen Kapitel festgelegt, wird automatisch die, der ID zugeordneten
Methode, aufgerufen.
Seite 114
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
6 Dialoge
Mit Hilfe von Dialogen kann der Anwender mit der Applikation kommunizieren und
spezifische Einstellung für die Anwendung einstellen. Dialoge sind normale Fenster,
die verschoben und Nachrichten empfangen können.
Dialoge werden meistens mit Hilfe des Ressource-Editor der Entwicklungsumgebung
generiert. Dabei können Sie wie mit einem Graphik-Editor einzelne Steuerelemente
in einem Fenster plazieren. Jedem Steuerelement muss dabei eine eindeutige ID
zugewiesen werden, über die später das Programm auf das jeweilige Steuerelement
zugreifen kann.
Es werden zwischen den zwei Dialogarten modal und nicht modal unterschieden. Die
Unterschiede dieser beiden Dialogarten werden nun in den folgenden zwei Kapiteln
beschrieben.
Ein Dialog kann ebenfalls als dialogbasierte Applikation eingesetzt werden. Dabei
besteht das Hauptfenster aus einem Eingabeformular und ist für die gesamte
Anwendung immer gleich. Diese Anwendung wird in Kapitel 6.3 – Dialogbasierte
Anwendung beschrieben.
Um eine Dialog-Klasse zu erstellen, wird diese von der Klasse CDialog abgeleitet.
Die MFC stellt zusätzlich weitere voll funktionsfähige Dialoge zur Verfügung. Ein
Beispiel hierfür wäre die Klasse CFileDialog, in der der Standard Öffnen- und
Speicherndialog realisiert ist.
Die graphische Oberfläche kann dabei von Hand in der neuen Klasse implementiert
werden, dies ist aber nicht zu empfehlen. Vielmehr sollte der Ressource-Editor für
Dialoge verwendet werden. Mit diesem Editor können Steuerelemente auf der
Oberfläche gezeichnet werden.
6.1 Modale Dialoge
Modale Dialoge sind Dialoge, die die Hauptanwendung sperren. D.h. wurde ein
modaler Dialog geöffnet, können keine weitere Eingaben in den Fenstern der
Anwendung vorgenommen werden. Ein Beispiel wäre hierfür ein Open-Dialog.
Im weiteren soll nun an Hand eines Beispiels die Klasse CDialog erklärt werden.
Zuerst wird mit dem Ressource-Editor eine Dialog-Ressource erzeugt. Hierfür soll ein
Dialog generiert werden, mit dem es möglich ist, für eine Personalverwaltung
Personendaten einzugeben. Der Dialog könnte dann wie in Abbildung 53 aussehen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 115
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Abbildung 53: Modaler Dialog
Um eine Dialog-Ressource in einem Dialog einbinden zu können, muss für diese
Ressource eine neue Klasse implementiert werden, die von CDialog abgeleitet
wurde. Am schnellsten geschied dies durch den Klassen-Assistent. Wurde der
Klassen-Assistent aufgerufen öffnet sich ein Dialog (Hinzufügen einer Klasse) in dem
nachgefragt wird, ob eine neue Klasse erzeugt werden soll. Durch bestätigen des
Dialogs mit OK öffnet sich ein weiterer Dialog (Neue Klasse) in dem der Name der
neuen Dialog-Klasse eingegeben werden muss. Als Klassennamen wurde hier
CModalerDialog gewählt. Nach Abschluß dieses Dialogs erhält man den KlassenAssisten wie in Abbildung 54.
Abbildung 54: Der Klassen-Assistent
Mit dem Klassen-Assistent kann nun für den Reset-Button eine Methode für die
Nachricht BN_CLICKED generiert werden. Zusätzlich können für alle Eingabefelder
Variablen definiert werden. Dabei werden die Variablen für Vorname (m_Vorname)
und Nachname (m_Nachname) als CString, Gehalt (m_Gehalt) und
Personalnummer (m_Personalnr) als UINT definiert. Wird der Klassen-Assisten mit
OK beendet, so werden in der Klasse alle Methoden und Membervariablen deklariert.
Zusätzlich
definiert
der
Klassen-Assisten
die
Methode
DoDataExchange(CDataExchange* pDX), durch die der Austausch von Daten
zwischen den Membervariablen und den Steuerelementen realisiert wird.
Seite 116
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
6.1.1 Der Dialog-Daten-Austausch
Für den Datenaustausch zwischen Steuerelementen und den zugeordneten
Membervariablen
des
Dialogs
stellt
die
MFC
die
Methode
DoDataExchange(CDataExchange* pDX) zur Verfügung. Sie wurde in der
Klasse CWnd als virtuelle Methode definiert und wird durch den Aufruf von
UpdateData ausgeführt.
Innerhalb dieser Methode muss nun der Datenaustausch durch die Funktionen mit
dem Präfix DDX_ erfolgen. Im Beispiel tritt nur DDX_Text auf. Es gibt aber noch
weitere DDX_-Funktionen, die abhängig vom Steuerelement verwendet werden
müssen. Alle DDX_-Funktionen sind bidirektional, d.h. es können Werte von den
Membervariablen in die Steuerelemente oder von den Steuerelementen in die
Membervariablen geschrieben werden. Die Richtung des Austausches hängt von
dem Parameter von UpdateData ab. Ist dieser TRUE, werden die Werte von den
Steuerelementen in die Membervariablen geschrieben. Bei FALSE werden die
Steuerelemente initialisiert.
DDX_Text ist für den Datenaustausch zwischen einer Membervariablen vom Typ
CString und einem Eingabefeld zuständig. Als Parameter besitzt sie ein Pointer auf
ein CDataExchange Objekt, ein int-Wert für die ID des Steuerelements und einige
weitere Parameter. Sie wurde für 11 unterschiedliche Datentypen überladen. Das
CDataExchange Objekt enthält eine Membervariable m_bSaveAndValidate, die
über die Transferrichtung Auskunft gibt. Ihr wird der Parameter von UpdateData
zugewiesen.
Zusätzlich zum Datenaustausch zwischen Steuerelementen und Membervariablen
gibt es die Möglichkeit die Werte auf die Gültigkeit zu prüfen. Hierfür stehen die
Methoden mit dem Präfix DDV_ zur Verfügung.
Um eine Zahleneingabe zu prüfen, stellt die MFC die Methode DDV_MinMaxUInt
zur Verfügung. Als Parameter besitzt diese Methode einen Pointer auf ein Objekt der
Klasse CDataExchange, die Membervariable, die dem Steuerelement zugeordnet
wurde, den Minimalwert und den Maximalwert. Wird nun der Dialog mit „OK“
verlassen und ein Eingabefeld kann nicht validiert werden, wird eine MessageBox
geöffnet, mit der Sie aufgefordert werden, im Dialog einen gültigen Wert einzugeben.
Eine weitere Validierungsmethode ist DDV_MaxChars. Sie prüft bei einem
Eingabefeld für Zeichen die maximale Anzahl von Zeichen. Wurde diese erreicht,
können keine weiteren Zeichen eingegeben werden. Als Parameter besitzt sie
wiederum einen Pointer auf ein Objekt von CDataExchange, die zugeordnete
Membervariable und die maximale Anzahl der Zeichen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 117
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Für das Beispiel könnte die Methode folgendermaßen aussehen:
void CModalerDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CModalerDialog)
DDX_Text(pDX, IDC_PERSONALNR, m_Personalnr);
DDX_Text(pDX, IDC_GEHALT, m_Gehalt);
DDV_MinMaxUInt(pDX, m_Gehalt, 0, 20000);
DDX_Text(pDX, IDC_NACHNAME, m_Nachname);
DDV_MaxChars(pDX, m_Nachname, 20);
DDX_Text(pDX, IDC_VORNAME, m_Vorname);
DDV_MaxChars(pDX, m_Vorname, 20);
//}}AFX_DATA_MAP
}
Die Datenaustausch-Funktion muss nicht von Hand implementiert werden, viel mehr
sollte sie über das Propertysheet „Member-Variablen“ des Klassen-Assistenten
gefüllt werden.
6.1.2 Ein modaler Dialog starten
Modale und nicht modale Dialoge unterscheiden sich in der Implementierung
hauptsächlich nur im Dialogaufruf und der Lebenszeit eines Dialogs.
Um einen modalen Dialog zu erhalten, muss eine Instanz des Dialogs angelegt
werden. Zu diesem Zeitpunkt wird nur Speicherplatz für die Instanz reserviert, es ist
noch kein Dialogfenster vorhanden. Danach können die Membervariablen, wurden
sie als public: deklariert, direkt, sonst über Memberfunktionen initialisiert werden.
Wurden die Membervariablen initialisiert, muss durch die Memberfunktion DoModal
der Klasse CDialog der Dialog gestartet werden. Diese Funktion kehrt erst wieder
vom Dialog zurück, wenn dieser mit „OK“ oder „Abbrechen“ beendet wurde.
DoModal hat als Rückgabewert eine Integerzahl, die Auskunft gibt, wie der Dialog
verlassen wurde. Wurde der Dialog mit „OK“ beendet, ist der Rückgabewert die
Konstante IDOK, bei „Abbrechen“ ist der Wert IDCANCEL. Konnte der Dialog nicht
erzeugt werden oder trat ein anderer Fehler ein, wird –1 oder IDABORT
zurückgegeben.
Seite 118
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Hier nun ein Beispiel wie ein modaler Dialog geöffnet wird:
...
void CMainFrame::OnDialogModalerDialogOeffnen()
{
// TODO: Code für Befehlsbehandlungsroutine hier einfügen
CModalerDialog modalerDialog;
modalerDialog.m_Personalnr = m_AktuellePersonalnr;
modalerDialog.m_Gehalt = 0;
if(modalerDialog.DoModal() == IDOK)
{
CString str = modalerDialog.m_Vorname + “ “;
str += modalerDialog.m_Nachname;
MessageBox(str,”Meldung”,MB_OK);
}
else
MessageBox(
“Es wurden keine Daten eingegeben.”,
“Meldung”,MB_OK);
}
...
OnDialogModalerDialogOeffnen soll eine Antwortfunktion für ein Menüeintrag
sein und durch die Frameklasse CMainFrame bearbeitet werden.
Zuerst wird eine lokale Instanz (modalerDialog) der Klasse CModalerDialog
angelegt. Danach werden die Membervariablen (m_Personalnr, m_Gehalt) des
Dialogs mit Werten gefüllt. m_AktuellePersonalnr soll dabei eine UINTMembervariable von CMainFrame sein, die die aktuelle Personalnummer enthält.
Darauf wird der Dialog mit der Methode DoModal gestartet. Wird der Dialog mit „OK“
verlassen, wird eine lokale Variable str von CString angelegt, der Vorname und
Nachname in diese kopiert und in einer MessageBox ausgegeben. Wird der Dialog
abgebrochen, wird der else-Zweig ausgeführt. In diesem wird ebenfalls eine
MessageBox geöffnet, in der der Text „Es wurden keine Daten eingegeben.“
ausgegeben wird.
6.1.3 Nachrichtentabelle in einem Dialog
Wie in einem Fenster, kann auch in einem Dialog eine Nachrichtentabelle
implementiert werden. So ist es möglich verschiedene Steuerelemente im Dialog zu
plazieren und auf deren Nachrichten zu reagieren.
Wurde wie in Abbildung 53 ein Reset-Button implementiert, so muss der Klick des
Buttons im Dialog abgefangen werden, damit die Membervariablen und somit die
Eingabefelder zurückgesetzt werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 119
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Die geschieht durch folgenden C++-Code:
...
BEGIN_MESSAGE_MAP(CModalerDialog, CDialog)
//{{AFX_MSG_MAP(CModalerDialog)
ON_BN_CLICKED(IDC_RESET, OnReset)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
...
void CModalerDialog::OnReset()
{
// TODO: Code für die Behandlungsroutine der Steuerelement// Benachrichtigung hier einfügen
m_Vorname = “”;
m_Nachname = “”;
m_Gehalt = 0;
UpdateData(FALSE);
}
...
Zuerst wird die Nachrichtentabelle für die Klasse CModalerDialog definiert, in der
auf die Nachricht BN_CLICKED des Reset-Buttons reagiert wird. Dieser Nachricht
wird die Methode OnReset zugewiesen.
Darunter folgt die Implementierung der Methode OnReset. Sie weist den Attributen
m_Vorname und m_Nachname einen leeren String zu, dem Attribut m_Gehalt den
Wert 0. Danach wird die Methode UpdateData(FALSE) aufgerufen, dadurch
werden die Eingabefelder mit den neuen Werten initialisiert.
6.1.4 Initialisieren eines Dialogs.
Es gibt eine weitere Möglichkeit, Dialoge zu initialisieren. Das Windowssystem
sendet, nachdem das Dialog-Fenster erstellt wurde, die Nachricht WM_INITDIALOG.
D.h. wurde diese Nachricht gesendet, ist das Dialog-Fenster und allen darin
liegenden Steuerelementen gültig. Es können somit alle Steuerelemente
angesprochen werden. Als Beispiel könnte der Reset-Button disabled werden und
erst wieder aktiv geschaltet werden, wenn eine Änderung vorgenommen wurde.
...
BOOL CModalerDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Zusätzliche Initialisierung hier einfügen
m_Vorname = ““;
UpdateData(FALSE);
GetDlgItem(IDC_RESET)->EnableWindow(FALSE);
return TRUE;
// return TRUE unless you set the focus to a control
// EXCEPTION: OCX-Eigenschaftenseiten sollten FALSE
// zurückgeben
}
...
Seite 120
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
OnInitDialog ist eine virtuelle Methode von CDialog. Zuerst wird
OnInitDialog der Vaterklasse (CDialog) aufgerufen. Danach wird dem Attribut
m_Vorname eine leere Zeichenkette zugewiesen und die Methode UpdateData
aufgerufen.
Mit der Methode GetDlgItem, die von der Klasse CWnd geerbt wird, erhält man
einen Pointer auf ein Objekt der Klasse CWnd, das der ID IDC_RESET zugeordnet ist.
D.h. man erhält hier einen Pointer auf des Fensterobjekt von CButton. Durch die
Methode EnableWindow, die ebenfalls in der Klasse CWnd implementiert ist, können
Fensterobjekte aktiv oder inaktiv geschaltet werden. Mit dem Wert FALSE werden die
Fensterelemente deaktiviert, was bedeutet, dass sie grau dargestellt und keine
Eingaben angenommen werden.
Damit der Reset-Button zu einem späteren Zeitpunkt wieder aktiv geschaltet werden
kann, muss auf Änderungen der Eingabefelder reagiert werden. Dazu muss, wie in
Kapitel 5.3 beschrieben, die Nachricht EN_CHANGE abgefangen und in der Methode
OnChangeNachname bearbeitet werden. Die Methode hat dann folgendes
Aussehen:
void CModalerDialog::OnChangeNachname()
{
GetDlgItem(IDC_RESET)->EnableWindow(TRUE);
}
6.1.5 Der direkte Zugriff auf Steuerelemente
Ein Dialog wird normalerweise im Ressource-Editor erstellt. Es gibt somit keine
direkte Verbindung zwischen Ressource (Steuerelement) und Objekt im
Programmcode, mit Ausnahme von DDX und DDV.
Die MFC stellt in der Klasse CWnd die Funktion GetDlgItem („get dialog item“) zur
Verfügung. Als Parameter besitzt diese Funktion eine Integerzahl, die die RessourceID des Steuerelements repräsentiert. Als Rückgabewert erhält man einen Pointer auf
ein CWnd Objekt. Ist das tatsächliche Steuerelement ein Button und es muss auf die
Funktionalität eines Buttons zurückgegriffen werden, muss dieser Pointer noch auf
CButton* gecastet werden.
...
CButton* pButton = (CButton*)GetDlgItem(IDC_BUTTON_RESET);
UINT state = pButton->GetState();
...
Der Dialog hat einen Button mit der Ressource-ID IDC_BUTTON_RESET. Von diesem
Steuerelement soll der Status abgefragt werden (Focus, Highlighted). Mit Hilfe der
Funktion GetDlgItem wird ein Pointer auf ein CWnd-Objekt erzeugt und durch den
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 121
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Cast-Operator (CButton*) der lokalen Variable pButton zugewiesen. Danach
wird über GetState der Status des Buttons abgefragt.
6.1.6 Zuweisung einer abgeleiteten Klasse an ein Steuerelement (Subclassing)
Alle Steuerelemente werden durch die Controls (CButton, CEdit, CStatic, usw.)
der MFC verwaltet. Soll aber die Funktionalität eines Steuerelements erweitert
werden, muss die Klasse dieses Steuerelements abgeleitet und die neue
Funktionalität hinzugefügt werden. Zusätzlich muss eine Verknüpfung zwischen
Steuerelement und Instanz der Klasse hergestellt werden. Dies geschieht durch
„Subclassing“
Wurde ein Steuerelement durch „Subclassing“ einer anderen Klasse zugewiesen,
werden die Windowsnachrichten, die das Steuerelement sendet oder die das
Steuerelement empfängt, an das zugewiesene Objekt der Klasse weitergeleitet.
Das „Subclassing“ sollte in der OnInitDialog-Methode vonstatten gehen.
Als Beispiel soll ein Edit-Steuerelement verwendet werden. In diesem Editfeld sollen
nur hexadezimale Zahlen eingegeben werden können. Hierfür wird eine neue Klasse,
CHexEdit, von CEdit abgeleitet und die Funktionalität so erweitert, dass nur noch
Zahlen und die Buchstaben A bis F eingegeben werden können. Das neue Editfeld
soll in einem Dialog getestet werden. Hierfür wird die Klasse CHexDialog von
CDialog abgeleitet.
Im Header-File der Dialog-Klasse:
class CHexDialog : public CDialog
{
...
protected:
CHexEdit m_HexEdit;
...
};
Im Source-File der Dialog-Klasse
BOOL CHexDialog::OnInitDialog()
{
...
if(!m_HexEdit.SubclassDlgItem(IDC_EDIT_HEX,this))
MessageBox(„Subclassing konnte nicht durchgeführt
werden !“,
„Fehlermeldung“,
MB_OK | MB_ICONERROR);
...
}
Im Header-File des Dialogs muss zuerst eine Membervariable m_HexEdit von der
abgeleiteten Klasse CHexEdit deklariert werden. Im Source-File wird in der
Seite 122
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Methode OnInitDialog die Methode SubclassDlgItem der Klasse CWnd für das
Objekt der Klasse CHexEdit aufgerufen. Als Parameter müssen hier die RessourceID des zuzuweisenden Steuerelements und einen Pointer auf das Dialog-Fenster
(Vaterfenster) übergeben werden. Der Rückgabewert ist von Typ BOOL. Er ist TRUE,
wenn die Methode erfolgreich ausgeführt werden konnte. Im Beispiel wird auf das
Fehlschlagen abgeprüft und im Fehlerfall eine MessageBox ausgegeben.
6.1.7 Was geschieht zum Schluß
Werden Dialoge mit dem „OK“-Button beendet, wird in der Dialogklasse die virtuelle
Funktion OnOK aufgerufen. Besitzt der Dialog einen automatischen Datenaustausch
oder eine Validierung, so werden die Membervariablen automatisch upgedatet.
Zusätzlich wird das Dialogfenster zerstört, nicht aber das Objekt der Klasse CDialog
oder davon abgeleitete Klassen.
Soll die Funktionalität des „OK“-Button erweitert werden, so muss diese Funktion
überschrieben werden. Zum Schluß sollte die Methode OnOK die Vatermethode
aufrufen.
Das gleiche gilt für den „Abbrechen“-Button. Bei Abbrechen wird die virtuelle
Funktion OnCancel von der Klasse CDialog aufgerufen. Soll die Funktionalität
dieser Methode erweitert werden, muss sie in einer abgeleiteten Klasse
überschrieben werden. Damit die Grundfunktionalität der Methode erhalten bleibt,
muss die Methode der Vaterklasse aufgerufen werden.
6.2 Nicht modale Dialoge
Nicht modale Dialoge, sind Dialoge die die Hauptanwendung nicht sperren. D.h.
wurde ein nicht modaler Dialog geöffnet, können weiterhin Eingaben in den Fenstern
der Anwendung vorgenommen werden. Ein Beispiel wäre hierfür der Suchen-Dialog
von Word.
In diesem Kapitel werden nicht auf alle Eigenschaften eines Dialogs eingegangen, da
sie gleich sind wie für einen modalen Dialog. Der einzige Unterschied ist das
Aufrufen des Dialog.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 123
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
6.2.1 Ein nicht modaler Dialog starten
Ein nicht modaler Dialog wird nicht über DoModal geöffnet. Vielmehr wird beim
Starten der Anwendung eine Instanz des nicht modalen Dialogs erzeugt und später
nach dem Initialisieren der Steuerelemente nur noch sichtbar gemacht.
Im Beispiel wurde ein eigener Suchen-Dialog mit dem Ressource-Editor erstellt.
Abbildung 55 zeigt den nicht modalen Dialog.
Abbildung 55: Nicht modaler Dialog
Dem Dialog wurde eine neue Klasse CSuchenDialog zugeordnet, die von CDialog
abgeleitet wurde.
Um einen nicht modalen Dialog zu erzeugen, muss die Methode Create von
CDialog aufgerufen werden. Dadurch wird ein Dialogfensterobjekt erzeugt. Der
Aufruf wird am besten beim Erzeugen des Vaterfensters aufgerufen, wie hier in der
OnCreate Methode von CMainFrame.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return –1;
// TODO: Speziellen Erstellungscode hier einfügen
m_SuchenDialog.Create(IDD_DIALOG_SUCHEN,this);
return 0;
}
Seite 124
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
Besitzt die Dialog-Instanz das Flag WS_VISIBLE, wird der Dialog sofort dargestellt.
Im Beispiel wurde das Flag im Ressource-Editor nicht gesetzt. Um nun das Fenster
darzustellen, muss die Methode ShowWindow der Klasse CWnd aufgerufen werden.
Im Beispiel wird dies in einer Menüfunktion realisiert.
void CMainFrame::OnDialogNichtModalerDialogOeffnen()
{
// TODO: Code für Befehlsbehandlungsroutine hier einfügen
CString selectedWord;
selectedWord = GetSelectedWord();
m_SuchenDialog.SetSearchString(selectedWord);
m_SuchenDialog.UpdateData(FALSE);
m_SuchenDialog.ShowWindow(SW_SHOW);
}
Hier wird durch die selbst implementierte Methode GetSelectedWord die
markierten Worte geholt und diese dann mit der Methode SetSearchString an
den Dialog weitergeleitet. Danach wird durch ShowWindow das Fenster im
Vordergrund dargestellt.
6.3 Dialogbasierte Anwendung
In einer dialogbasierten Anwendung ist dem Hauptfenster ein Dialogobjekt
zugeordnet und wird in diesem immer dargestellt. Diese Art von Anwendung wird
benötigt wenn für die Eingabe von Werten ein Formular zur Verfügung stehen soll
und die Eingabe als Hauptaufgabe der Anwendung besteht. Als einfaches Beispiel
kann man sich eine Formatierungsanwendung, die z.B. Disketten formatiert oder ein
Einkommenssteuerprogramm, das die Formulare der Einkommenssteuererklärung
enthält, vorstellen.
Im Beispiel soll eine Formatierungsanwendung realisiert werden, die ein Eingabefeld
wie Abbildung 56 besitzt.
Abbildung 56: Dialogbasierte Anwendung
Bei einer dialogbasierten Anwendung wird in der Applikationsklasse
CAnwendungsDialogApp in der Methode InitInstance ein modaler Dialog
gestartet, der dann das Hauptfenster ist.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 125
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dialoge
BOOL CAnwendungsDialogApp::InitInstance()
{
...
CAnwendungsDialogDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// ZU ERLEDIGEN: Fügen Sie hier Code ein, um ein Schließen des
// Dialogfelds über OK zu steuern
}
else if (nResponse == IDCANCEL)
{
// ZU ERLEDIGEN: Fügen Sie hier Code ein, um ein Schließen des
// Dialogfelds über “Abbrechen” zu steuern
}
// Da das Dialogfeld geschlossen wurde, FALSE zurückliefern, so d
ass
// wir die Anwendung verlassen, anstatt das Nachrichtensystem der
// Anwendung zu starten.
return FALSE;
}
Zuerst wird ein lokales Dialogobjekt dlg von der Klasse CAnwendungsDialogDlg
erzeugt. Dann wird das Dialogobjekt als Hauptfenster der Anwendung zugeordnet.
Danach wird der Dialog durch DoModal gestartet. Der Dialog kehrt erst wieder
zurück, wenn er mit „Beenden“ geschlossen wird. In der if-Anweisung kann noch
der Rückgabewert der Methode DoModal bearbeitet werden. Zum Schluß muss die
Methode InitInstance mit FALSE verlassen werden, damit die Anwendung
automatisch beendet wird.
Um auf die Nachrichten der Steuerelemente (Buttons, Editfelder) reagieren zu
können, muss in der Dialogklasse eine Nachrichtentabelle definiert sein, die die
Zuordnung der Nachrichten auf die Membermethoden übernimmt.
Seite 126
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7 Dokumente und Ansichten
7.1 Ein Dokument + eine Ansicht = Single Document Interface
In den ersten Tagen der MFC hatte eine Applikation prinzipiell zwei Komponenten:
1. Das Applikations-Objekt, welches die Anwendung selbst repräsentierte.
2. Das Fensterobjekt, welches das Fenster der Applikation repräsentierte.
Erste Pflicht des Applikationsobjektes war die Erzeugung eines Fensterobjektes,
während das Fensterobjekt die Nachrichten (messages) abarbeitete. Zusätzlich
präsentierte die MFC natürlich noch ein paar Klassen für den generellen Gebrauch
wie CString oder CTime ohne weiteren Bezug zu Windows. Nichts desto trotz war
die MFC 1.0 eine dünne Wrapper Schicht um die Win32-API, die WindowsProgrammierer ein objektorientiertes Interface zu den existierenden Win32Programmiermitteln wie z.B. Fenster, Dialogboxen, Gerätekontexte (device context)
anbot.
Hinweis: Leider ist die sprachliche Einteilung innerhalb der Windows-Programmierung sehr
verwirrend. Die Sprache der Win-32-API kennt zwar sogenannte Windows-Objekt z.B. die FensterObjekte. Diese müssen aber nicht ein Objekt im objektorientierten Sinne darstellen!
Erst mit der MFC 2.0 änderte sich die Art und Weise, wie Windows-Anwendungen
geschrieben wurden – und heute noch geschrieben werden – mit die Einführung der
Dokument/Ansicht Architektur (Document/View). In einer Dokument/AnsichtApplikation werden die Daten der Applikation durch ein Dokumentenobjekt
(document object) repräsentiert und verschiedene Sichtweisen auf das Dokument
werden durch ein oder mehrere Ansichtsobjekte (view objects) repräsentiert.
Dokumenten- und Ansichtsobjekte arbeiten eng zusammen, um die Eingaben des
Benutzers zu verarbeiten und textuelle oder grafische Repräsentationen der
resultierenden Daten zu zeichnen.
Das Rahmenfenster, das wir schon früher kennengelernt haben, wird durch die MFCKlassen CFrameWindow, bzw. CMDIFrameWindow modelliert. Der Unterschied
zwischen beiden wird später deutlich. Wichtig ist, dass das Rahmenfenster nicht
mehr wie früher der Hauptaktor bei der Verarbeitung von Nachrichten ist, sondern
eher als Containerobjekt für Views, Werkzeugleisten, Statusleisten und andere
Objekte dient.
Die Dokument/Ansicht-Architektur nimmt dem Programmierer viele Routinearbeiten
ab. Ein Beispiel ist die Sicherheitsabfrage, ob ein Benutzer beim Schließen eines
Dokuments seine geänderten und noch nicht gesicherten Daten vor dem Verlassen
speichern möchte. Diese Abfrage, wie sie in Abbildung 57 dargestellt ist, bietet das
MFC-Framework automatisch, wenn eine Dokument/Ansicht-Applikation vom
Anwendungsassistent erstellt wird.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 127
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Abbildung 57: automatische Sicherheitsabfrage
MFC bietet zwei Typen von Dokument/Ansicht-Applikationen über zwei verschiedene
Interfaces:
1. singel document interface (SDI)
2. multi document interface (MDI)
SDI-Applikationen können jeweils nur ein Dokument zum gleichen Zeitpunkt
bearbeiten. Typisches Beispiel ist die mit Windows ausgelieferte Anwendung
WordPad. Im Gegenzug dazu können MDI-Anwendungen mehrere Dokumente
gleichzeitig laden. Eine typische MDI-Anwendung ist Word für Windows.
Das Framework vollbringt die Glanzleistung, viele der Unterschiede zwischen MDIund SDI-Anwendungen für den Programmierer transparent zu halten. Viele
Windows-Programmierer wurden jedoch abgeschreckt, MDI-Anwendungen zu
entwickeln, da das neue Look-and-Feel von Windows95 und Windows NT 4.0 das
sogenannte dokumentenorientierte Programmiermodell favorisiert, was eher der
Verwendung von SDI entspricht.
Wir aber wollen hier unseren Blick vorerst auf SDI-Anwendungen konzentrieren. Da
die meisten Begriffe und Vorgehensweisen bei MDI ähnlich sind, wird das Kapitel zur
Programmierung von MDI-Anwendungen dann etwas kürzer ausfallen.
7.1.1 Dokument/View – Das Fundament
Unsere Erkundung der Dokument/Ansicht-Architektur beginnt mit einem Blick auf das
Konzept der verschiedenen Objekte und Ihre Verknüpfungen miteinander. Abbildung
58 zeigt eine Schema einer SDI Applikation mit Dokument und Ansicht.
Seite 128
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Applikation-Objekt
Nachrichten an das Rahmenfenster
Nachrichten an den View
Rahmenfenster-Objekt
Ansicht-Objekt
Dokument-Objekt
bidirektionaler Informationsfluß zwischen
Dokument und Ansicht
Abbildung 58: Die Dokument/Ansicht-Architektur einer SDI-Anwendung
Das Rahmenfenster ist das Top-Level-Fenster der Applikation. Ein Top-Level
Window wurde mit dem Attribut WS_OVERLAPPEDWINDOW erzeugt. Dies bedeutet,
dass das Framework die folgenden Fenstereigenschaften automatisch einbaut:
-
resizing border (veränderliche Größe mit der Möglichkeit, durch Anfassen des
Fensterrahmens die Größe zu verändern)
caption bar (Titelzeite)
system menu (Systemmenü, mit entsprechendem Button in der linken oberen
Ecke)
minimize / maximize button (Minimierungs- und Maximierungsknopf in der rechten
oberen Ecke)
close button (Schliesen-Knopf , ebenfalls rechte obere Ecke)
Der entsprechende Code zur Ausführung dieser Grundfunktionalitäten wird durch
das Framework geliefert und muss vom Programmierer noch modifiziert werden, falls
er vom Standard abweichende Aktionen ausführen möchte.
Die Ansicht ist das Kindfenster des Rahmenfensters. Es füllt immer automatisch den
gesamten Clientbereich des Rahmenfensters vollständig aus. Die Ansicht wird bei
SDI-Anwendungen immer dem verfügbaren Platz im Clientbereich des
Rahmenfensters angepaßt, d.h. wenn der Benutzer das Rahmenfester in der Breite
oder Höhe ändert, ändert sich automatisch die Größe der Ansicht.
Im Dokument werden die Daten gespeichert, die in der Ansicht für den Benutzer
sichtbar gemacht werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 129
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
MFC bietet zwei Klassen, CDocument und CView, die benutzt werden, um eigene
Ansichten und Dokumente zu erzeugen. Im Normalfall werden die Ansichtsklasse
und die Dokumentenklasse wie folgt von CDocument und CView abgeleitet, womit
die Objektinstanzen automatisch die Funktionalitäten (Methoden) der Eltern
mitbringen:
Rahmenfester:
Ansicht:
Dokument:
class MyFrameWnd:: public CframeWnd
class MyView:: public Cview
class MyDocument:: public Cdocument
Die Pfeile in Abbildung 58 repräsentieren die Datenflüsse zwischen den Objekten.
Das Applikationsobjekt pumpt Nachrichten in das Rahmenfenster und die Ansicht,
wobei das Rahmenfenster auch Nachrichten an andere Objekte weiterleiten kann, so
dass diese an der Nachrichtenabarbeitung beteiligt werden. Das Ansichtsobjekt
übersetzt Maus- und Tastatureingaben in Kommandos, die auf den Daten des
Dokuments operieren, während das Dokument wiederum Informationen über seine
Daten an die Ansicht weiterleitet, damit die Ansicht ein Abbild der Daten für den
Benutzer darstellen kann. Die Interaktion zwischen den Objekten ist komplex, aber
das Bild wird mit jeder selbst geschriebenen Applikation klarer und mit jedem Stück
selbst analysiertem Code werden die Rollen verständlicher, die Applikationsobjekte,
Ansichten, Dokumente oder Rahmenfenster spielen.
Die in Abbildung 58 dargestellte Architektur hat sehr wichtige Folgen für das Design
und die Operationen einer ausführbaren Applikation. In MFC 1.0 wurden oft die
Daten eines Programms in Membervariablen der Rahmenfensterklasse gespeichert.
Das Programm „zeichnete“ Sichtweisen dieser Daten, indem es direkt auf diese
Membervariablen zugriff und GDI-Funktionen (GDI = Graphics Device Interface)
benutzte, um direkt im Clientbereich des Rahmenfensters zu zeichnen. Die
Dokument/Ansicht-Architektur zwingt den Programmierer zu einem wesentlich
modulareren Aufbau seiner Programme. Daten der Applikation werden im
Dokumentenobjekt gekapselt und die Ansicht ist für die Darstellung der Daten
zuständig. Eine Dokument/Ansicht Applikation versucht nie den Gerätekontext
(device context) für den Clientbereich des Rahmenfensters zu erhalten und darin zu
zeichnen; statt dessen zeichnet solch eine Applikation im Ansichtsobjekt.
Seite 130
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.1.2 Die überarbeitete InitInstance-Funktion
Einer der interessantesten Aspekte einer SDI Anwendung mit Dokument/AnsichtArchitektur ist die Art und Weise, wie das Rahmenfenster, Dokumenten- und
Ansichtsobjekt erzeugt werden. Betrachtet man die InitInstance Funktion, die der
Visual C++ Applikationswizard für eine SDI-Applikation erzeugt, findet man in etwa
folgendes vor:
CSingelDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CMyView));
AddDocTemplate(pDocTemplate);
CComandLineInfo cmdInfo;
ParseCommadLine(cmdInfo);
if(!ProcessShellCommand(cmdInfo))
return FALSE;
return TRUE;
Dieser Code enthält nichts, was auch nur entfernte Verwandtschaft zu dem Code
einer selbst gestrickten WIN32-Applikation andeuten könnte (Vergleiche hierzu
Kapitel 1.4 – „Hello, World“ als Win32-API Programm).
Ein intensiver Blick auf den obigen Codeauszug wird uns zeigen, was eine
Dokument/Ansicht - Applikation zum Leben erweckt und was sich hinter den Kulissen
tut.
Ganz am Anfang stehen die Zeilen
CSingelDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CMyView));
die ein SDI-Dokumententemplate direkt aus der MFCKlasse CSingleDocTemplate
erzeugen. Sinn und Zweck des Dokumententemplates ist die Identifizierung
1. der Dokumentenklasse, die die Dokumente der Applikation repräsentiert,
2. der Rahmenfensterklasse, die Ansichten auf die Dokumente einschließen,
und
3. der Ansichtsklassen, die die grafische Repräsentation der Dokumentdaten
zeigen.
Im Dokumententemplate wird ebenfalls eine Ressourcen-ID gespeichert. Diese
Ressourcen-ID benutzt das Framework beim Laden der Menüs, Acceleratoren, und
anderer Ressourcen, die das Benutzerinterface für einen bestimmten
Dokumententyp formen. Der Applikationswizard benutzt die intern definierte
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 131
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Konstante IDR_MAINFRAME als die Ressourcen-ID, aber man könnte jede andere
Integerkonstante verwenden, so lange sie nicht null ist.
Das MFC-Makro RUNTIME_CLASS() gibt einen Zeiger auf eine CRuntimeClassStruktur für die angegebene Klasse zurück, die das Framework in die Lage versetzt,
Objekte dieser Klasse zur Laufzeit zu erzeugen.
Nachdem das Dokumententemplate erzeugt wurde, fügt das Statement
AddDocTemplate(pDocTemplate);
das Template zu einer Liste von Templates hinzu. Alle Templates dieser Liste
können durch das Applikationsobjekt verarbeitet werden. Jedes Template, das auf
diesem Weg registriert wurde, definiert einen Dokumententyp, den die Applikation
unterstützt.
In den Zeilen:
CCommandLineInfo cmdInfo;
ParseCommadLine(cmdInfo);
wird CWinApp::ParseCommandLine benutzt um ein CCommandLineInfo-Objekt
zu erzeugen und mit Werten zu initialisieren, die die Parameter reflektieren, die beim
Starten der Applikation in der Kommandozeile eingegeben wurden. Hier könnte z.B.
der Dateiname eines Dokumentes stehen, das beim Starten geladen werden soll.
Mit der If-Anweisung
if(!ProcessShellCommand(cmdInfo))
return FALSE;
werden die Kommandozeilenparameter verarbeitet. Neben anderen Dingen ruft
ProcessShellCommand() seinerseits CWinApp::OnFileNew() auf, um die
Applikation mit einem „leeren“ Dokument zu starten, falls in der Kommandozeile kein
Dateiname eingegeben wurde, oder CWinApp::OpenDocumentFile() um die in
der Kommandozeile angegebene Datei beim Starten zu öffnen. In dieser Phase der
Programmausführung erzeugt das Framework das Dokumenten-, das
Rahmenfenster- und ein Ansichtsobjekt unter Verwendung der Informationen, die im
Dokumententemplate gespeichert sind. ProcessShellCommand() gibt TRUE
zurück, falls die Initialisierung erfolgreich verlief, und FALSE, falls nicht. Ein FALSE
aus ProcessShellCommand() führt dazu, dass InitInstance() ebenfalls ein
FALSE zurückgibt, wodurch die gesamte Applikation gleich beendet wird.
Seite 132
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Nachdem die Applikation gestartet ist, das Dokument, das Rahmenfenster und die
Ansichten erstellt wurden, wird die Nachrichtenschleife angeworfen und die
Applikation beginnt Nachrichten zu erhalten und zu verarbeiten. Im Gegensatz zu
MFC 1.0-Applikationen, die typischerweise alle Nachrichten auf Memberfunktionen
des Rahmenfensters abbildeten, teilen Applikationen der Dokument/Ansicht Architektur
Nachrichten
zwischen
Dokumenten-,
Ansichtenund
Rahmenfensterobjekt auf. Das Framework liefert viel Hintergrundarbeit, um diese
Arbeitsaufteilung zu ermöglichen. Im Betriebsystem Windows können nur Fenster
Nachrichten erhalten, so dass das Framework einen komplizierten KommandoRoutingmechanismus implementiert, der Kommandos und Update-Nachrichten von
Menüs und Controls von einem Objekt an andere Objekte in einer vorbestimmten
Abfolge sendet, bis eines der Objekte die Nachricht abarbeitet, oder die Nachricht
letztlich an ::DefWindowProc() geleitet wurde. Später werden wir das
Kommandorouting genauer betrachten, und es wird klarer werden, weshalb es ein so
mächtiges Feature des Frameworks ist, dessen Fehlen die Dokument/AnsichtArchitektur unmöglich machen würde.
7.1.3 Das Dokumentobjekt
In einer Dokument/Ansicht - Applikation werden die Daten in einem
Dokumentenobjekt der Klasse CDocument gespeichert. Der Ausdruck „Dokument“
wird dabei leicht mißverstanden, da er leicht die Vorstellungen an eine
Textverarbeitung oder eine Tabellenkalkulationsprogramm aufwirbelt. Das Dokument
der Dokument/Ansicht - Architektur ist wesentlich allgemeiner als diese Vorstellung.
Das „Dokument“ einer Dokument/Ansicht - Applikation darf als abstrakte
Repräsentation der Applikationsdaten verstanden werden, wodurch eine klare
Grenze gezogen wird, zwischen der Datenhaltung und der Datendarstellung für den
Benutzer.
Typischerweise
beinhaltet
ein
Dokumentobjekt
öffentliche
Memberfunktionen, die anderen Objekten, wie z. B. einem View zu Verfügung
stehen, um die Daten zu manipulieren, oder auszulesen. Das gesamte
Datenhandling wird vom Dokumentobjekt selbständig vorgenommen. Hierdurch wird
eine strikte Datenkapselung erreicht. Das Dokumentenobjekt einer Textverarbeitung
könnte die Daten z. B. in einem CByteArray speichern, und öffentliche Methoden,
wie z. B. AddChar(), RemoveChar() und GetChar() für den Zugriff auf die Daten
vorweisen. Methoden wie AddNewLine(), DeleteLine() könnten die öffentliche
Schnittstelle zum Dokumentenobjekt erweitern.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 133
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Eine abgeleitete Dokumentenklasse erbt einige wichtige Memberfunktionen von
CDocument, eine Auswahl davon ist in der nachfolgenden Tabelle abgebildet.
SetModifiedFlag sollte jedesmal benutzt werden, wenn die Daten des Dokuments
verändert wurden. Diese Funktion setzt ein Flag innerhalb des Dokumentenobjektes,
das dem Framework mitteilt, dass das Dokumentenobjekt ungesicherte Daten
enthält. Über die Funktion IsModified kann man auch selbst nachprüfen, ob ein
Dokument ungesicherte Daten enthält. Mit Hilfe von GetTitel und GetFileName
kann man den Titel, sowie den vollen Pfad- und Dokumentennamen der zum
Dokument gehörigen Datei ermitteln. Beide Funktionen geben ein Cstring - Objekt
zurück, falls das Dokument bereits mit einer Datei verknüpft wurde.
Tabelle 18: wichtige Memberfunktionen von CDocument
Funktion
GetFirstViewPosition
GetNextView
GetPathName
GetTitel
IsModified
SetModifiedFlag
UpdateAllViews
Beschreibung
Gibt einen POSITION Wert zurück, der in
GetNextView zur Numerierung der Dokumente
benützt werden kann
Gibt einen Cview-Pointer zurück, der auf den
nächsten Eintrag in der Liste aller mit dem Dokument
assoziierten Views verweist.
Gibt den Datei- und Pfadnamen des Dokuments in
einem CString-Objekt zurück, NULL falls das
Dokument noch nicht mit einer Datei verknüpft wurde.
Gibt ein CString-Objekt zurück, das den Titel
enthält, mit dem das Dokument verknüpft wurde.
Nonzero à Dokument enthält ungesicherte Daten
0 à Alle Daten des Dokuments wurden gesichert.
Setzt oder löscht das Modified-Flag des
Dokuments, das anzeigt, ob das Dokument
ungesicherte Daten enthält.
Frischt alle mit dem Dokument assoziierten Ansichten
auf, indem es die Funktion OnUpdate jeder Ansicht
aufruft.
Seite 134
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Drei der in Tabelle 18 aufgelisteten Funktionen dienen der Interaktion mit den
Ansichten eines Dokuments. Die Funktion UpdateAllViews wird in SDIApplikationen nicht sehr häufig verwendet, da jedes Dokument nur ein View besitzt.
Dieses wird aufgrund der Benutzerinteraktion schon häufig genug upgedatet,
abgesehen von den Updates vor und nach Änderungen am Document-Objekt.
Anders ist dies bei MDI-Applikationen. Dort wird UpdateAllViews benutzt, um das
Dokument und die Views synchron zu halten. Ein Document-Objekt kann seine
Views nacheinander aufrufen und mit jedem View individuell kommunizieren. Dazu
dienen die Funktionen GetFirstView und GetNextView. Das folgende
Codefragment ist ein billiges Plagiat für UpdateAllViews:
POSITION pos = GetFirstViewPosition();
while (pos != NULL)
GetNextView(pos)->OnUpdate(NULL, 0, NULL);
So könnten jedoch die Parameter für UpdateView z. B. individuell für jeden View
bestimmt, oder gar einzelne Views übersprungen werden.
Die Klasse CDocument enthält mehrere virtuelle Funktionen, die überschrieben
werden können, um das Verhalten eines Dokuments zu verändern. Einige werden
nahezu immer in abgeleiteten Dokument-Klassen überschrieben. Die am häufigsten
überschriebenen Funktionen zeigt Tabelle 19.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 135
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Tabelle 19: Die wichtigsten überschreibaren Funktionen von CDocument
OnNewDocument
OnOpenDocument
DeleteContents
Serialize
Wird vom Framework aufgerufen, wenn ein Dokument
erzeugt wird. Hier können Dokumentendaten kurz vor
dem Erstellungszeitpunkt initialisiert werden.
Wird vom Framework aufgerufen, wenn ein Dokument
vom Speichermedium (Festplatte/Disk) gelesen wird.
Hier werden Dokumentendaten initialisiert, kurz bevor
ein neu geladenes Dokument verfügbar wird.
Diese Funktion wird vom Framework aufgerufen, wenn
der Inhalt eines Dokuments gelöscht wird. Hier kann die
Deallokierung
von
Speicher
und
Ressourcen
vorgenommen
werden,
bevor
ein
Dokument
geschlossen wird.
Diese vom Framework aufgerufene Funktion dient zur
Serialisierung des Dokuments in oder von einer Datei.
Hier ist das richtige Plätzchen um Dokument spezifischen Serialisierung - Code unterzubringen, damit
das Dokument auch gespeichert und geladen werden
kann.
OnNewDocument wird benutzt, um jedes neu erzeugte Dokument zu initialisieren.,
während OnOpenDocument verwendet wird, um die nicht serialisierten
Datenmember des Dokument-Objektes zu initialisieren, wenn ein neues Dokument
von Platte geladen wird. In einer SDI-Applikation wird das Dokument-Objekt einmalig
erzeugt und wird jedesmal wieder verwendet, wenn ein Dokument erzeugt oder
geöffnet wird. Da der Konstruktor also nur einmal ausgeführt wird, unabhängig
wieviele Dokumente geöffnet und geschlossen wurden, sollte eine SDI-Applikation
einmalig
notwendige
Initialisierungen
im
Konstruktor
ausführen
und
Initialisierungscode für jedes neue Dokument in OnNewDocument oder
OnOpenDocument packen.
Bevor ein neues Dokument erzeugt oder geöffnet wird, ruft das Framework die
virtuelle Funktion DeleteContents auf, um die existierenden Daten im Dokument
zu löschen. Deshalb ist eine SDI-Applikation gezwungen, die Funktion
CDocument::DeleteContents zu überschreiben und Ressourcen, die im Kontext
des Dokument-Objekts allokiert waren, wieder freizugeben, bzw. andere, notwendige
Aufräumarbeiten zu erledigen.
MDI-Applikationen folgen allgemein diesem Modell ebenfalls, obwohl sie für jedes neu erzeugte und
geöffnete Dokument ein neues Dokument-Objekt anlegen, und dieses beim Schließen des Dokuments
auch wieder zerstören.
Wenn OnNewDocument und OnOpenDocument in einer von CDocument
abgeleiteten Klasse überschrieben werden, ist wieder wichtig, auch die beiden
Funktionen in der Basisklasse aufzurufen. Sonst wird DeleteContents vom
Framework nicht aufgerufen, wenn ein neues Dokument geladen oder erzeugt wird
genauso wie andere, wichtige Initialisierungsaufgaben, die das Framework
übernimmt, nicht ausgeführt werden. Zusätzlich zum Aufruf von DeleteContents,
Seite 136
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
zeigt OnOpenDocument von CDocument den allseits bekannten Dialog „Datei
Öffnen“ an, um vom Benutzer den Namen und das Verzeichnis zu erhalten , in dem
die Datei liegt, die das zu öffnende Dokument enthält. Ebenfalls ruft diese Funktion
die CDocumet::Serialize Funktion auf, um das Dokument von Platte zu
serialisieren.
Die
überschriebenen
Funktionen
OnNewDocument
und
OnOpenDocument sollten zu allererst die Basisklassenimpelmentierungen der
Funktionen aufrufen, und jegliche weitere Arbeit unterlassen, wenn diese den
Rückgabewert FALSE liefern. Die überschriebenen Versionen von OnNewDocument
und OnOpenDocument selbst sollten TRUE als Rückgabewert an das Framework
senden, um Ihre erfolgreiche Abarbeitung anzuzeigen, wie es im folgenden
Codeausschnitt gezeigt wird.
BOOL CMyDoc::OnNewDocument()
{
if (!Cdocument::OnNewDocument())
return FALSE;
//nitialize the Document
return TRUE;
}
BOOL CMyDoc::ONOpenDocument()
{
if(!CDocument::ONOpenDocument())
return FALSE;
//Initialize Members of the document object that are not
//initialized when the document is serialized from disk
return TRUE;
}
Wenn ein Dokument geöffnet oder gespeichert wird, verwendet das Framework die
Serialize-Funktion des Dokuments, um die Daten des Dokuments zu serialisieren.
Die Serialize-Funktion können wir uns vorerst ähnlich wie die überschreibbaren
Operatoren für in- und outputstreams in C++ denken. Der Applikationsentwickler
schreibt die Serialize-Funktion, um die Daten des Dokument-Objektes in den
Stream zu senden und umgekehrt aus dem Stream herauszuholen; das Framework
übernimmt alle anderen Aufgaben (in unsere Analogie das Bereitstellen der Steams
und die Operationen auf den physikalischen Geräten). Diese Arbeit wird in der
Klasse CArchive gekapselt (Datei zum Lesen / Schreiben öffnen, physikalischer
Disk-I/O, usw.).
7.1.4 Das Ansicht-Objekt (View-Object)
Während der einzige Zweck eines Dokument-Objektes die Speicherung der Daten
der Applikation ist, gibt es gleich zwei Gründe für die Existenz der Ansichts-Objekte.
Zum einen werden die visuellen Repräsentationen der Daten im Dokument-Objekt
durch ein Ansichts-Objekt auf dem Bildschirm dargestellt, und zum anderen sorgt ein
Ansichts-Objekt für die Übersetzung der Benutzereingaben in Kommandos, die auf
den Dokument-Daten arbeiten (speziell diejenigen Maus- und Tastaturnachrichten,
die nicht zum Dokument-Objekt geroutet werden wie z.B. die KommandoNachrichten). Dadurch stehen Dokument- und Ansichts-Objekt in einer engen
Verknüpfung zueinander, und die Information, die zwischen den beiden ausgetauscht
wird, fließt in beide Richtungen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 137
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Mit einem Dokument-Objekt können mehrere Ansichten verbunden sein, aber eine
Ansicht gehört immer nur zu einem Dokument. Das Framework speichert einen
Zeiger auf das zugehörige Dokument-Objekt in dem Attribut m_pDocument der
Ansicht und macht den Zeiger durch die Methode GetDocument verfügbar.
Umgekehrt kann ein Dokument-Objekt seine Views durch die Benutzung von
CDocument::GetFirstViewPosition
und
CDocument::GetNextView
identifizieren. Wenn der AppWizard den Sourcecode für eine View-Klasse generiert,
überschreibt er GetDocument der Basisklasse mit einer Funktion, die
m_pDocument auf den entsprechenden Dokument-Objektyp castet und als Ergebnis
zurückliefert. Dieses Überschreiben erlaubt den typsicheren Zugriff auf DokumentObjekte und eliminiert sonst notwendige Benützung expliziter Casts, nach jedem
Aufruf von GetDocument.
Die Klasse CView der MFC definiert die Basiseigenschaften einer Ansicht. Die
abgeleiteten Ansichts-Klassen fügen Funktionalität zu dieser Basisdefinition der
Ansicht hinzu. Die abgeleiteten Ansichts-Klassen, die in der MFC angeboten werden,
sind in der Tabelle 20 zu finden. Einige, wie z.B. CEditView sind so designed, dass
sie direkt verwendet werden können. Andere hingegen, wie z.B. CScrollView, sind
abstrakte Basisklassen und nur für das Ableiten eigener Ansichtsklassen nützlich.
Tabelle 20: Abgeleitete Ansichtsklassen der MFC
Funktion
CCtrlView
CEditView
CRichEditView
CListView
CTreeView
CScrollView
CFormView
CRecordView
CDAORecordView
Beschreibung
Basisklasse
für
CEditView,
CRichEditView,
CListView und CTreeView. Kann auch benutzt
werden, um andere Ansichtsklassen abzuleiten, die
Controls wrappen.
Wrappt die Funktionalität eines Windows Edit-Control
und fügt die Funktionalitäten Drucken, Suchen und
Suchen-und-Ersetzen hinzu.
Wrappt die Funktionalität eines RichEdit Controls.
Wrappt die Funktionalität eines ListView Controls
Wrappt die Funktionalität eines TreeView Controls.
Von CView abgeleitete, abstrakte Basisklasse, die
Scrolling-Eigenschaften zu einem View hinzufügt. Dies
ist die Basisklasse für CFormView und CDAOFormView.
Bietet scrollbare Ansichten, die – durch DialogTemplates erzeugte – Controls anbieten.
Kombiniert ein CFormView-objekt und ein CRecordSetobjekt, um eine Ansicht auf eine Ansicht für
Datenbanken zu ermöglichen.
DAO-Version von CRecordView.
Ähnlich der CDocument- Klasse, beinhalten CView und seine abgeleiteten Klassen
virtuelle Memberfunktionen, die vom Programmierer überschrieben werden können,
um die Operationen eines Views anzupassen. Die wichtigsten überschreibbaren
Funktionen sind in der Tabelle 21 aufgelistet. Eine der wichtigsten überschreibbaren
Funktionen ist eine rein virtuelle Funktion namens OnDraw, die jedesmal aufgerufen
wird, wenn die Ansicht eine WM_PAINT- Nachricht erhält.
Seite 138
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
In Applikationen, die nicht auf dem Dokumenten/Ansicht-Konzept beruhen, werden
WM_PAINT –Nachrichten von einer Nachrichtenbehandlungsroutine Namens
OnPaint verarbeitet, die ein CPaintDC für Ihre Zeichenarbeit benutzt. In einem
Dokument/Ansicht-Programm fängt das Famework die WM_PAINT-Nachricht ab und
leitet sie zusammen mit einem generischen CDC für die Zeichenarbeit an die
OnDraw-Funktion der Ansicht weiter. Es wird kein Nachrichten-Mapping benötigt, da
OnDraw virtuell ist. Die folgende Implementierung von OnDraw zeichnet „Hello, MFC“
zentriert in den View.
void CMyView::OnDraw(DCD* pDC)
{
CRect rect;
GetClientRect(&rect);
pDC->DrawText(“Hello, MFC”, -1, rect,DT-SINGLELINE|
CT_CENTER|DT_VCENTER);
}
Tabelle 21: Die wichtigsten, überschreibbaren Funktionen
Funktion
OnDraw
OnInitialUpdate
OnUpdate
Beschreibung
Wird aufgerufen um die Daten des Dokuments zu
zeichnen. Wird überschrieben, um die Ansicht eines
Dokumentes zu zeichnen.
Wird aufgerufen, wenn die Ansicht zum ersten Mal mit dem
Dokument verknüpft wird (attach). Wird überschrieben um
die Ansicht eines neu erzeugten oder frisch geladenen
Dokumentes zu initialisieren
Wird aufgerufen, wenn die Daten eines Dokumentes sich
geändert haben und die Ansicht upgedated werden muss.
Wird überschrieben um „smartes“ Updateverhalten zu
bekommen, das nur den Teil der Ansicht neu zeichnet, der
sich ändert, anstatt die gesamte Ansicht neu zu zeichnen.
Die Tatsache, dass die Ansicht nicht Ihr eigenes Device-Context-Objekt erzeugen
muss, ist dabei von geringerer Wichtigkeit. Der echte Grund, warum das Framework
OnDraw benutzt, ist, dass derselbe Code für die Ausgabe in ein Fenster, das
Drucken oder die Druckvorschau verwendet werden kann. Wenn eine WM_PAINTNachricht ankommt, und das Framwork einen Zeiger auf ein Device-Context für das
Zeichnen (paint-DC) an OnDraw übergibt, so geht die Ausgabe in ein Fenster. Wenn
ein Dokument ausgedruckt wird, ruft das Framework dieselbe OnDraw-Funktion auf,
und übergibt einen Zeiger auf ein Drucker-DC (print-DC). Da das Gaphics Device
Interface (GDI) ein geräteunabhängiges, grafisches System darstellt, kann derselbe
Code (nahezu) identische Ausgaben auf zwei verschiedenen Geräten erzeugen,
wenn er mit zwei verschiedenen DC‘s aufgerufen wird. Das Framework zieht den
Vorteil daraus und das Erstellen der Druckfunktionen – normalerweise eine
unangenehme Arbeit unter Windows – wird ein leichtes Unterfangen. Tatsächlich ist
das Drucken in einer Dokumenten/Ansicht-Applikation wesentlich einfacher als das
Drucken in einer konventionellen Applikation.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 139
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Die beiden CView-Funktionen, die man normalerweise häufig in abgeleiteten
Klassen überschreibt, sind OnInitialUpdate und OnUpdate. Ansichten, wie auch
Dokumente, werden in SDI-Applikationen einmal erzeugt und immer
wiederverwendet. OnInitialUpdate in einer SDI-Anwendung wird immer dann
aufgerufen, wenn ein Dokument geöffnet oder neu erzeugt wird. die defaultImplementation von OnInitialUpdate ruft OnUpdate auf, und die default
Implementation von OnUpdate wiederum erklärt die client area der Ansicht für
ungültig, um ein Neuzeichnen zu erzwingen. OnInitialUpdate wird per Dokument
benutzt, um die Attribute (Datenmember) der Ansichtsklasse zu initialisieren und
andere, auf die Ansichten bezogene Initialisierungen auszuführen. In einer
CScrollView-Klasse z.B. wird im allgemeinen in OnInitialUpdate die Funktion
SetScrollSizes der Ansicht aufgerufen, um die scrolling-Parameter zu
initialisieren. Es ist wichtig die Basisklassen-Version von OnInitialUpdate aus
einer überschriebenen Version aufzurufen, sonst wird die Ansicht nicht upgedated,
wenn ein neues Dokument geöffnet oder erzeugt wird.
Die OnUpdate-Funktion einer Ansicht wird aufgerufen, wenn die Daten eines
Dokumentes modifiziert werden und jemand UpdateAllViews aufruft –
normalerweise entweder das Dokument oder eine Ansicht des Dokuments. Man
müßte OnUpdate niemals überschreiben, da die default-Implementation das
Neuzeichnen übernimmt. Aber in der Praxis wird oft OnUpdate überschrieben, um
die Performance zu optimieren, indem man nur den Teil neu zeichnet, der von den
Änderungen betroffen ist, anstatt die gesamte Ansicht neu zu zeichnen. Dies ist
besonders hilfreich in MDI-Applikationen. Der unschöne Flackereffekt kann eliminiert
werden, der oft in inaktiven Ansichten auftritt, wenn Dokumente in der aktiven
Ansicht modifiziert werden und die default-OnUpdate-Funktion des Frameworks
aufgerufen wird.
In Multiple-View-Applikationen kann eine Ansicht durch Überschreiben der
OnActivateView-Funktion herausfinden, wann sie aktiviert und deaktiviert wird.
Der erste Parameter in OnActivateView ist ungleich Null, wenn die Ansicht gerade
aktiviert wird, und Null, wenn sie gerade deaktiviert wird. Der zweite und dritte
Parameter sind CView-Zeiger, die die Ansichten identifizieren, die aktiviert,
beziehungsweise deaktiviert werden. Wenn die Zeiger gleich sind, wurde das FrameFenster der Applikation aktiviert, ohne eine Änderung in der aktiven Ansicht zu
verursachen. Nicht unerwähnt soll bleiben, dass Ansichts-Objekte manchmal die
Eigenschaften der OnActiveView-Funktion benutzen um eine Farb-Palette zu
realisieren.
Ein
Rahmenfenster
kann
die
aktuelle
Ansicht
durch
CFrameWnd::GetActiveView erhalten und durch CFrameWnd::SetActiveView
setzen.
7.1.5 Das Rahmenfenster-Objekt
Bisher haben wir die Rollen der Applikations-Objekte, Dokumenten-Objekte
Ansichts-Objekte
in
Dokument/Ansichts-Applikationen
betrachtet.
Applikationsobjekt bringt den Stein ins Rollen durch die Erzeugung
Dokumententemplates, welche die unterstützten Dokumenttypen beschreiben.
und
Das
von
Das
Seite 140
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Dokument speichert die Applikationsdaten. Die Ansicht stellt die Daten dar und
übersetzt Benutzereingaben in Operationen auf dem Dokument.
Das Objekt, das wir nun noch berücksichtigen müssen, ist das Rahmenfenster, das
den physikalischen Arbeitsplatz der Applikation auf dem Bildschirm definiert und als
Container für die Ansicht dient. Eine SDI-Applikation benutzt nur ein Rahmenfenster
– ein CFrameWnd, das als Top-level Fenster der Applikationen dient und die
Ansichten des Dokuments einrahmt. Wie im Kapitel 7.3.1 zu sehen sein wird, benutzt
eine MDI-Anwendung mehrere untergeordnete Fenster anstatt des Rahmenfensters.
Rahmenfenster spielen eine wichtige Rolle und eine oft falsch verstandene Rolle bei
der Operation von Dokument/Ansicht-Applikationen. MFC-Anfänger denken bei
Rahmenfenstern zu oft an ein einfaches Fenster. Tatsächlich ist ein Rahmenfenster
ein intelligentes Objekt, das viel von dem dirigiert, was hinter den Kulissen einer
Dokument/Ansichts-Applikation abläuft. Zum Beispiel bietet die CFrameWnd-Klasse
OnClose und OnQueryEndSession-Behandlungsfunktionen, um sicherzustellen,
dass der Benutzer eine Chance erhält, ungesicherte Daten zu sichern, bevor die
Anwendung terminiert und Windows herunterfährt. Die CFrameWnd-Klasse behandelt
auch die grundlegend wichtige Aufgabe der Größenanpassung von Ansichten, wenn
die Größe des Rahmenfensters verändert wird (resizing), oder andere Aspekte des
Fensterlayouts sich ändern. Sie enthält Operationen für die Manipulation der
Werkzeugleisten (Toolbars) und Statusleisten (Statusbars), die Identifizierung aktiver
Dokumente und Ansichten, und vieles mehr.
Vielleicht ist der beste Weg, den Beitrag der CFrameWnd-Klasse zu verstehen, sie
mit der grundlegenderen Klasse CWnd zu vergleichen. Die CWnd-Klasse ist im
Grunde eine C++-Wrapper Klasse für ein gewöhnliches Fenster. CFrameWnd ist
abgeleitet von CWnd und fügt nur die Glöckchen, Pfeifchen und sonstige
Verzierungen hinzu, die ein Rahmenfenster benötigt um eine proaktive Rolle bei der
Ausführung einer Dokument/Ansicht-Anwendung zu erreichen.
7.1.6 Dynamische Objekterzeugung
Wenn das Framework sich anschickt Dokument-, Ansicht- und RahmenfensterObjekte zu erzeugen während des Ablaufs einer Programmausführung, sind die
Klassen, von denen die Objekte konstruiert werden mit einem Feature ausgestattet,
das sich dynamische Objekterzeugung nennt (dynamic creation). MFC erleichtert die
Arbeit beim Schreiben dynamisch erzeugbarer Klassen durch die Makros
DECLARE_DYNCREATE und IMPLEMENT_DYNCREATE. Alles was man zu tun hat ist:
1. In die Klassendeklaration wird ein Aufruf des Makros DECLARE_DYNCREATE
eingebaut. DECLARE_DYNCREATE akzeptiert nur einen Parameter – der Name
der Klasse, die dynamisch erzeugbar sein soll.
2. Das IMPLEMENT_DYNCREATE-Makro wird außerhalb der Klassendeklaration
aufgerufen. IMPLEMENT_DYNCREATE akzeptiert zwei Parameter – den Namen
der Klasse, die dynamisch erzeugbar sein soll und den Namen der Basisklasse.
Eigentlich ist die Bezeichnung dynamisch erzeugbare Klasse falsch, denn nicht die
Klasse, die diese Makros benutzt, sondern Objekte dieser Klasse können nun zur
Laufzeit mit einem Statement wie das folgende erzeugt werden:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 141
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
RUNTIME_CLASS (CMyClass)->CreateObject();
Es gibt keinen grundlegenden Unterschied von der Verwendung des new-Operators
zur Erzeugung eines CMyClass-Objektes, aber es umgeht einen kleinen Mangel in
der Sprache C++. In C++ existiert keine Möglichkeit, ein Objekt über new zu
instanziieren von dem nur der Objektname, jedoch nicht der Typ bekannt ist. Oft
weiß man aber zur Programmlaufzeit nicht, welcher Typ ein Objekt besitzt. Man
muss die Run-Time-Type-Information zu Rate ziehen. Statements wie die folgenden
funktionieren nicht:
CString strClassName = “MyClss”;
CMyClass* ptr = new strClassName;
Der Compiler versucht ein Objekt mit vom Typ „strClassName“ zu erzeugen, da er
nicht registriert, dass strClassName nur eine Variable ist, die den Klassennamen
im eigenen Namen enthält. Er versucht jedoch hartnäckig eine Klasse mit dem Literal
strClassName als Namen zu instanziieren, was schief gehen muss, denn diese
Klasse gibt es nicht.
Was aber passiert tatsächlich, wenn man eine Klasse dynamisch erzeugbar
schreibt? Das DECLARE_DYNCREATE-Objekt benötigt drei Member in der
Klassendeklaration: ein statisches Datenmember vom Typ CRuntimeClass, eine
virtuelle Funktion namens GetRuntimeClass und eine statische Funktion namens
CreateObject. Wenn man in seinen Code
DECLARE_DYNCREATE (CMyClass)
einfügt, ergibt der Präpropzessorlauf
public:
static AFX_DATA CRuntimeClass classCMyClass
virtual CRuntimeClass* GetRuntimeClass() const;
static CObject PASCAL CreateObject();
Das IMPLEMENT_DYNCREATE-Makro initialisiert die Struktur CRuntimeClass mit
Informationen wie den Klassennamen, die Größe des instanziierten Objekts, die
Adresse der Basisklasse. Es erzeugt auch den Inlinecode für die Funktionen
GetRuntimeClass()
und
CreateObject().
Wenn
das
IMPLEMENT_DYNCREATE-Makro in der Version 4.x der MFC wie folgt aufgerufen
wird:
IMPLEMENT_DYNCREATE (CMyClass, CBaseClass)
dann wird CreateObject folgendermaßen implementiert:
CObject* PASCAL CMyClass::CreateObject() {
return new CMyClass; }
Seite 142
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Frühere MFC-Versionen benutzten eine davon abweichende Implementation von
CreateObject, die Speicher unter Verwendung der in der CRuntimeClassStruktur gespeicherten Größeninformation allokierte und manuell das Objekt im
Speicherbereich initialisierte. Die Implementation von CreateObject in der MFC ab
Version 4 ist „echteres“ C++, da für eine dynamisch erzeugbare Klasse, die den newOperator überlädt, CreateObject den überladenen Operator verwendet.
7.1.7 Mehr über das SDI-Dokumententemplate
An früherer Stelle in diesem Kapitel wurde ein Beispiel gezeigt, wie ein SDIDokumententemplate aus einer CSingleDocTemplate-Klasse erzeugt wird. Dem
Konstruktor des Template wurden vier Parameter übergeben: ein Integer-Wert gleich
IDR_MAINFRAME und drei RUNTIME_CLASS-Zeiger. Der Zweck dieser
RUNTIME_CLASS-Makros sollte nun klar sein, so dass der Integer-Wert im ersten
Parameter näher in Augenschein genommen werden muss. Dies ist aktuell eine
Mehrzweck-ID, mit der die folgenden Ressourcen identifiziert werden:
-
Das Icon der Applikation.
Das Menü, das mit Dokumenten diesen Typs assoziiert wird.
Die Tabelle mit den Beschleunigungs-Tasten (accelerator table), die zum Menü
gehört.
Ein „Dokumenten-String“ der die voreingestellte Dateierweiterung für Dokumente
diesen Typs dient, sowie andere Dokumenteneigenschaften.
In einer SDI Dokumenten/Ansicht-Anwendung erzeugt das Framework zuerst ein
Top-Level Fenster für die Applikation durch die Erzeugung eines RahmenfensterObjektes. Hierzu wird die Laufzeitinformation, die im Dokumententemplate
gespeichert ist, benutzt. Anschließend folgt der Aufruf der LoadFrame-Funktion des
Objektes. Einer der Parameter, die LoadFrame akzeptiert, ist eine Ressourcen-ID,
die die vier obigen Ressourcen identifiziert. Es überrascht nicht, dass die
Ressourcen-ID, die das Framework an LoadFrame übergibt dieselbe ist, die auch an
das Dokumententemplate geliefert wurde. LoadFrame erzeugt das Rahmenfenster
und und lädt die Ressourcen Menü, Acceleratoren und Icon zusammen in einem
Schritt, aber wenn der Prozess erst einmal arbeitet, müssen alle diese Ressourcen
die gleiche ID zugewiesen bekommen. Dies ist auch der Grund, warum die selben in
der .rc-Datei definierten ID‘s für eine Dokumenten/Asicht-Applikation vom
Framework mehrfach für verschiedene Ressourcen verwendet werden.
Der Dokumentenstring ist eine String-Ressource die aus einer Kombination aus
sieben Teilstrings gebildet wird, die durch das Whitespace-Zeichen ‚\n‘ getrennt
werden. Jeder Teilstring beschreibt eine Eigenschaft des Rahmenfenster oder des
Dokumententyps. Von links nach rechts gelesen, bedeuten die Teilstrings folgendes:
(Zum weiteren Verständnis trägt eventuell die Abbildung 59 bei:)
-
Der Titel, der in der Titelleiste des Rahmenfensters erscheint. Für top-level
Rahmenfenster ist dies normalerweise der Name der Applikation – z.B. „Microsoft
Draw“. Für die child windows document frames, die in MDI-Anwendungen zum
Einsatz kommen, ist dieser Teilstring normalerweise leer.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 143
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
-
-
-
-
Der Name, der mit neuen Dokumenten verknüpft wird. Falls dieser Teilstring
ausgelassen wird, wird als Voreinstellung der Name ‚Untitled‘verwendet.
Ein beschreibender Name für den Dokumententyp, der gemeinsam mit anderen
Dokumententypen in einer Dialogbox erscheint, wenn der Benutzer den Befehl
Neu aus dem Datei-Menü benutzt – z.B. ‚Tabellenkalkulation‘ oder ‚Zeichnung‘.
Dieses Teilstring kommt nur in MDI-Anwendungen zum Einsatz, die zwei oder
mehr Dokumenttypen registrieren.
Ein beschreibender Name des Dokument-Typs, kombiniert mit einer WildcardDateispezifikation, die jeweils die voreingestellte Erweiterung des Dateinamens
darstellt – z.B. „Drawing Files (*.drw)“. Dieser Teilstring wird im Feld „Dateityp“
der Dialogbox „Öffnen“oder „Speichern unter...“ verwendet.
Eine voreingestellte Dateinamens-Erweiterung für Dokumente diesen Typs – z.B.
„.drw.“
Ein Name ohne Leerzeichen, der den Dokumenttyp in der Registry identifiziert –
z.B.
„Draw.Document.“
Wenn
die
Applikation
CWinApp::RegisterShellFileTypes benutzt, um ihre Dateitypen zu
registrieren, wird dieser Teilstring als Wert für den HKEY_CLASSES_ROOTUnterschlüssel benutzt, der wiederum nach der Dateierweiterung des Dokuments
benannt wird.
Ein beschreibender Name für den Dokumenttyp – z.B. „Microsoft Draw
Document.“ Ungleich dem Teilstring, der diesem im Dokumentenstring
vorausgeht, darf dieser Teilstring Leerzeichen enthalten. Wenn die Applikation
CWinApp::RegisterShellFileType benutzt um ihre Dokumenttypen zu
registrieren, ist dies der für menschliche Lebewesen lesbare Namen, den die
Windows 95 oder Windows NT – Shell im Eigenschafts - Dialog für Dokumente
diesen Typs anzeigt.
Erweiterung des Dateinamen
Bezeichnung des Dokumenttyps
Abbildung 59: Dialog „Öffnen“
Man muss nicht alle sieben Teilstrings verwenden, wenn man eine
Dokumentenstring - Ressource bildet; man kann einzelne Teilstrings auslassen,
indem man nach dem letzten Trennzeichen ‚\n‘ sofort das nächste Trennzeichen
Seite 144
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
‚\n‘einfügt, und man kann nachfolgende Nullzeichen weglassen. Wenn man eine
Applikation mit Hilfe des AppWizards erstellt, bildet der AppWizard den
Dokumentenstring anhand der Infos, die zuvor im Eigenschaften-Dialog „Advanced
Options“ eingegeben wurden. Die Ressourcen-Statements für einen typischen SDIDocumentstring könnte folgendermaßen aussehen:
STRINGTABLE
BEGIN
IDR_MAINFAME “MicrosoftDraw\n\n\nDraw Files
(*.drw)\n.drw\nDraw.Document\nMicrosoft Draw Document”
END
(Anm.: der SDI-Dokumentenstring muss in einer Zeile ohne festen Zeilenumbruch
stehen! Obiger String wurde vom automatischen Zeilenumbruch auf zwei Zeilen
verteilt.)
Das Schlüsselwort STRINGTABLE erzeugt eine string-table-Ressource. Dies ist eine
Ressource, die aus einem oder mehreren Textstrings besteht, wobei jeder Textstring
durch eine eindeutige Ressourcen-ID identifizierbar ist. Andere Schlüsselworte sind
DIALOG, der eine Dialogressource erzeugt, oder MENU, der eine Menüressource
erzeugt. Wenn die Applikation mit einem leeren Dokument gestartet wird, hat sein
Rahmenfenster den Titel „Untitled – Draw“. Die voreingestellte Erweiterung des
Dateinamen ist „*.drw“ und „Dawing Files (*.drw)“ ist eine der Auswahlmöglichkeiten
die in der Combobox „Datei des Typs“ innerhalb den Standard-Dialogboxen „Öffnen“
und „Speichern unter...“ .erscheint.
Nachdem ein Dokumententemplate erzeugt ist, können die Teilstrings, die zu dem
Documentstring gehören über die Funktion CDocTemplate::GetDocString der
MFC wiedergefunden werden. Beispielsweise kopiert das Statement
CString strDefExt;
pDocTemplate->GetDocString(strDefExt, DocTempalte::filterExt);
die voreingestellte Erweiterung des Dateinamens in die CString-Variable, mit dem
Namen strDefExt.
7.1.8 Registrierung von Dokument-Typen bei der Shell des Betriebsystems
In Windows 95 und Windows NT 4 können Dateien durch Doppelklick auf das Icon
oder Klicken mit der rechten Maustaste auf das Icon und Auswählen des
Menüpunktes „Öffnen“ im erscheinenden Kontextmenü in der sogenannten „native“
Applikation gestartet werden, d.h. mit der Applikation aufgerufen werden, die Dateien
diesen Typs erzeugt. Zusätzlich können Dokumente durch den Menüeintrag
„Drucken“ im Kontextmenü unter Verwendung der Druckbefehle der native
Applikation ausgedruckt werden (vergleiche ). Ebenso kann eine Datei mit der Maus
ge-drag-t und in eine Anwendung gezogen werden, die den Dateityp „versteht“ und
dort ge-drop-t werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 145
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Abbildung 60: Kontextmenüpunkte „Öffnen“ und „Drucken“ im Explorer
Damit diese Operationen funktionieren, muss eine Applikation Ihre Dokumenttypen
bei der Shell des Betriebssystems anmelden. Dies bedeutet,, dass die Applikation
eine Serie von Einträgen zum Registry-Zweig HKEY_CLASSES_ROOT hinzufügen
muss, die alle Dateinamen-Erweiterungen eines Dokumenttyps identifiziert und
angibt, welche Operationen im Kontextmenü erscheinen sollen, bzw. wie die
Operationen mit Hilfe der Applikation ausgeführt werden. In einer konventionellen
Windows-Applikation wird die Registrierung durchgeführt, indem eine .reg-Datei
mitgeliefert wird, die der Benutzer in die Registry „mergen“ kann, oder
programmtechnisch durch das Schreiben der Einträge in die Registry mit
::RegCreateKey, ::RegSetKey, ::RegSetValue und anderen WIN32-APIFunktionen. Im Gegensatz dazu benötigt eine MFC-Applikation nur eine einfache
Funktion und registriert dadurch jeden Dateityp, den sie unterstützt. Der Aufruf von
CWinApp::RegisterShellFileTypes()
nach dem finalen Aufruf von AddDocTemplate in InitInstance schmiedet die
Verbindung zwischen der Applikation, den Dokumenten, die sie erzeugt und der
Windows 95 oder Windows NT Shell. Der Aufruf von RegisterShellFileTypes
fügt auch einen Eintrag in die Registry, um das Icon zu identifiziert, das in der
ausführbaren Datei der Applikation gespeichert ist. Dadurch wird jedes DokumentObjekt, das die Windows Shell darstellt, mit Hilfe dieses Icons angezeigt.
Eine mit CWinApp verknüpfte Funktion ist EnableShellOpen. Sie fügt den MDIDokument/Ansicht-Anwendungen eine nettes Feature hinzu. Hat eine Applikation ein
Dokumenttyp mit RegisterShellFileTypes und EnableShellOpen registriert,
und ein Benutzer doppelklickt ein Dokument-Icon in der Shell während die
Applikation ausgeführt wird, startet die Shell keine zweite Instanz der Applikation;
statt dessen sendet die Shell ein DDE Kommando „open“ an die existierende Instanz
und übergibt den Dateinamen des Dokuments. Ein DDE-Handler, der in der
CDocManager-MFCKlasse eingebaut ist, die Dokumente im Namen von CWinApp
managed, filtert die Nachricht und ruft OnOpenDocument auf, um das Dokument zu
Seite 146
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
öffnen. Demzufolge erscheint das Dokument in einem neuen Fenster innerhalb des
Top-Level MDI-Rahmens, gerade so als habe der Benutzer das Dokument über die
Menübefehle „Datei“ und „Öffnen“ der Applikation geladen. Ähnliche DDEKommandos erlauben Druckanfragen der Betriebssystem Shell durch laufende
Applikationsinstanzen auszuführen. Jeder der jemals DDE-Code geschrieben hat,
weiß ein Lied davon zu singen, dass dies ohne die Hilfe des Frameworks kein
Picknick ist.
Man kann „Drag and Drop“ Unterstützung für Dokumente in Dokument/AnsichtApplikationen einfügen durch Aufruf der Funktion DragAcceptFiles der
Rahmenfenster-Klasse.
DragAcceptFiles
registriert
Fenster,
damit
WM_DROPFILES-Nachrichten von ihnen empfangen werden können, wenn sie Ziel
einer Drop-Operation sind, bei der Dokument-Icons aus dem Dokumentcontainer der
Shell (z.B. Shell-Folder) beteiligt sind. Die CFrameWnd-Klasse bietet einen
OnDropFiles-Handler für WM_DROPFILES-Nachrichten, der die OnOpenDocumentFunktion, mit dem Namen der Datei die ge-drop-pt wurde, aufruft.
DragAcceptFiles arbeitet gleich für SDI- und MDI-Anwendungen. Die Funktion
wird normalerweise aus InitInstance aufgerufen durch ein Statement wie dieses:
m_pMainWnd->DragAcceptFiles();
In
einer
SDI-Anwendung
ist
es
wichtig
DragAcceptFiles nach
ProcessShellCommands aufzurufen, da das Rahmenfenster nicht existiert, bevor
das ProcessShellCommand aufgerufen wurde.
7.1.9 Kommando-Routing (Command Routing)
Eine der bemerkenswertesten Eigenschaften der Dokument/Ansicht-Architektur ist,
dass eine Applikation Kommandonachrichten nahezu überall behandeln kann.
Kommandonachrichten sind in der Sprache der MFC WM_COMMAND und WM_NOTIFYNachrichten, die generiert werden, wenn ein Eintrag eines Menüs selektiert wurde,
ein Tastatur-Accelerator eingetippt wurde oder ein Control eine Benachrichtigung an
sein Eltern-Objekt sendet. Das Rahmenfenster ist der Empfänger der meisten
Kommandonachrichten, aber sie können auch im Ansicht-Objekt, im DokumentObjekt oder im Applikations-Objekt behandelt werden, wenn man einfach Einträge für
die Nachrichten die man behandeln möchten in der Message Map einfügt. Das
Kommando Routing ist das Verfahren, das dem Programmierer die Möglichkeit gibt,
Kommandos in den Klassen zu verarbeiten, in denen es Sinn macht, anstatt alles in
die Klasse des Rahmenfensters hineinzupacken. Update-Kommandos für MenüEinträge, Toolbar Buttons und andere Objekte des Benutzerinterfaces sind genauso
Subjekte des Kommando-Routings, so dass ON_UPDATE_COMMAND_UI-Handler
ebenfalls in Klassen gepackt werden können, die nicht vom Typ Rahmenfenster sind.
Der Mechanismus der das Kommando-Routing zum Laufen bringt, ist tief versteckt in
den Eingeweiden des Frameworks. Wenn ein Rahmenfenster eine WM_COMMAND
oder WM_NOTIFY-Nachricht erhält, ruft es die virtuelle Funktion OnCmdMessage auf,
die ein Feature in allen von CCmdTarget abgeleiteten Klassen ist. Damit beginnt der
Routing-Prozeß. Die Implementation von OnCmdMessage in der Klasse CFrameWnd
sieht so aus:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 147
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
BOOL CFrameWnd::OnCmdMsg(...) {
//zuerst durch die aktuelle Ansicht schleusen.
CView pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(...))
return TRUE;
//Dann durch das Rahmenfenster.
if(CWnd::OnCmdMsg(...))
return TRUE;
//Last but not least, durch die App schleusen
CWndApp *pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(...))
return TRUE;
return False
}
CFrameWnd::OnCmdMsg routet die Nachricht zuerst an die aktive Ansicht durch
Aufruf der Ansicht-Funktion OnCmdMsg. Falls pView->OnCmdMsg den Rückgabewert
0 ergibt, der anzeigt, dass die Nachricht nicht von der Ansicht verarbeitet wurde (was
bedeutet, dass die Messagemap der Ansicht keinen Eintrag für die Nachricht
aufweist), versucht das Rahmenfenster die Nachricht selbst zu verarbeiten durch den
Aufruf der Funktion CWnd::OnCmdMsg. Falls dies auch fehlschlägt, bemüht das
Rahmenfenster danach das Applikations-Objekt. Falls letzten Endes keines der
Objekte die Nachricht verarbeitet, liefert die Funktion CFrameWnd::OnCmdMsg()
den Rückgabewert FALSE für die Standardverarbeitung.
Dies erklärt, wie eine Kommando-Nachricht, die durch das Rahmenfenster
empfangen wurde, an die aktive Ansicht und das Applikations-Objekt geroutet wird.
Aber was ist mit dem Dokument-Objekt? Wenn CFrameWnd::OnCmdMsg die
Funktion OnCmdMsg der aktiven Ansicht aufruft, versucht die Ansicht zuerst die
Nachricht selbst zu verarbeiten. Falls die Ansicht keinen Handler für die Nachricht
besitzt, ruft sie die Funktion OnCmdMsg des Dokument-Objektes. Falls das
Dokument-Objekt ebenfalls versagt, geht es die Treppe weiter aufwärts zum
Dokumenten-Template. Die Abbildung 61 illustriert den gesamten Pfad, den eine
Kommando-Nachricht entlang wandert, wenn sie an ein SDI-Rahmenfenster geliefert
wird. Die aktive Ansicht erhält die erste Chance, gefolgt vom Dokument, das mit der
Ansicht verbunden ist, dem Dokumenten-Template, dem Rahmenfenster selbst und
zuletzt dem Applikation-Objekt. Das Routing stoppt wenn kein Objekt entlang diesem
Weg die Nachricht verarbeitet und sie schließlich bei ::DefWindowProc angelangt
ist, falls keine der Message Maps der Objekte einen Eintrag für die Nachricht enthält.
Das Routing ist fast identisch für Kommando-Nachrichten, die bei MDIRahmenfenster ankommen, indem das Framework dafür sorgt, dass alle relevanten
Objekte die Möglichkeiten erhalten einzuspringen, mit eingeschlossen die KinderRahmenfenster, welche die aktive Ansicht umgeben.
Seite 148
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
::DefWindowProc
Applikation-Objekt
Rahmen Fenster
Dokumenten Template
Dokument
Aktive Ansicht
Abbildung 61: Routing einer an ein SDI-Rahmenfenster gesendeten
Kommando-Nachricht
Der Wert des Kommando-Routing wird offensichtlich, wenn man betrachtet, wie eine
typische Dokument/Ansicht-Anwendung Kommandos aus Menüs, Acceleratoren und
Toolbar-Buttons verarbeitet. Durch Konvention werden die Kommandos Datei-Neu,
Datei-Öffnen und Datei-Schließen auf das Applikation-Objekt gemappt, wo CWinApp
die Memberfunktionen OnFileNew, OnFileOpen und OnFileExit für die
Verarbeitung bietet. „Datei speichern“ und „Datei speichern unter...“ werden auf das
Dokument-Objekt
gemappt,
das
die
default-Implementierung
CDocument::OnFileSave und CDocument::OnFileSaveAs bietet. Kommandos
zum Anzeigen und Verstecken der Werkzeugleisten und Statusleisten werden im
Rahmenfenster verarbeitet unter Verwendung der Memberfunktionen in der Klasse
CFrameWnd. Die meisten anderen Kommandos werden entweder durch das
Dokument oder die Ansicht verarbeitet.
Ein wichtiger Punkt, den man sich bei der Überlegung „Wohin mit dem
Nachrichtenhandler-Handler?“ im Hinterkopf merken sollte ist, dass nur KommandoNachrichten und Benutzerinterface-Updates Subjekte des Routings sind. „Standard“Windows-Nachrichten wie WM_CHAR, WM_LBUTTONDOWN, WM_CREATE, und WM_SIZE
müssen durch das Fenster verarbeitet werden, das die Nachricht empfängt. Mausund Tastatur-Nachrichten gehen generell an die Ansicht und die meisten anderen
Nachrichten an das Rahmenfenster. Dokument-Objekte und Applikation-Objekte sind
niemals Empfänger von Kommando-Nachrichten.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 149
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.1.10 Vordefinierte Kommando-ID‘s und default-Kommando-Implementationen
Wenn man eine Dokument/Ansicht-Anwendung programmiert, kodiert man
typischerweise die Handler für die Menübefehle nicht alle selbst. CWinApp,
CDocument, CFrameWnd und andere MFC-Klassen bieten Standardhandler für
allgemeine Menü-Befehle wie „Datei öffnen“ und „Datei speichern“. Zusätzlich bietet
das Framework ein Sortiment an Kommando-ID‘s für Standard Menü-Einträge wie
ID_FILE_OPEN oder ID_FILE_SAVE, von denen viele in den Message-Maps der
Klassen „vorverdrahtet“sind, die sie benutzen.
Die Tabelle 22 am Ende dies Kapitels listet die gebräuchlichsten, vordefinierten
Kommando-ID‘s und die damit assoziierten Kommando-Handler auf. Die Spalte
„vorverdrahtet?“ zeigt an, ob die Handler automatisch aufgerufen werden (Ja) oder
nur wenn der Programmierer einen korrespondierenden Eintrag in der Massage Map
der Klasse verwendet (Nein). Ein vorverdrahteter Handler wird eingeschaltet wenn
die korrespondierende Kommando-ID einem Menü-Eintrag zugewiesen wurde; ein
Handler der nicht vorverdrahtet ist, wird eingeschaltet, wenn die ID eines MenüEintrags zu der Handler-Funktion über einen Eintrag in der Message-Map
zugewiesen wird. Beispielsweise können die Defaultimplementationen der
Kommandos Datei-Neu und Datei-Öffnen in den Funktionen OnFileNew und
OnFileOpen der Klasse CWinApp gefunden werden, aber keine der beiden
Funktionen ist mit der Applikation verbunden, bis ein Eintrag ON_COMMAND in der
Message Map eingefügt wird (Kommt der AppWizard zur Erstellung des Skeletts
einer SDI- oder MDI-Anwendung zur Verwendung, so schreibt dieser die
ON_COMMAND-Einträge für OnFileNew und OnFileOpen, sowie für andere defaultHandler). CWinApp::OnAppExit arbeitet von sich aus und benötigt keinen Eintrag
in der Message Map. Man muss nur beachten, dass dem Menüeintrag DateiBeenden die ID ID_APP_EXIT zugewiesen wird. Und schon wird OnAppExit wie
magisch beim Beenden der Anwendung aufgerufen, wenn der Benutzer „Beenden“
aus dem „Datei“-Menü auswählt. Der Grund ist ganz einfach: CWinApp’s MessageMap enthält ein Eintrag
ON_COMMAND(ID_APP_EXIT, OnAppExit)
und Message Maps werden, wie alle andere Klassenmember, durch Vererbung an
die abgeleiteten Klassen weitergegeben.
Die MFC-Klassen CEditView und CRichEditView enthalten bereits für einige der
in Tabelle 22 im Edit-Menü aufgeführten Einträge die entsprechenden KommandoHandler. Andere Ansichten müssen jedoch Ihre eigenen Handler mitbringen, bzw. sie
müssen programmiert werden.
Man muss natürlich nicht die vordefinierten Kommando-ID‘s oder KommandoHandler, die das Framework bietet, verwenden. Man kann sich jederzeit selbst
eigene Handler ausdenken und eigene ID‘s definieren, vorausgesetzt natürlich die
entsprechenden Einträge in der Message Map wurden eingefügt um eigene ID‘s und
default Handler, oder default ID‘s und eigene Handler oder eigene ID‘s und eigene
Handler zusammenzubringen. Kurz gesagt, man kann soviel Unterstützung des
Frameworks in Anspruch nehmen, wie man gerne möchte. Aber je mehr man das
Framework kennenlernt, desto weniger Code wird man selbst schreiben.
Seite 150
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.1.11 Die erste Document/View-Anwendung – Paint
Nun, da wir ein wage Vorstellung davon haben, was die Dokumenten/AnsichtArchitektur alles umfaßt, und den Anflug eines Gefühls für die Implementationsdetails
haben, ist die Zeit reif, um eine eigene Dokument/Ansicht-Anwendung zu kreieren.
Falls einige der bislang vorgestellten Konzepte ein wenig abstrakt erschienen, dann
sollte es weiterhelfen, wenn man den Code einer funktionierenden
Dokument/Ansicht-Anwendung nachvollzieht, und die Dinge so ins rechte Licht rückt.
Das hier vorgestellte Programm wurde schon in vielen Büchern als Lehrbeispiel
benutzt, durchgekaut und noch mal entwickelt. Es handelt sich um eine MalProgrämmchen im SDI-Gewand, das dem Benutzer ermöglicht, gerade Linien
verschiedener Dicke und Farbe auf den Bildschirm zu zaubern. In den folgenden
Kapiteln wird die Anwendung nach dem stringenten Konzept der Wiederverwendung
weiter benutzt, um mehrere Ansichten auf dasselbe Dokument zu demonstrieren
(unter Verwendung von Splitter-Windows).Die anschließende Konversion in eine
MDI-Anwendung ist dann eine verhältnismäßig einfache Übung. Der vollständige
Code der Anwendung ist in den folgenden Kapiteln abgedruckt.
Paint besteht aus mehreren Dateien. Je Klasse eine Headerdatei (.h) und eine
Implementierungsdatei (.cpp). Dazu kommen die Dateien Resource.h mit den
#defines und die paint1.rc mit den Ressourcenstatements. Die Aufteilung
einzelner Klassen in je zwei Dateien ist ein gängiges Prinzip das auch der
AppWizzard einsetzt um die Übersichtlichkeit zu erhöhen. Das Prinzip wird oft unter
dem Stichwort „Interfaceseparation“ verwendet, da die Klassendefinition in der
Headerdatei das Interface der Klasse darstellt.
Daneben ergibt sich die Möglichkeit, modulweise zu compilieren, was viele Compiler
ausnützen. Die einzelnen *.cpp-Klassendateien werden unabhängig voneinander in
je eine Objektcode-Datei übersetzt, die der Linker anschließend zum
Gesamtprogramm zusammenfügt. Änderungen in einer Klasse führen somit zu einer
Neuübersetzung der entsprechenden .cpp-Datei und anschließendes Linken zum
Projekt, anstatt einer neuen Übersetzung des gesamten Projektes mit allen Dateien.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 151
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Das Paint-Projekt besteht aus folgenden Dateien:
-
Die Applikationsklasse CPaint1App, die von CWinApp abgeleitet ist. (Dateien
Paint1.cpp und Paint1.h)
Die Rahmenfenster-Klasse CMainFrame, die von CFrameWnd abgeleitet ist.
(Dateien MainFrm.cpp und MainFrm.h)
Die Dokument-Klasse CPaint1Doc, die von CDocument abgeleitet ist.
(Paint1Doc.cpp und Paint1Doc.h)
Die Ansichtsklasse CPaint1View, die von CScrollView abgeleitet ist. (Dateien
Paint1View.cpp und Paint1View.h)
Die Dialogklasse CAboutDialog, die von CDialog abgeleitet wurde. (in der
Datei paint1.h).
Die Klasse CLine, die von CObject abgeleitet ist. (Dateien CLine.cpp und
CLine.h)
Im folgenden sollen die wichtigen Stellen der Applikation-, Rahmenfenster-,
Dokument und Ansichtklasse angerissen werden.
7.1.11.1
Das Programm
Die Projekt- und Sourcecode-Dateien zu Paint1 werden während des MFC-Kurses
verteilt.
7.1.11.2
Die Klasse CPaintApp
CPaintApp ist die Applikations-Klasse. Sie enthält zwei eigene Memberfunktionen:
InitInstance und OnAppAbout. Die zweite zeigt die About-Dialogbox, wenn der
Benutzer „Info über Paint 1 ...“ im „Hilfe“-Menü auswählt. Ein Eintrag für eine
ON_COMMAND-Nachricht verbindet den Menüeintrag (ID = ID_APP_ABOUT) zur
CPaintApp::OnAppAbout-Funktion. Die Message-Map von CPaintApp verbindet
auch die Menüeinträge „Öffnen“ und „Neu“ im „Datei“- Menü mit den
korrespondierenden Handlern in CWinApp. Das Applikations-Objekt verarbeitet auch
das „Beenden“-Kommando im „Datei“-Menü. Der zugehörige Handler ist
CWinApp::OnAppExit, für den kein weiterer Eintrag in der Message Map benötigt
wird, da ID_APP_EXIT in der Message-Map von CWinApp bereits enthalten ist.
CPaintApp’s InitInstance-Funktion sieht wie die weiter oben in diesem Kapitel
untersuchte Funktion aus. Sie erzeugt ein Dokumenten-Template aus der MFCKlasse
CSingleDocumentTemplate,
registriert
das
Dokument
mit
AddDocumentTemplate und erzeugt ein Fenster auf dem Bildschirm durch den
Aufruf von ProcessCommandLine. Bevor die Funktion jedoch irgendwas davon
ausführt, werden die Statements:
SetRegistryKey(“Programmierung mit MFC”);
LoadStdProfileSettings();
LoadStdProfileSettings, kombiniert mit dem ID_FILE_MRU_FILE1-Eintrag im
„Datei“-Menü der Applikation, weist das Framework an, eine „most recently used“Seite 152
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Dateiliste in das „Datei“-Menü aufzunehmen. Das Framework übernimmt das
Caching der Dateinamen und die Anzeige der Dateien in dem Menü wo der Eintrag
ID_FILE_MRU_FILE1 auftritt vollständig alleine. Falls vier Dateinamen nicht genug
sind (oder zu viele sind), kann man die Zahl der angezeigten Dateinamen im
Wertebereich von 0 bis 16 beim Aufruf von LoadStdProfileSettings wählen
(z.B. LoadStdProfileSettings(8) für 8 Dateinamen). SetRegistryKey
instruiert das Framework die MRU-Dateinamen in der Registry anstatt in einer
eigenen .ini-Datei abzulegen, was standardmäßig für Applikationen unter
Windows95- oder NT verwendet werden sollte. Man kann sich gespeicherte MRUListen mit Hilfe des Tools Regedit.exe ansehen. Die Abbildung 62 zeigt die
beispielhaft.
Abbildung 62: Recent File List in der Registry
InitInstance ruft auch RegisterShellFileTypes auf, um die Dokumente
unserer Anwendung (*.pnt) bei der Betriebsystem-Shell zu registrieren, und
DragAcceptFiles um das Öffnen von Dokumenten via Drag and Drop zu
ermöglichen. Die Dateierweiterung .pnt ist im Dokumentenstring in der .rc-Datei
definiert.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 153
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.1.11.3
Die Klasse CMainFrame
CMainFrame ist das Rahmenfenster das als Hauptfenster der Anwendung dient. Die
enthält drei Memberfunktionen, die eine benutzerdefinierte Nachrichtenverarbeitung
darstellen:
• OnCreate
• OnMeasureItem
• OnDraw
OnCreate konvertiert die acht Einträge im „Farbe“-Menü des Programms in
Menüeinträge vom Typ „owner-drawn“. OnMeasureItem und OnDraw zeichnen
letztlich die Menüeinträge. Nochmals der Hinweis auf die beiden Makros
DECLARE_DYNCREATE und IMPLEMENT_DYNCREATE, um CMainFrame zu einer
dynamisch erzeugbaren Klasse zu machen.
//In MainFrame.h
DECLARE_DYNCREATE(CMainFrame)
//In MainFrame.cpp
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
Diese Statements werden benötigt, damit das Framework das Anwendungsfenster
erzeugen kann. Ähnliche Statements sind in CPaintDoc und CPaintView
wiederzufinden (natürlich mit anderen Klassennamen).
Wenn das Framework einen „owner-drawn“-Menüeintrag in Folge einer
WM_DRAWITEM-Nachricht zeichnet, erhält es einen COLORREF-Wert, der die Farbe
der Menüeinträge – aus dem statischen Feld m_crColors – identifiziert. Der Pinsel,
der die Farbvorschau im „Farbe“-Menü zeichnet, wird wie folgt erzeugt:
pBrush = new CBrush(CPaintDoc::m_crColors[lpids->
itemID-ID_COLOR_BLACK])
Da die Farbe, mit denen die Linien gezeichnet werden, Teil der Dokumenten-Klasse
sind, und da das Rahmenfenster seine Farbinformation von der Dokumenten-Klasse
erhält,
ändert
sich
durch
das
Tauschen
von
Farben
im
Feld
CPaintDoc::m_crColors nicht nur die Farbe der angefertigten Zeichnung,
sondern auch die Farben im „Farbe“-Menü der Anwendung!
7.1.11.4
Die Klasse CPaintDoc
In Paint1 ist ein Dokument ein Satz an Linien, die durch CLine-Objekte definiert
sind. Als Speicher dient das Feld CPaint1Doc::m_lineArray. Dies ist ein Feld
vom Typ CObArray, das natürlich privat deklariert wurde, um direkten Zugriff auf
die Datenmember zu verbieten. Die notwendigen Funktionen für den Zugriff auf die
Daten sind:
Seite 154
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
-
-
CPaint1Doc::AddLine
mit
zwei
CPoint-Objekten
als
Übergabeparameter(Start- und Endpunkt der Linie). Die Informationen über die
Breite und Farbe einer Linie wird aus dem Dokument gewonnen. Somit kann ein
neues CLine-Objekt erzeugt und dem Array ein Zeiger darauf hinzugefügt
werden.
CPaintDoc::GetLine erwartet einen 0-basierten Index, um den Zeiger, der an
dieser Indexposition im Feld m_lineArray steht, zurückzugeben.
CPaint1Doc::GetLineCount gibt die Zahl der CLine-Objekte in
m_lineArray zurück.
Alle drei Funktionen werden vom View-Objekt der Anwendung verwendet. Wichtig ist
noch zu wissen, dass zur Verwaltung von CLine-Objekten zwei Variablen im
Dokument eingesetzt werden müssen(!). m_nWidth und m_nColor sind private
Datenmember, die einen Index auf aktuell eingestellte Breite und Farbe enthalten.
Die Frage, warum dies nicht in der Ansicht oder dem Rahmenfenster verwaltet wird,
ist einfach zu beantworten: Linien sind Eigenschaften des Dokuments. Sollten zwei
Ansichten das selbe Dokument zeigen, wirken sich Farb- und Breitenauswahl in
einem View sofort auch in der anderen Ansicht aus.
7.1.11.5
Die Klasse CPaintView
Wegen Änderungen in der Beispielapplikation vorübergehend herausgenommen...
7.1.11.6
Die Klasse Cline
Die Informationen über die Linien, die ein Benutzer mit der Anwendung zeichnet,
werden in einem Array aus CLine-Objekten abgelegt. Die Klasse CLine wurde
public von der MFC-Klasse CObject abgeleitet, wodurch CLine sehr einfach um
die Serialisierung erweitert werden kann, so dass die Gemälde durch das Hinzufügen
weniger Zeilen Code speicher- und ladbar sind.
Zum Zeichnen einer Linie benötigt man folgende Informationen:
-
2 x CPoint für die Koordinaten der zwei Endpunkte
1 x UINT für die Liniendicke
1 x COLORREF für die Linienfarbe
Neben der Kapselung der Info’s, die für das Zeichnen von Linien benötigt werden,
leisten die CLine-Objekte jedoch noch wesentlich mehr. Wurde der OnPaintHandler der Anwendung aufgerufen (View muss neu gezeichnet werden), so führt
OnPaint keine Zeichenoperation selbst aus, sonder weist durch Aufruf der virtuellen
Funktion CLine::Draw ein CLine-Objekt an, sich auf dem Bildschirm zu zeichnen.
Der einzigste Beitrag, den OnDraw dabei leistet, ist die Übergabe eines Zeigers auf
den device context, den CLine::Draw für seine Zeichenarbeit benutzt. Dieser
streng objektorientierte Ansatz von CLine ermöglicht es, die Klasse zum einen
weiter zu vererben und so problemlos die Wiederverwendbarkeit des Codes zu
erhöhen, und ergibt ein wesentlich robusteres Programmgerüst.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 155
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Damit wird die Klasse CLine folgendermaßen deklariert:
class Cline : public CObject
{
private:
CPoint m_ptFrom;
CPoint m_ptTo;
UINT m_nWidth;
COLORREF m_crColor;
public:
CLine(CPoint, CPoint, UINT, COLORREF)
virtuial void Draw(CDC*);
}
CLine::Draw benutzt einen GDI-Pinsle (GDI-Pen) mit den Werten von m_crColor
und m_nWidth, setzt den Pinsel in den device context und zeichnet eine gerade
Linie von m_ptFrom nach m_ptTo.
Die Datenrepräsentation im Dokument–Objekt besteht im Grunde nur aus einem
CObArray, also einem eindimensionalen Feld, das Zeiger auf die CLine-Objekte
enthält.
7.2 Mehr Ansicht
Wie bereits bekannt ist, sind Dokument/Ansicht-Applikationen nicht auf ein Dokument
und eine Ansicht auf die Dokumentendaten beschränkt. Unter Verwendung von
sogenannten Splitter-Fenstern, deren Funktionalität vollkommen durch das
Framework geliefert wird, kann eine „Single Document Interface“-Anwendung (SDI)
zwei oder mehrere Ansichten auf dasselbe Dokument in größenveränderbaren
„panes“ (=Teilfenster) präsentieren. Diese Teilfenster unterteilen den Client-Bereich
des Rahmenfensters. Die Dokument/Ansicht-Architektur geht natürlich auch so weit,
dass eine Anwendung mehrere Dokumente mit jeweils mehreren Ansichten ,
mehrere parallel geöffnete Dokumente und verschiedene Dokumenttypen bieten
kann. Auch wenn durch Windows95 und Windows NT 4.0 die Verwendung des
„Multiple Document Interface“ unterwandert wird, sind MDI-Anwendungen immer
noch vorherrschend und werden vielleicht wieder in Mode kommen, wie die
erfolgreiche Anwendungen aus dem Hause Microsoft und anderen führenden
Softwarefirmen zeigen.
Mit dem Wissen im Hinterkopf, was man für eine SDI-Anwendung benötigt, ist es
sehr einfach das Paradigma auf mehrere Dokumente und mehrere Ansichten zu
erweitern. Zuerst werden in diesem Kapitel die Splitterwindows gestreift, um zu
zeigen, wie mehrere Ansichten auf ein Dokument gehandhabt werden. Anschließend
soll ein Blick auf die MFC-Unterstützung zum Thema MDI zeigen, wie einfach aus
einer SDI-Anwendung eine MDI-Anwendung wird.
7.2.1 Mehrere Ansichten – Splitter-Windows
Für SDI-Anwendungen sieht das Applikationsframwork ein einfaches Hilfsmittel für
die Darstellungen von zwei oder mehreren Ansichten zur selben Zeit in Form der
Teilfenster oder Splitter-Windows vor. Splitter-Windows basieren auf der MFC-Klasse
CSplitterWindow. Ein Splitter-Window teilt ein Fenster in zwei oder mehrere
Seite 156
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
horizontale, vertikale oder horizontale und vertikale Bereiche auf. Diese durch
Splitterbars in der Größe einstellbaren Bereiche werden auch als Panes bezeichnet.
Jeder Splitterbereich enthält eine Ansicht auf die Daten eines Dokuments. Die
Ansichten sind die Kinder des Splitter-Windows und das Splitter-Windows wiederum
ist ein Kind des Rahmenfensters. In einer SDI-Anwendung ist ein Splitter-Window
das Kind des toplevel Rahmenfensters. Dahingegen ist ein Splitter-Window in einer
MDI-Anwendung das Kind eines MDI-Dokumentrahmens, das innerhalb des toplevel
Framewindows frei beweglich ist (floating Window), Eine Ansicht, die innerhalb eines
Splitter-Window positioniert wurde, kann CView::GetFrameWindow() benutzen,
um einen Zeiger auf sein Elternfenster zu bekommen.
Die MFC unterstützen zwei Typen von Teilfenstern: statische und dynamische. Die
Zahl der Spalten und Zeilen eines statischen Splitter-Windows wird bei der
Erzeugung gesetzt und kann vom Benutzer nicht beeinflußt werden. Dafür steht es
dem Benutzer frei, die Größe der Spalten und Zeilen eines statische Splitter-Window
individuell mittels Splitter-Reitern (splitter bar) zu verändern. Ein statische SplitterWindows kann maximal 16 Zeilen und maximal 16 Spalten besitzen. Im Gegensatz
dazu ist ein dynamischen Splitter-Window auf maximal 2 Spalten und maximal 2
Zeilen begrenzt, jedoch kann es interaktiv vom Benutzer gesplittet und wieder
zusammengefügt werden. Dies geschieht wiederum mit dem Splitterbar. Die
Ansichten innerhalb eines Splitter-Window sind nicht vollständig unabhängig
voneinander. Wenn ein dynamisches Teilfenster horizontal in zwei Panes gesplittet
wird, dann besitzen die beiden Zeilen eigenständige, horizontale Scroll-Balken und
teilen sich einen horizontalen Scrollbalken. Entsprechend besitzen die beiden
Spalten bei vertikalem Splitting einen gemeinsamen horizontale Scroll-Balken.
Abbildung 63 und Abbildung 64 zeigen diese Eigenschaften des dynamischen und
statischen Splitter-Windows.
Abbildung 63: SDI-Anwenung mit dynamischem Splitter-Window
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 157
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Abbildung 64: SDI-Anwendung mit statischem Splitter-Window
Dynamische Splitter-Windows können zur Programmlaufzeit vom Benutzer in Panes
geteilt werden, indem der Splitter-Bar, ein kleiner Reiter, links der vertikalen oder
oberhalb der horizontalen Bildlaufleiste mit der Maus bei gedrückter linker Maustaste
nach gezogen wird. Abbildung 65 zeigt einen solchen Splitter-Bar.
Splitter-Bar
Abbildung 65: Splitter-Bar eines dynamischen Splitter-Window
Die Wahl zwischen statischem und dynamischen Splitter-Window steht und fällt
natürlich mit der Frage, ob der Benutzer einer Applikation selbst darüber entscheiden
soll, ob und wann er das Fenster splitten möchte. Ein Anderes Kriterium könnte z.B.
sein, welche Art von Ansichten in der Anwendung zum Einsatz kommen. Es ist
natürlich einfacher, zwei oder mehrere Ansichten in einem statischen Splitter-Window
zu verwenden, da beim programmieren festgelegt wird, welche Ansicht in welchem
Teil des Fensters erscheinen soll. Die Ansichten in einem dynamischen SplitterFenster werden vollständig durch das Framework verwaltet. Dies bedeutet, dass ein
dynamisches Splitter-Fenster dieselbe Ansichts-Klasse für alle Ansichten verwendet,
es sei denn eine neue Klasse wird von CSplitterWnd abgeleitet und das
Standardverhalten modifiziert. Dies riecht nicht nur nach Arbeit...
7.2.2 Dynamische Splitter-Windows erzeugen
Dynamische Splitter-Windows werden über CSplitterView::Create erzeugt. Die
Erzeugung und Initialisierung eines dynamischen Splitter-Fensters geschieht in zwei
Schritten:
Seite 158
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
1. Hinzufügen eines Daten-Members vom Typ CSplitterWnd zur Klasse des
Rahmenfensters.
2. Überschreiben der virtuellen Funktion OnCreateClient des Rahmenfensters
und Aufruf von CSplitterWnd::Create, um ein neues Splitter-Window im
Clientbereich des Rahmenfensters zu erzeugen.
Die folgende OnCreateClient-Funktion überschreibt das Standardverhalten wie
oben beschrieben:
BOOL CMyFrame::OnCreateClient(LPCREATESTRUCT lpcs,
CCreateContext* pContext)
{
return m_wndSplit.Create(this,2,2,CSize(1,1),pContext);
}
In diesem Beispiel ist m_WndSplit ein Memberdatum vom Typ CSplitterWnd der
Rahmenfenster-Klasse CMyFrame. Der erste Parameter in CSplitterWnd:Create
identifiziert das Eltern-Objekt des Splitter-Window. Im zweiten und dritten Parameter
wird festgelegt, wie viele Bereiche vom Benutzer maximal erzeugt werden dürfen. Da
dynamische Splitter-Windows in maximal 2 Zeilen und 2 Spalten geteilt werden
können, sind diese Werte immer 1 oder 2. Im vierten Parameter werden die
Grenzmaße (Höhe und Breite) eines Panes in Form eines CSize-Objektes
übergeben. Unterschreitet/Überschreitet in unserem Beispiel ein Pane die Größe von
1 Pixel Breite oder 1 Pixel Höhe, so wird es zerstört/erzeugt. Der fünfte Parameter ist
ein Zeiger auf eine Struktur vom Typ CCreateContext, die vom Framework
geliefert wird. Das Member m_pNewViewClass identifiziert die Ansicht, die in den
Panes des Splitter-Window verwendet wird. Das Framework erzeugt die erste
Ansicht und setzt sie in das erste Teilfenster. Weiter Ansichten erzeugt das
Framework ebenfalls selbständig, so wie neue Teilfenster erzeugt werden.
CSplitterWnd::Create bietet zwei weitere, optionale Parameter. Dort können der
Style und die Child-Window-ID angegeben werden. In den meisten Fällen reichen die
Voreinstellung aus. Die voreingestellte Child-Window-ID AFX_IDW_PANE_FIRST ist
eine „magig number“, die es dem Rahmenfenster ermöglicht, die mit SplitterWindows verknüpften Ansichten zu identifizieren. Diese ID muss verändert werden,
wenn man ein zweites Splitter-Window in einem Rahmenfenster erzeugt, welches
bereits ein Splitter-Window enthält.
Wenn ein dynamisches Splitter-Window erzeugt wurde, hält das Framework die
notwendige Logik bereit. Wenn das Fenster ursprünglich nicht aufgeteilt war, der
Benutzer einen Split-Bar nimmt und mit der Maus in die Mitte des Fensters zieht,
splittet das Framework das Fenster vertikal und erzeugt eine neue Ansicht, die die
Daten im neuen Pane darstellt. Da dies alles zur Laufzeit passiert, muss die Ansicht
natürlich auch zur Laufzeit erzeugbar sein! Zieht der Benutzer den vertikalen SplitterBar auf die linke oder rechte Ecke des Fensters, so dass der Split-Bereich das
Grenzmaß unterschreitet, zerstört das Framework den zweite Bereich.
Die CSplitterWnd-Klasse verfügt über einige sehr nützliche Methoden, um ein
Splitter-Window nach Informationen zu befragen. Neben anderen Dingen kann man
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 159
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
die Anzahl der Spalten und Zeilen die augenblicklich dargestellt werden abfragen, die
Höhe einer Zeile oder Breite einer Spalte erfragen oder sich den Pointer auf die
Ansicht in einer bestimmten Spalte und Zeile geben lassen. Möchte man z.B. ein
Teilen-Kommando in das „Fenster“-Menü einbauen, so genügt es dem Menüpunkt
die ID ID_WINDOW_SPLIT zu verpassen. Die ID ist vorverdrahtet mit der Funktion
CView:::OnCommandSplit und dem Update-Handler CView::OnUpdateSplit,
um einen Tracking-Prozess auszuführen, bei dem Phantom-Splitter-Bar mit den PfeilAuf und Pfeil-Ab-Tasten verschoben und mit der Return-Taste die neue Position des
Splitter-Bars festgelegt werden kann. Dies ist in Abbildung 66 dargestellt.
1.
2.
vertikaler und horizontaler
Phantom-Splitter-Bar
Abbildung 66: Phantom-Splitter-Bar
7.2.3 Das Linken der Views
Fügt man ein Splitter-Window zu einer SDI-Applikation hinzu, trägt man als
Programmierer dafür Sorge, dass bei Eingaben in einem View die anderen Views
upgedated werden. Das Framework bietet die notwendigen Mechanismen in der
Gestalt von CDocument::UpdateAllViews und CView::OnUpdate. Es ist also
an der Zeit, diese Funktionen eingehender zu betrachten.
Angenommen die Paint-Anwendung gewähre dem Benutzer 4 Ansichten durch
dynamische Splitter-Windows auf das Dokument. Falls eine Änderung in einer der
Ansichten Auswirkungen auf die Bilder in den anderen Ansichten hat, sollten alle
Ansichten upgedatet werden, um die Änderungen zu übernehmen. Hierzu dient
CDocument::UpdateAllViews. Ein Dokumentenobjekt enthält eine Liste aller
Ansichten, die mit ihm verknüpft sind, eingeschlossen die Ansichten, die in den
Panes eines Splitter-Window dargestellt werden. Wenn die Daten des DokumentenObjektes in einer Anwendung mit mehreren Ansichten modifiziert wurden, sollte das
Objekt, das die Änderungen vornahm, meistens eine Ansicht oder das DokumentenObjekt selbst, UpdateAllViews aufrufen, um die Ansichten auf den aktuellen Stand
zu bringen. UpdateAllViews iteriert durch die Liste der Ansichten und ruft die
virtuelle OnUpdate-Funktion jeder Ansicht auf. UpdateAllViews hält also alle
Ansichten eines Dokuments synchron.
Seite 160
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
CView bietet eine triviale Implementation von OnUpdate die eine Ansicht für ungültig
erklärt und damit einen Aufruf von OnDraw erzwingt. Dies ist in jedem Fall ein
ineffizienter Weg um eine Ansicht upzudaten, da gezwungenermaßen die gesamte
Ansicht neu gezeichnet werden muss. Häufig muss jedoch nur ein kleiner Teil der
Ansicht neu gezeichnet werden, so dass UpdateAllViews und OnUpdate
„Hinweis“-Parameter zur Optimierung des Update-Vorgangs bieten. Der Prototyp von
UpdateAllViews sieht wie folgt aus:
void UpdateAllViews(CView* pSender,
LPARAM lHint = 0L,
CObject* pHint = NULL)
Ähnlich sieht der der Prototyp von OnUpdate aus:
void OnUpdate(CView* pSender,
LPARAM lHint = 0L,
VObject* pHint = NULL)
Der erste Parameter, pSender, verweist auf den View, der das Dokument upgedatet
hat. Falls pSender nicht-NULL ist, iteriert UpdateAllViews durch alle Ansichten
außer der hier angegebenen. Ist dieser Parameter NULL, so werden alle Ansichten
des Dokuments upgedatet. Für gewöhnlich wird man den View, der die Änderung am
Dokument
vornahm,
selbständig
updaten
lassen,
und
anschließend
UpdateAllViews mit pSener = this aus dieser Ansicht aufrufen. Genau so gut
könnte man aber die Ansicht direkt UpdateAllViews mit pSender = NULL
aufrufen lassen und darauf warten, dass daraufhin OnUpdate dieser Ansicht mit
aufgerufen wird. Dahingegen muss UpdateAllViews immer mit pSender = NULL
aufgerufen werden, wenn die Änderungen an den Daten des Dokuments durch das
Dokument-Objekt selbst vorgenommen wurden. Damit wird sichergestellt, dass alle
Ansichten auf den neuesten Stand gebracht werden.
Die Parameter lHint und pHint enthalten hint-Infos die von UpdateAllViews an
OnUpdate weitergeleitet werden. Eine simple Verwendung für hint-info ist die
Übergabe der Adresse einer RECT-Struktur oder eines CRect-Objektes in lHint,
um anzugeben welcher Teil der Ansicht neu gezeichnet werden muss. OnUpdate
kann diese Information verwenden, um seine Zeichenarbeit zu minimieren. Falls die
Daten des Dokuments aus einem CObArray bestehen, und UpdateAllViews
wurde aufgerufen weil ein neues CObject zum Dokument hinzugefügt wurde, kann
pHint benutzt werden um die Adresse des neuen CObject-Objekt zu übergeben.
Die Standardimpementation von OnInitialUpdate im Framework ruft OnUpdate
mit lHint = 0 und pHint = NULL auf. Dies ist der Fall, wenn das Dokument
erzeugt oder geladen wird. Wenn OnUpdate aufgerufen wird und lHint und pHint
enthalten unerwartete Werte, sollte man den Aufruf an die Basisklasse weiterleiten.
Das Framework ist dann für das Neuzeichnen der Ansicht verantwortlich.
UpdateAllViews und OnUpdate eignen sich nicht nur für Splitter-Windows; sie
sind nützlich für alle Anwendungen, die mehrere Ansichten auf ein Dokument
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 161
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
unterstützen, eingeschlossen solche MDI-Applikationen die mehrere Ansichten auf
ein Dokument in einzelnene MDI-Fenstern enthalten. Das Beispielprogramm Paint
wird nun so ausgebaut, dass das Zusammenspiel von UpdateAllViews und
OnUpdate in einer SDI-Anwendung mit Splitter-Windows gut nachvollzogen werden
kann. Eine so abgewandelte Ansichten-Klasse arbeitet auch hervorragend in MDIAnwendungen.
7.2.4 Splitted Paint
Paint2 ist identisch mit Paint1 bis auf ein paar Zeilen Code. Ein privates
CSplitterWnd Memberdatum mit dem Namen m_WndSplitter wurde zur
MainFrame-Klasse hinzugefügt, und die Funktion CFrameWnd::OnCreateClient
so
überschrieben,
dass
sie
ein
dynamisches
Splitter-Window
mit
CSplitterWnd::Create erzeugt. Die einzige weitere Änderung ist in der Klasse
CPaintView zu finden. Sie bietet ein OnUpdate, um alle Views bei Aufruf von
UpdateAllViews zu erneuern. Zusätzlich ruft der OnLButtonUp-Handler der
Klasse CPaintView UpdateAllViews auf, wenn eine Linie zum Dokument
hinzugefügt wurde, so dass auch alle anderen Ansichten upgedatet werden. Der
Kern des OnLButtonUp-Code sah vorher folgendermaßen aus:
CLine* pLine = GetDocument()->AdLine(m_ptFrom, point);
if (pLine != NULL)
pLine->Draw(&dc)
Jetzt wird folgendes ausgeführt:
CLine* pLine = GetDocument()->AdLine(m_ptFrom, point);
if (pLine != NULL)
{
pLine->Draw(&dc)
GetDocument()->UpdateAllViews( this, 0, pLine)
}
Nach dem Hinzufügen einer Linie mit der AddLine-Funktion und dem Zeichnen der
Linie in der aktuellen Ansicht wird UpdateAllViews durch den Dokument-Zeiger
mit pSender = this aufgerufen. Dies bedeutet, dass alle anderen AnsichtsObjekte außer dem, das UpdateAllViews aufgerufen hat, neu gezeichnet werden.
Durch die Übergabe der neu hinzugefügten Linie ist es in OnUpdate ein Leichtes,
die Ansicht auf den aktuellsten Stand zu bringen:
void CPaint::OnUpdate (CView* pSender, LPARAM LHint,
CObject* pHint)
{
if pHint !=NULL)
{
CClientDC dc(this);
OnPrepareDC(&dc);
((Cline*) pHint)->Draw(&dc)
return;
}
CScrollView::OnUpdate(pSender, lHint, pHint);
}
Seite 162
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
Diese Vorgehensweise ist natürlich wesentlich effizienter, als das Neuzeichnen der
gesamten Ansicht. Hier wird nun in dem Fall, dass pHint gleich NULL ist, die
Basisklassen-Version von OnUpdate aufgerufen. Dies ist wichtig, da
CPaintView::OnInitialUpdate
die
Basisklassen-Version
von
OnInitialUpdate aufruft, ruft das Framework seinerseits OnUpdate mit pHint =
NULL auf. Durch die Behandlung von NULL und nicht-NULL-Werten wird die korrekte
Zusammenarbeit von OnUpdate mit dem Framework in allen Lebenslagen erreicht.
7.2.5 Statische Splitter-Windows
Statische Splitter-Windows werden ganz ähnlich gehandhabt wie dynamische
Splitter-Windows, bis auf dass ein zusätzlicher Schritt bei ihrer Erzeugung nötig ist.
Statische Splitter-Windows werden mit CSplitterWnd::CreateStatic erzeugt,
anstatt mit CSplitterWnd:Create. Außerdem ist es die Aufgabe des
Programmierers, die Ansichten nach der Rückkehr von CreateStatic in die
Fensterbereiche einzufügen. CSplitterWnd wurde hierfür mit der Funktion
CreateView versehen, die diese Aufgabe erfüllt. Die Prozedur für das Hinzufügen
statischer Splitter-Windows erfolgte in 3 Schritten:
1. Hinzufügen eines CSplitterWnd Datenmember in der Rahmenfenster-Klasse
2. Überschreiben der Rahmenfenster-Funktion OnCreateClient und Aufruf von
CSplitterWnd::CreateStatic für die Erzeugung eines statischen SplitterWindow.
3. Verwendung von CSplitterWnd::CreateView zur Erzeugung einer Ansicht in
jedem Bereich des Splitter-Window.
Einer der großen Vorteile bei statischen Splitter-Windows ist, dass der
Programmierer selbst die Kontrolle über die Art der Ansichten hat, die mit den Panes
des Splitter-Window verknüpft sind. Die folgende Funktion OnCreateClient
erzeugt ein statisches Splitter-Window mit einer Zeile und zwei Spalten und fügt ein
CTextView in den linken Bereich und ein CPictureView in den rechten Bereich
des Fensters ein:
BOOL CMyFrame::OnCreateClient (LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
if ( !m_wndSplitter.CreateStatic(this,1,2)
|| !m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CTextView),
CSize(128,0), pContext)
|| !m_wndSplitter.CreateWiew(0,1,RUNTIME_CLASS(CPictrureView)
CSize(0,0),pContext)
)
return FALSE;
return TRUE;
}
CreateStatic identifiziert das Eltern-Objekt und die Zahl der Zeilen und Spalten,
die das Splitter-Window enthält. CreateView wird einmal für jeden Pane
aufgerufen. Panes werden durch Zähler identifiziert, wobei zu beachten ist, dass das
links oben liegende Pane mit 0 ,0 identifiziert wird. Dementsprechend plaziert der
erste Aufruf von CreateView eine Ansicht vom Type CTextView in den linken
Bereich des Splitter-Window (Zeile 0, Spalte 0) und der zweite Aufruf eine Ansicht
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 163
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
vom Typ CPictureView in den rechten Bereich des Splitter-Windows (Zeile 0,
Spalte 1). Die Ansichten werden nicht von Hand instanziiert, sondern durch das
Framework. Hierfür reicht die Applikation CRunTimeClass-Zeiger an CreateView
anstatt Zeigern auf echte CView-Objekte. Wie bei den dynamischen SplitterWindows müssen die Views, die in statischen Splitter-Windows zum Einsatz
kommen, dynamisch erzeugbar sein, sonst können sie vom Framework nicht
verwendet werden. Das CSize-Objekt, das an CreateView übergeben wird
spezifiziert die Startgröße des Panes. Im obigen Fall wird der Bereich, in dem die
CTextView-Ansicht liegt 128 Pixel breit, und der Bereich in dem die
CPictureView-Ansicht liegt, belegt den Rest des Splitter-Window. Die Breite, die
für den rechten Pane und die Höhe der beiden Panes ist 0, da diese Werte vom
Framework ignoriert werden. Wenn ein Splitter-Window nur eine Zeile besitzt,
belegen die Panes natürlich die gesamte Höhe des Client-Bereichs im ElternFenster, unabhängig davon welche Werte in CSize angegeben sind. Ähnlich verhält
sich dies bei einem Splitter-Window das n Spalten besitzt. Der am weitesten rechts
liegende Bereich belegt den übrigen Platz zwischen der rechten Grenze des n-1-ten
Bereichs und der rechten Grenze des Clientbereichs des Eltern-Fensters.
7.2.6 Three-Way Splitter-Windows
Selbstverständlich kann man static-Splitter-Windows auch ineinander schachteln,
indem man in ein Splitter-Window in ein Pane eines anderen Splitter-Windows legt.
Die folgende OnCreate-Funktion erzeugt ein three-way static Splitter-Window, das
vertikal in zwei Bereiche unterteilt ist und dessen rechte Spalte in zwei horizontale
Bereiche unterteilt wird. Der Benutzer kann die relative Höhe der Bereiche durch die
Splitter-Bars einstellen, aber das grundsätzliche Layout ist festgelegt, da es sich
immer um statische Splitter-Windows handelt.
BOOL CMyFrame::OnCreateClient(LPCREATESTRUCT lpCreateStruct,
CCreateContext* pContext)
{
if ( !m_wndSplitter1.CreateStatic(this,1,2)
|| !m_wndSplitter1.CreateView(0,0,RUNTIME_CLASS(CTextView),
CSize(128,0), pContext)
|| !m_wndSplitter2.CreateStatic(&m_wndSplitter1,
2,1,WS_CHILD|WS_VISIBLE,
m_wndSplitter1.IdFromRowCol(0,1))
|| !m_wndSplitter2.CreateView(0,0,RUNTIME_CLASS(CPaintView),
CSize(0,128),pContext)
|| !m_wndSplitter2.CreateView(1,0,RUNTIME_CLASS(CPaintView),
CSize(0,0), pCntext)
)
return FALSE;
return TRUE;
}
Kurz zusammengefaßt geschieht folgendes innerhalb des if-Statements, das die
drei Bereiche erzeugt und initialisiert:
1. Das erste Splitter-Window (m_Splitter1) wird erzeugt durch das Framework
mittels CreateSplitter.
m_WndSplitter1 erhält eine Zeile und zwei
Spalten.
Seite 164
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
2. Ein CTextView wird hinzugefügt zum ersten (linken) Bereich des
m_wndSplitter1 mit CreateView.
3. Ein zweites Splitter-Window, m_wndSplitter2, wird im zweiten (rechten)
Bereich von m_wndSplitter1 erzeugt. Sein Eltern-Fenster ist nicht das
Rahmenfenster, sondern m_wndSplitter1 und es wird mit einer ID verknüpft,
die besagt, dass es in Zeile 0, Spalte 1 liegt. Die entsprechende ID für
m_wndSplitter2
erhält
man
aus
der
Funktion
CSplitterWnd::IdFromRowCol,
die
eine
simple
mathematische
Konvertierungsroutine benutzt, um einen Offset auf AFX_IDW_APNE_FIRST aus
der Zeile und Spalte zu berechnen.
4. Die CreateView-Funktion wird zweimal aufgerufen um je einen CPaintView in
die zwei m_wndSplitter2-Bereichen einzufügen.
Die Verwendung eines dynamischen Splitter-Window für m_wndSplitter2 würde
etwas mehr Arbeit mit sich bringen wegen den Voraussetzungen, von denen das
Framework ausgeht, wenn es Ansichten dynamisch erzeugt, um damit – vom
Benutzer interaktiv – erzeugte Bereiche zu füllen.
7.2.7 CSplitterWnd selbst gekocht (CSplitterWnd ableiten)
Die Klasse CSplitterWnd enthält mehrere virtuelle Funktionen, die man in
abgeleiteten Klassen überschreiben kann. Eine dieser Funktionen ist die schon
häufig zitierte CSplitterWnd::CreateView, die vom Framework aufgerufen wird,
um eine neue Ansicht zu erzeugen, wenn ein dynamisches Splitter-Window geteilt
wird. Dies kann man sich zu nutze machen und verschiedene Ansichten in den
verschiedenen Bereichen eines dynamischen Splitter-Window zu erzeugen. Man
leitet
CSplitterWnd ab,
überschreibt
CreateView und
ruft
dort
CSplitterWnd::CreateView mit einem CRuntimeClass-Zeiger auf die Ansicht
des gewählten Typs.
Die folgende CreateView-Funktion erzwingt ein CTextView im Bereich mit der
Zeilennummer 1 und Spaltennummer 0, unabhängig vom Typ der Ansicht in Zeile 0,
Spalte 0:
BOOL CDynaSplitterWnd::CreatView(
int row
int col,
CRuntimeClass* pViewClass,
SIZE sizeInit,
CCreateContext* pContext)
{
if ((row == 1) && (col == 0))
return CSplitterWnd::CreateView (row, col,
RUNTIME_CLASS(CTextView),
sizeInit, pContext);
return CSplitterWnd::CreateView(row, col, pViewClass,
sizeInit, pContext);
}
Diese Vorgehensweise ist allerdings nicht sonderlich elegant, da die Ansichts-Klasse
für die 1. Zeile und 0. Spalte fest codiert ist, was grundsätzlich zu vermeiden ist. Man
sollte besser eine generische (und wiederverwendbare) Klasse für dynamische
Splitter-Windows schreiben, die verschiedene Typen von Ansichtsklassen unterstützt
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 165
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
durch das Hinzufügen einer RegisterView-Funktion. Diese Funktion hat dann die
Aufgabe die durch CRuntimeClass-Zeiger identifizierte Ansichten mit den Spalten
und Zeilen zu korrelieren. Bevor CSplitterWnd::Create aufgerufen wird, könnte
das Splitter-Window initialisiert werden mit Informationen über den Typ der Ansicht,
die in einem Bereichen eingesetzt wird. CreateView könnte dann diese Information
verwenden um die entsprechenden Ansichten zu generieren.
7.2.8 Splitter-Windows in MDI-Anwendungen
Die bisherige Untersuchung von Splitter-Windows in SDI-Anwendungen kann 1:1 auf
MDI-Anwendungen übertragen werden. Der Unterschied besteht darin, dass die
Splitter-Windows nun Kind-Fenster der MDI-Fenster (CMDIChildWnd) sind, die je
eine Ansicht auf ein Dokument einrahmen, anstatt dass sie Kind-Fenster des toplevel Rahmenfensters sind. Alle anderen Aspekte sind exakt identisch wie sie bisher
für SDI-Anwendungen besprochen wurden.
7.3 Mehr Dokument
Im folgenden soll ein genauerer Blick auf das Multiple Document Interface (kurz MDI)
geworfen werden. Ganz zu Beginn wurde eine Anwendung im Win32-SDK Stil
gezeigt. Natürlich ließe sich eine SDI- oder MDI-Anwendung auch mit dem Win32API programmieren (wenn man nur genügend Geduld und Zeit mitbringt). Nachdem
nun die Vorteile bekannt sind, die das Framework MFC beim Bilden einer SDIAnwendung beschert, erstaunt es kaum, dass der Schritt von einer SDI-Anwendung
zu einer MDI-Anwendung sehr klein ist.
7.3.1 MFC – und das MDI-Interface
Der wichtigste Unterschied zwischen dem Single-Document-Interface und dem
Multiple-Document-Interface ist, dass in SDI-Anwendungen zu einem Zeitpunkt
immer nur ein Dokument geöffnet sein kann. MDI-Anwendungen erlauben im
Gegensatz dazu mehrere gleichzeitig geöffnete Dokumente. Um in einer SDIAnwendung ein neues Dokument bearbeiten zu können, muss zuerst das aktuelle
Dokument geschlossen werden. In einer MDI-Anwendung kann ein Benutzer so viele
Dokumente gleichzeitig öffnen wie er möchte. Die begrenzenden Subjekte sind der
Speicher des Computers und einige weitere Ressourcen. MDI-Anwendungen
unterstützen auch mehrere Dokument-Typen. So könnte eine All-In-One-Anwedung
Textverarbeitungs-, Tabellenkalkulations- und Diagramm-Dokumente unterstützen.
Ähnlich wie ihre SDI-Gegenstücke, speichern MDI Dokument/Ansicht-Anwendungen
ihre Daten in Dokument-Objekten vom Typ CDocument (oder einem davon
abgeleiteten Typ) und präsentieren ihre Ansichten in View-Objekten, die vom Typ
CView oder einem davon abgeleiteten Typ sind. Mit jedem geöffneten Dokument
können – theoretisch – beliebig viele Ansichten verknüpft sein. Das Framework
erzeugt die erste Ansicht, wenn das Dokument geöffnet wird, und ein simpler
Funktionsaufruf kann zusätzliche Ansichten erzeugen. Jede Ansicht wird in einem
eigenen MDI-Kindfenster dargestellt, das ein Ausschnitt aus dem top-level
Rahmenfenster belegt. Derselbe Mechanismus, der mehrere Ansichten in einer SDIAnwendung mit mehreren Splitter-Windows synchron hält, synchronisiert auch
mehrere Ansichten eines Dokuments in einer MDI-Anwendung. Die Erzeugung einer
MDI-Anwendung, die mehrere Typen von Dokumenten enthält, beschränkt sich auf
Seite 166
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
die Registrierung der einzelnen Dokumenten-Tempates für jeden Dokument-Typ
Wenn der Benutzer das „Datei-Neu“-Menü wählt um ein neues Dokument zu
erzeugen, zeigt das Framework eine Dialogbox mit einer Liste die Dokument-Typen
an, aus der der Benutzer einen auswählen kann.
Die Abbildung 67 zeigt eine
Dokument/Ansicht-Anwendung.
schematische
Repräsentation
einer
MDI
Applikations-Objekt
top-level MDI Rahmenfenster
MDI Client-Fenster
Kind-Fenster MDI-Rahmen
(Dokument-Rahmen)
Dokument-Objekt A
Dokument-Objekt B
Abbildung 67: Die MDI Dokument/Ansicht-Architektur
Das Hauptfenster der Applikation ist ein Rahmenfenster-Objekt einer Klasse die von
CMDIFrameWnd abgeleitet wurde. Der Client-Bereich dieses Rahmenfensters enthält
einen speziellen Bereich mit dem Namen „MDI-Client“-Fenster, der vom
Rahmenfenster erzeugt wird unter Verwendung der vordefinierten WNDCLASS mit
Namen „MDICLIENT“. Ansichts-Fenster, die von den MDI Kindfenstern (auch MDIRahmen) umrahmt werden, zeigen die Ansichten der offenen Dokumente. Sie sind
innerhalb des Arbeitsbereiches frei beweglich, der vom MDI-Client Fenster begrenzt
wird. Die MFC kapselt die Funktionalität der Dokument-Rahmenfenster in der Klasse
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 167
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
CMDIChildWnd. So wie eine Ansicht ein Kind des top-level Rahmenfensters in einer
SDI-Anwendung ist, ist das MDI-Clientfenster ein Kind des top-level Rahmenfensters
in einer MDI-Anwendung. Weiter unten in der Hierarchie sind DokumentRahmenfenster Kinder des MDI-Client-Fensters und Ansichten sind Kinder der
Dokument-Rahmen. Dokument-Objekte enthalten die Daten, die in den Ansichten
dargestellt werden.
Versteckt unter der Oberfläche sind dutzende von Details die das Framework für den
Programmierer übernimmt. MDI-Kindfenster z. B. können minimiert und maximiert
werden innerhalb des top-level MDI-Rahmens. Wenn ein Kind maximiert wird,
verschwindet seine Titelzeile und sein Dokument-Icon erscheint in der Menüleiste
des top-level Rahmenfenster gemeinsam mit den Knöpfen zum Minimieren,
Wiederherstellen und Schließen des maximierten Dokument-Rahmens. Wenn man
ein MDI-Rahmenfenster auf CMDIFrameWnd und CMDIChildWnd basieren lässt,
werden diese und andere Verhaltensweisen durch das Framework automatisch
implementiert. Das Framwork bietet außerdem Kommando- und Update-Handler für
die folgenden Punkte im „Fenster“-Menü einer MDI-Anwendung (vgl. Abbildung 68):
-
ein “neues Fenster“-Kommando, das eine neue Ansicht auf ein geöffnetes
Dokument generiert.
„Überlappend“ und „Nebeneinander“-Kommandos, die offene Dokument-Rahmen
organisieren.
Ein „Symbole anordnen“-Kommando, das minimierte Dokument-Rahmen
organisiert, indem es sie am unteren Rand des MDI-Clientfenster auflistet.
Abbildung 68: Fenstermenü einer MDI-Anwendung
Alles was man tun muss, um einen „Fenster“-Menüpunkt der MDI-Anwendung
hinzuzufügen ist das Einfügen des erforderlichen Ressourcen-Statements in die .rcDatei der Applikation und die Zuweisung der vordefinierten ID’s (s. Kapitel 7.1.11.2)
zu den Punkten im Menü. Dinge wie das Einfügen einer Liste der aktuell offenen
Dokumente im „Fenster“-Menü, so dass der Benutzer leicht zu einem Dokument
wechseln kann, dessen Rahmen von einem anderen Dokumentenrahmen verdeckt
wird, nimmt das Framework dem Programmierer dankbarer Weise ab.
Kurzum, das Framework managed nahezu jeden Aspekt des Benutzerinterfaces
einer MDI-Applikation. Dies erspart dem Programmierer die langweilige Arbeit, diese
Punkte selbst zu programmieren. Dies ist der Grund, weshalb die Unterschiede
zwischen SDI- und MDI-Anwendungsprogrammierung auf ein Minimum an relativ
unwichtigen Implementationsdetails schrumpfen.
Seite 168
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.3.2 Alternativen zu MDI
MDI stellt nun nicht das Ende der Fahnenstange dar, möchte man dem Benutzer die
Möglichkeit geben, mehr als ein Dokument in einer Instanz der Anwendung
gleichzeitig zu bearbeiten. „The Windows Interface Guidelines for Software Design“
stellt drei Alternativen zum MDI-Programmiermodell vor:
-
-
-
Workspace Model: Visual C++ ist ein gutes Beispiel hierfür. Zusammengehörige
Dokumente werden in einem Arbeitsbereich zusammengefaßt. Die einzelnen
Dokumente können in MDI-artigen Dokumentrahmen bearbeitet werden.
Workbook Model: Alle Ansichten belegen den gesamten Clientbereich des toplevel Rahmenfensters, und können über Tabs erreicht werden. Dies ist ähnlich
wie maximierte Dokumentrahmen in einer MDI-Anwendung. Der Benutzer kommt
über die Tabs von einer Ansicht in die nächste. Dies ist ähnlich wie bei den
sogenannten Property-Sheets in Visual C++.
Projekt Model: Im Gegensatz zum Workspace Modell werden die Dokumente in
SDI-artigen Rahmenfenstern bearbeitet. Die herausragenste Eigenschaft ist, dass
kein top-level Rahmenfenster mehr existiert. Die SDI-Dokumente fliegen frei über
den Desktop.
Leider unterstützt MFC noch keines der alternativen Benutzerinterfaces direkt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 169
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dokumente und Ansichten
7.3.3 Anhang zu Dokumente und Ansichten
Tabelle 22: Vordefinierte Kommando-ID‘s und Nachrichten-Handler
Command ID
Datei-Menü
ID_FILE_NEW
ID_FILE_OPEN
ID_FILE_SAVE
ID_FILE_SAVE_AS
ID_FILE_PAGE_SETUP
ID_FILE_PRINT_SETUP
ID_FILE_PRINT
ID_FILE_PRINT_PREVIEW
ID_FILE_SEND_MAIL
ID_FILE_MRU_FILE1
–
ID_FILE_MRU_FILE16
ID_APP_EXIT
Edit-Menü
ID_EDIT_CLEAR
ID_EDIT_CLEAR_ALL
ID_EDIT_CUT
ID_EDIT_COPY
ID_EDIT_PASTE
ID_EDIT_PASTE_LINK
ID_EDIT_PASTE_SPECIAL
ID_EDIT_FIND
ID_EDIT_REPLACE
ID_EDIT_UNDO
ID_EDIT_REDO
ID_EDIT_REPEAT
ID_EDIT_SELECT_ALL
Ansicht-Menü
ID_VIEW_TOOLBAR
ID_VIEW_STATUS_BAR
Fenster-Menü
ID_WINDOW_NEW
ID_WINDOW_ARRANGE
ID_WINDOW_CASCADE
ID_WINDOW_TILE_HORZ
ID_WINDOW_TILE_VERT
Menüeintrag
Default Handler
vorverdrahtet?
Neu
Öffnen
Speichern
Speichern unter
Seite Einrichten
Drucker Einrichten
Drucken
Druckvorschau
Senden an
N/A
CWinApp::OnFileNew
CWinApp::OnFileOpen
CDocument::OnFileSave
CDocument::OnFIleSaveAs
kein
CWinApp::OnFilePrintSetup
CView::OnFilePrint
CView::OnFilePrintPreview
CDocument::OnFileSendMail
CWinApp::OnOpenRecentFile
Nein
Nein
Ja
Ja
N/A
Nein
Nein
Nein
Exit
CWinApp::OnAppExit
Ja
Löschen
Alles Löschen
Ausschneiden
Kopieren
Einfügen
Verknüpfung einfügen
Inhalte Einfügen
Finden
Ersetzen
Zurück
Wiederholen
Wiederholung
Alles auswählen
kein
kein
kein
kein
kein
kein
kein
kein
kein
kein
kein
kein
kein
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
Toolbar
Status Bar
CFrameWnd::OnBarCheck
CFrameWnd::OnBarCheck
Ja
Ja
Neues Fenster
Alle anordnen
Überlappend
Untereinander
Nebeneinander
CMDIFrameWnd::OnMDIWindowNew
CMDIFrameWnd::OnMDIWindowCmd
CMDIFrameWnd::OnMDIWindowCmd
CMDIFrameWnd::OnMDIWindowCmd
CMDIFrameWnd::OnMDIWindowCmd
Ja
Seite 170
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
8 Dateien und Serialisierung
8.1 Das Dateisystem und MFC
Das Dateisystem ist ein Stützpfeiler fast jeden Betriebsystems. Windows bietet mit
der Win32-API eine Anzahl an „klassischer“ Funktionen für den Programmierer:
• Öffnen und Schließen von Dateien
• Lesen und Schreiben aus und in Dateien
• Ändern des Filepointers (seeking) auf eine bestimmte Position
• ...
Die MFC kapselt diese Funktionen in der Klasse CFile. Diskfiles werden als
abstrakte Objekte vom Typ CFile behandelt, auf denen Operationen über die
Memberfunktionen ausgeführt werden.
Das folgende Codefragment zeigt beispielhaft, wie ein CFile-Objekt verwendet
werden kann um einen Teil einer Datei in den Speicher einzulesen:
CFile file("MyFile.doc ", CFile::modeRead);
UINT nBytesRead = file.Read(pBuffer, 0x8000);
Diese Statements öffnen das File namens MyFile.doc und lesen etwa 32K des
Files in den Puffer, auf den der Zeiger pBuffer verweist. Der Parameter
CFile::modeRead ist ein Zugriffsmodifizierer und ist als enum-Konstante in Afx.h
definiert. Welche weiteren Zugriffsmodifizierer existieren und wie sie auf die
entsprechenden Datei-Zugriffsmodifikatoren der Win32-API gemappt werden kann
man der Beschreibung der Klasse CFile entnehmen.
Die Funktion CFile::Read liest die angegebene Zahl an Bytes aus dem File. Der
Rückgabewert gibt die Zahl der tatsächlich ausgelesenen Bytes an. Die Zahl der
tatsächlich gelesenen Bytes kann kleiner sein als die gewünschte Zahl der zu
lesenden Bytes, falls das Dateiende beim Lesen erreicht wird.
Da das CFile-Objekt file in obigem Beispiel auf dem Stack erzeugt wurde, wird es
beim Verlassen des Scope auch wieder zerstört. Der dabei durchlaufene Destruktor
der Klasse CFile schließt automatisch die geföffnete Datei. Alternativ kann man die
zugehörige Datei natürlich auch über die Methode CFile::Close schließen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 171
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
Das folgende Beispiel soll die Verwendung der Klasse CFile anschaulich aufzeigen:
char* pBuffer = new[0x1000];
CFile file (szFilename, CFile::modeReadWrite);
DWORD dwBytesRemaining = file.GetLength();
UINT nBytesRead;
DWORD Position;
while (dwBytesRemaining)
{
dwPosition = file.GetPosition();
nBytesRead = file.Read(pBuffer, 0x1000);
::CharLowerBuffer(pBuffer, nBytesRead);
file.Seek((LONG) dwPosition), CFile::begin);
file.Write(pBuffer, nBytesRead);
dwBytesRemaining -= nBytesread;
}
delete[] pBuffer;
Nachdem eine Puffer zur Aufnahme der gelesenen Zeichen mit new allokiert wurde,
wird die Datei für Schreiben und Lesen geöffnet. Zusätzlich wird
dwBytesRemaining mit der Dateigröße in Bytes initialisiert. Daten werden in
Blöcken zu je 4K gelesen und in Kleinbuchstaben durch die Funktion
::CharLowerBuffer konvertiert. Anschließend wird der geänderte Puffer wieder in
die Datei geschrieben, wobei der Dateizeiger auf die richtige Stelle vorgerückt wird,
so dass die zuvor gelesenen Zeichen überschrieben werden. Bei jedem
Schleifendurchlauf wird die Zahl der bereits verarbeiteten Bytes (=Zeichen) von der
Dateilänge
abgezogen.
Die
Schleife
wird
solange
wiederholt,
bis
dwBytesRemaining 0 erreicht hat. Dann wird der Zeiger pBuffer deallokiert
wodurch der Puffer-Speicher wieder freigegeben wird.
8.2 Ausnahmen im Dateigeschäft– CFileException
Oberflächlich betrachtet erscheint die Benutzung der Methoden der Klasse CFile
straight forward. Falls man schon einmal Dateieingabe- oder –augabe unter
Verwendung von Betriebsystemfunktionen programmiert hat, dann erscheinen die
Methoden von CFile bekannt, da die Methoden von CFile jeweils eine analoge
Betriebsystemfunktion haben. Die Klasse CFile vereinfacht die Dinge etwas, indem
sie die Datei im Konstruktor-Aufruf öffnet und automatisch im Destruktor schließt.
Aber man sollte noch mehr über die Klasse CFile wissen, bevor man sie „straight
away“ verwendet und die Dateieingabe und –ausgabe seiner Applikationen damit
aufbaut.
Traditionelle Dateeingabe- und Dateiausgabefunktionen geben spezielle Error-Codes
als Returnwerte zurück um anzuzeigen, ob eine Operation geklappt hat, oder im
Fehlerfalle was die Ursache des Fehlers war. Im Gegensatz reagiert CFile auf
einen Fehlerzustände durch das Werfen von Exceptions mit Zeiger auf
CFileException-Objekte. Das ist gegenüber den klassischen Funktionen insofern
ein Vorteil, als dass man nicht nach jeder Dateioperation den Returnwert prüfen und
redundanten Code zur Fehlerbehandlung schreiben muss. Andererseits kennt jeder
Exceptions-erfahrene C++ Programmierer die Mißstände, die den unerfahrenen
Programmierer erwarten, wenn Exceptions im unpassenden Moment geworfen
werden. Das typische Beispiel ist der nicht mehr ausgeführte Aufruf eines delete,
wenn das new zwar noch geklappt hat, der Rest innerhalb der try-Klammer jedoch
nicht. Durch die Exceptions wurde der „gedachte“ Programmablauf unterlaufen und
Seite 172
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
häßliche memory leaks sind die Folge. Bei der Verwendung von CFile muss immer
daran gedacht werden, dass jeder Aufruf einer Membermethode eine Exception
erzeugen kann. Auch der Konstruktor von CFile wirft Exceptions, wenn z.B. die zu
öffnende Datei nicht existiert oder aus anderen Gründen nicht darauf zugegriffen
werden kann.
Das werfen eine Exception kann im Konstruktor von CFile kann jedoch auch
umgangen werden, indem man mittels des Standardkonstruktors ein leeres CFileObjekt erzeugt du anschießend CFile::Open verwendet. CFile::Open selbst
wirft keine Exception. Statt dessen gibt sie einen klassischen Fehlerstatus zurück
(Nonzero = Datei erfolgreich geöffnet, 0 = Öffnen der Datei hat nicht geklappt) und
initialisiert optional (!) einen CFileException-Objekt mit Detailinformationen über
das Ergebnis der Operation. Die Exception wird jedoch nicht geworfen!
Der folgende Beispielcode zeigt eine „quick ’n‘dirty“-Methode ganz ohne Exception:
CFile file;
if(file.Open(...))
{
//Make something with the file;
}
else
//Open failed
{
MessageBox("Unable to open the file");
}
Eleganter geht es mit einem selbst erzeugten CFileException-Objekt:
CFile file;
CFileException e;
if (file.Open("MyFile", CFile::modeRead, &e))
{
//do something with file
}
else
//Open failed, why??
{
if(e.m_cause == CFileException::fileNotFound)
MessageBox("Konnte Datei nicht finden.");
esle if (e.m_cause == CFileException::tooManyOpenFiles)
MessageBox("Keine Dateihandles mehr verfügbar.");
else
MessageBox("Datei öffnen war fehlerhaft");
}
Das letzte else-Statement ist der „catch-call“ für die Fehlerfälle, die nicht speziell
getestet werden. Übrigens: Exceptions sind vorhersehbare Fehlerfälle. Manche
Fehlerfälle (z.B. Festplattencrash) sind nicht „vorhersehbar“.
Alle andere Funktionen bieten diese Methodik zur Vermeidung von Exceptions die
geworfen werden nicht. Zur Beruhigung sei aber angemerkt, dass nicht gefangene
Exceptions unter MFC kein Beinbruch sind, da die meisten Exceptions über einen
default-Exceptionhandler verfügen, der die Exception fängt. Dieser Handler gibt den
Fehlertext in einer Messagebox aus. Nicht gefangene Exceptions erscheinen erst
dann störend, wenn sie Speicherlecks verursachen oder wenn sie dazu führen, dass
normalerweise
reparable
Fehlerzustände
zu
irreparablen
werden.
Unglücklicherweise ist es so, dass sehr viel Code den Bach runtergeht, bis eine
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 173
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
Exception den Defaulthandler erreicht. Deshalb sollte bei der Verwendung von
CFile genügend Zeit in einen gut durchdachten catch-Block investiert werden.
Zudem sollte gerade bei Dateieingabe- und Dateiausgabeoperationen nicht an
erklärenden Messageboxen gespart werden, wenn es sich um die Daten des
Anwenders handelt.
Betrachten wir obiges Beispiel zur Konvertierung von Groß- in Kleinbuchstaben, so
könnte z.B. das Read eine Exception werfen. Und schon wären 1K Speicher bis zum
nächsten Programmstart unbrauchbar, da die Zeile
delete[] pBuffer
nicht aufgerufen wird. Dies ist eine äußerst zweifelhafte Methode den Benutzer zum
Kauf von mehr Speicher zu bewegen!
Das folgende Beispiel zeigt, wie man es besser macht:
char* pBuffer = new[0x1000];
try
{
CFile file (szFilename, CFile::modeReadWrite);
DWORD dwBytesRemaining = file.GetLength();
UINT nBytesRead;
DWORD Position;
while (dwBytesRemaining)
{
dwPosition = file.GetPosition();
nBytesRead = file.Read(pBuffer, 0x1000);
::CharLowerBuffer(pBuffer, nBytesRead);
file.Seek((LONG) dwPosition), CFile::begin);
file.Write(pBuffer, nBytesRead);
dwBytesRemaining -= nBytesread;
}
}
catch (CFileException* e)
{
if(e->m_cause == CFileException::fileNotFound)
MessageBox("Konnte Datei nicht finden.");
esle if (e->m_cause ==
CFileException::tooManyOpenFiles)
MessageBox("Keine Dateihandles mehr
verfügbar.");
else if (e->m_cause == CFileException::hardIO)
MessageBox("Hardware Fehler.")
else
MessageBox("Unbekannter Dateifehler");
e->Delete();
}
delete[] pBuffer;
Seite 174
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
8.3 Serialisierung
8.3.1 Was ist Serialisierung
„Serialisierung ist der Vorgang, bei dem sich Objekte selbst in ein Speicherobjekt
archivieren und daraus wiederherstellen.“ Das Speicherobjekt in diesem Sinne kann
eine Festplatte, eine logische Datei oder gar ein Datennetz sein. Wie der Name
schon ausdrückt muss dabei das „virtuelle“ und unförmige Objekt in irgendeiner Art
und Weise seriell gemacht werden, da Plattenspeicher und Netze die Eigenschaft
haben, immer nur ein Bit nach dem anderen zu speichern und zu übertragen. Die
Serialisierung muss also etwas ähnliches bewerkstelligen, wie die StreamOperatoren (die selbst nur spezielle Funktionen darstellen). Das Objekt muss Stück
für Stück „weggeschrieben“ werden. Eine Festplatte kann mit einem „Zeiger auf ein
Objekt der Klasse Student“ wenig anfangen. Für die Festplatte ist der Zeiger eine
Folge von 32 Einsen und Nullen, das Objekt selbst ebenfalls eine Folge von Einsen
und Nullen. Die Interpretation der Nullen und Einsen ist dem jeweiligen Programm
vorbehalten. Genau diese Interpretation muss der Serialisierunsgvorgang leisten.
Student pcoStudent1
Student1:Class Student
char* m_pszName;
int m_nSemester
struct Address* m_pcoAddress
Serialisierung
Hans Meier\0\n27\nHinter dem Mond
5\0\n12345\nMondhausen\0
Abbildung 69 Serialisierung am Beispiel Student
Die Abbildung 69 zeigt stark vereinfacht am viel zitierten Beispiel der Klasse Student
was Serialisierung bedeutet. Die Klasse Student enthält einen char-Zeiger, der
vermutlich im Konstruktor auf einen Speicherbereich initialisiert wird, der per new
vom Heap organisiert wird. Im Destruktor sollte das zugehörige delete[ ] nicht
vergessen werden, sonst wundert sich der Benutzer irgendwann über schwindenden
Speicher auf seinem Rechner. Das selbe gilt für den struct Address, der selbst
wiederum in diesem Beispiel aus drei char-Array’s besteht: die Straße, die
Postleitzahl und der Ort. Dieses reichlich künstliche Gebilde aus verzettelten
Speicherbereichen, das nebenbei gesagt nur in unseren Gedanken eine Einheit
bildet, soll nun irgendwie per Serialisierung in ein Archivobjekt gesteckt werden (z.B.
per tcp über ein LAN-Netzwerk übertragen werden). Dazu sollen die Daten
hintereinander übertragen werden. Den Zeiger wird man sicherlich nicht übertragen,
da er beim Empfänger sinnlos ist. Beim Sender werden unser Studentenobjekt
sicherlich anders im Speicher liegen, wie beim Empfänger. Dann wäre der Zeiger,
der nur aussagt, um was für ein Objekt-Typ es sich handelt und wo die relative
Startadresse im Speicher ist, völlig bedeutungslos.
Wir übertragen also die Zeichenfolge, die der Abbildung gezeigt ist. Dabei wissen
Sender und Empfänger, wie die Zeichen zu interpretieren sind, also dass z.B. die
einzelnen Zeichenfolgen mit dem Zeichen ‘\0‘ beendet werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 175
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
8.3.2 MFC und Serialisierung
Nachdem der Vorgang der Serialisierung in gewisser Weise verwandt mit den
Stream-Operatoren sind, können viele grundlegende Vorgänge abgekupfert werden.
Vernünftige Klassenbibliotheken bieten die Serialisierung bereits standardmäßig an.
Die MFC bietet dem Programmierer ebenfalls Serialisierungsoptionen an.
Serialisierung im Sinne der MFC ist jedoch immer fest verknüpft mit dem persistenten
Speichern von Daten, z.B. dem schreiben oder lesen von Disk. Dabei wird in ein
CArchive-Objekt serialisiert, das seinerseits die Datenmember auf ein dauerhaftes
Speichermedium archiviert (z.B. eine Festplatte unter Verwendung eines CFileObjektes).
8.3.2.1 CObject – die Mutter aller Klassen
Ein Blick auf Klassenhierarchie der MFC verrät uns, dass Mehrheit der MFC-Klassen
entweder direkt oder indirekt von der Klasse CObject abgeleitet sind. In den
meisten Fällen kommt man beim Programmieren nicht direkt mit der Klasse
CObject in Kontakt, sondern man verwendet nur Ihre nützlichen Eigenschaften und
die Features die sie an die abgeleiteten Klassen weitergibt. Die drei Hauptdienste,
die CObject bietet sind:
• Serialisierungsunterstützung
• Unterstützung für Run-Time Class Information (Anmerkung: Die Ursprünge der
MFC sind aus den Tagen, da C++ keine Laufzeit-Informationen unterstützte,
deshalb existiert hierfür ein eigenes, gegenüber dem C++-Standard verändertes
Konzept.)
• Diagnose- und Debug-Unterstützung
8.3.2.2 CObject und die Serialisierung
CObject enthält zwei Memberfunktionen die eine Rolle bei der Serialisierung
spielen: IsSerializable und Serialize. Die IsSerializable-Funktion gibt
TRUE zurück falls ein Objekt Serialisierung unterstützt und FALSE wenn es
Serialisierung nicht unterstützt. Serialisierung ist bei den MFC also eine Option und
keine Bedingung. Serialize serialisiert die Datenmember auf ein Speichermedium,
das seinerseits wiederum durch ein CArchive-Objekt repräsentiert wird. Möchte
man eine Klasse bilden, die serialisierbar ist, leitet man diese Klasse von CObject
direkt oder indirekt ab. Um in die neue Klasse Serialisierung einzubauen,
überschreibt man die Funktion Serialize, die von CObject gererbt wird mit einer
eigenen Version, die die klassenspezifischen Daten (die Attribute) berücksichtigt.
MFC bietet dabei natürlich einen überladenen Einfüge- und Extraktionsoperator für
allgemeine Datentypen, die das Schreiben von Serialisieungsfunktionen einfach
gestalten.
Seite 176
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
Als Beispiel wollen wir die Klasse CBirthday betrachten:
void CBirthday::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if (ar.IsStoring)
ar << m_day << m_month << m_year;
else
ar >> m_day >> m_month >> m_year;
}
CBirthday::Serialize ruft als erstes die Version der Funktion aus der BasisKlasse auf, so dass die Basisklasse ihre eigenen Datenmember archivieren kann.
Als nächstes ruft Serialize die Funktion IsStoring der Klasse CArchive auf.
IsStoring findet heraus ob die Daten in das CArchive-Objekt geschrieben oder
daraus ausgelesen werden sollen. Entsprechend dem Ergebnis von IsStoring
werden die Operatoren << oder >> zur Archivierung der Klassenmember verwendet.
Hinter dem CArchive-Objekt kann z.B. ein CFile-Objekt stecken. Verwendet man
den Ansatz der Dokument/Ansicht-Applikation, erzeugt das Framework sogar das
CArchive-Objekt mit dem zugehörigen CFile-Objekt. Der Vorgang der
Serialisierung ersetzt somit umständliche und fehlerträchtige Funktionen zum
Speichern der Daten der Applikation auf eine äußerst elegante Art und Weise.
8.3.3 Eine Applikation serialisiert Ihre Daten
Die im vorangegangenen Kapitel beschriebene Klasse CFile ist zwar schon eine
großer Schritt, um innerhalb einer Applikation die Daten persistent zu speichern.
Noch einfacher gestaltet sich das Speichern von Objekten unter Verwendung der
Serialisierung. Eine große Rolle spielt die Serialisierung bei den Applikationen, die
mit Unterstützung der Dokument/Ansicht-Architektur des MFC-Frameworks erstellt
wurden.
Da die Serialisierung gerade eben im Zusammenhang mit dem MFC-Framework
auftritt, soll im folgenden Schritt für Schritt gezeigt werden, wie eine MFC-Klasse
erzeugt wird, deren Objekte serialisierbar sind.
1. Die Klasse muss von CObject abgeleitet werden, so dass sie die Unterstützung
von Serialisierung, dynamische Objekterzeugung und Laufzeit-Typinformation
von CObject erbt.
2. In der Klassendeklaration muss das MFC-Macro DECLARE_SERIAL aufgerufen
werden. DECLARE_SERIAL erwartet einen Übergabeparameter: Den Namen der
Klasse, für die das Macro aufgerufen wird.
3. Die Serialize-Funktion der Basisklasse muss überschrieben werden, damit die
Memberattribute der Klasse serialisiert werden.
4. Falls die abgeleitete Klasse keinen Standardkonstruktor besitzt (= Konstruktor
ohne Parameter), muss ein Standardkonstruktor hinzugefügt werden. Dieser
Schritt ist notwendig, da das MFC-Framework ein Objekt über den
Standardkonstruktor „on the fly“ erstellt, und seine Memberattribute per
Serialisierung von Disk initialisiert.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 177
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
5. In der Klassenimplementation muss das MFC-Macro IMPLEMENT_SERIAL
aufgerufen werden. Das Macro erwartet drei Parameter: Den Namen der Klasse,
den Namen der Basisklasse und eine Schemanummer. Die Schemanummer ist
eine Integerzahl, die in einer serialisierbaren Klasse als Versions-Kenner dient.
Wenn das MFC-Framework ein Objekt von Disk in den Speicher liest, wirft es
eine Exception vom Typ CFileException, falls der Versionskenner des Objekts
auf Disk nicht mit dem im Speicher übereinstimmt. Diese Schemanummer muss
jedesmal (von Hand) inkrementiert werden, wenn sich das zu speichernde Objekt
ändert. Typisches Beipsiel hierfür ist die Versionsnummer von WordDokumenten, die mit Microsoft Word 97 die Zahl 8 tragen.
Angenommen, die Klasse CDate wäre bereits geschrieben worden und man wollte
diese Klasse serialisierbar machen. Die urspüngliche Klasse CDate habe folgendes
aussehen:
class CDate
{
private:
int m_day, m_month, m_year;
public:
CDate(int day, int month, int year)
int GetDay();
int GetMonth();
int GetYear();
void SetDay(int day);
void SetMonth(int month);
void SetYear(int year);
};
Es sollte nun ein Einfaches sein, diese Klasse in eine serialisierbare Klasse
umzuschreiben:
class CDate : public CObject
{
DECLARE_SERIAL(CDate)
private:
int m_day, m_month, m_year;
public:
CDate() {}
CDate(int day, int month, int year)
void Serialze(CArchive& ar);
int GetDay();
int GetMonth();
int GetYear();
void SetDay(int day);
void SetMonth(int month);
void SetYear(int year);
};
Seite 178
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
Die Implementation
folgendermaßen aus:
der
überschriebenen
Version
von
Serialize
sieht
void Serialize(CArchive &ar)
{
CObject::Serialize (ar);
if(ar.IsStoring())
ar << m_day << m_month << m_year;
else
ar >> m_day >> m_month >> m_year;
}
Zusätzlich muss noch irgendwo in der Implementation von CDate das folgende
Statement eingefügt werden:
IMPLEMENT_SERIAL(CDate, CObject, 1)
Die Versionsnummer dieser Klasse ist 1. Nach einiger Zeit wird es vielleicht
notwendig, CDate um das Kürzel A.D. oder B.C. zu erweitern. Die neue Version von
CDate erhält dann die Version 2!
Die Funktion CDate::Serialize ruft wieder zuerst die Basisklassen-Version von
CObject::Serialize auf, damit die Datenmember der Basisklasse serialisiert
werden. Im vorliegenden Falle führt CObject::Serialize nichts aus, aber falls
CDate indirekt von CObject abgeleitet wäre, könnte es durchaus sein, dass die
Elternklasse ihre Datenmember serialisieren möchte. Deshalb sollte man sich
angewöhnen grundsätzlich die Basisklassen-Version von Serialize um
sicherzustellen, dass alle Komponenten eines Objekts archiviert werden.
Wie
die
Richtung
des
Datenflusses
mittels
CArchive::IsStoring
herausgefunden werden kann ist bereits bekannt. Die MFC-Klasse CArchive
abstrahiert das Interface der Klasse CFile und puffert den Eingabe- und
Ausgabefluß um die Perfomance des Lese-/Schreibvorgangs zu verbessern.
CArchive verfügt ebenfalls über Überladene Ein- und Ausgabeoperatoren um mit
CObject-Zeigern und primitiven Datentypen wie int, BYTE, WORD, und DWORD
arbeiten zu können. Die Operatoren arbeiten auch mit CPoint und CRect und
andere MFC-Klassen, für die Ein- und Ausgabeoperatoren definiert wurden.
Nun da die Klasse CDate serialisiert wurde, stellt sich die Frage, wie ein solche
Objekt serialisiert wird? Innerhalb der Dokument/Ansicht.Architektur geschieht dies
fast automatisch. Der Vorgang wird im Kapitel 7 (Dokumente und Ansichten) näher
erläutert. Möchte man dies von Hand programmieren, hält man sich am besten an
die folgenden drei Schritte:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 179
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
1. Erzeugen eines Objekts vom Typ CFile und öffnen der datei, in die das zu
serialisierende Objekt geschrieben wird (oder umgekehrt aus der das zu
serialisierende Objekt gelesen wird). Falls das Objekt aus dem Archiv gelesen
werden soll wird die Datei mit dem Zugriffsmodifizierer CFile::modeRead
geöffnet.
Andernfalls
verwendet
man
CFile::modeCreate
und
CFile::modeWrite.
2. Erzeugen eines CArchive-Objektes. Dem Konstruktor übergibt man zwei
Parameter. Als erstes einen Zeiger auf das CFile-Objekt, das in Schritt 1
initialisiert wurde. Der zweite Parameter ist ein Flag: CArchive::load, falls das
Objekt gelesen werden soll, CArchive::store falls das Objekt in das Archiv
geschrieben werden soll.
3. Dann kann die Serialize-Funktion des Objektes mit dem CArchive-Objekt als
Parameter aufgerufen werden.
Es ist wirklich so einfach wie es klingt. Der folgende Programmauszug öffnet
MyFile.doc und speichert das Datum des Autors. Anschließend wird das Datum wieer
gelesen.
CFile file;
CDate date(27,1,1971);
if (file.Open("MyFile.doc",
CFile::modeCreate|CFile::modeWrite))
{
CArchive ar(&file, CArchive::store);
date.Serialize(&ar);
}
try
{
file.Close();
}
catch (CFileException* e)
{
AfxMessageBox("Schließen von MyFiel.doc schlug fehl");
delete e;
}
//testweise das Datum auf 0.0.0 setzen
date.SetYear(0);
date.SetMonth(0);
date.SetDay(0);
if (file.Open("MyFile.doc", CFile::modeRead))
{
CArchive ar(&file, CArchive::store);
date.Serialize(&ar);
}
printf("Der autor wurde am %n %n %n geboren", date.GetDay(), date.GetMonth(),
date.GetYear())
Seite 180
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dateien und Serialisierung
Leider hat die Einfachheit wie üblich auch ihre Schattenseiten. In diesem Fall ist es
das Überladen der Operatoren << und >>. Das folgende Statement wird nicht
funktionieren:
ar << date;
Sicherlich könnte man statt des Objektes selbst auch einen Zeiger auf ein CObjektObjekt (oder ein davon abgeleitetes Objekt) übergeben. Für Zeiger auf CObjekt sind
<< und >> überschrieben. Damit würde aber die Versionierung außer Kraft gesetzt,
denn wir übergeben letzten Endes nur den CObjekt-Teil des CDate-Objekts. Dieser
Teil kennt zwar (aufgrund der Regeln für das überschreiben von nicht virtuellen
Funktionen) zufälligerweise die Funktion CDate::Serialize, aber nicht die
Versionsnummer, die ja im Makro-Aufruf IMPLEMENT_SERIAL(...) steckt.
Die Serialisierung im Sinne der persistenten Datenspeicherung und des wieder
Hehrstellens von persistenten Daten wird noch wesentlich komfortabler, wenn man
eine Dokument/Ansicht-Applikation mit Unterstützung durch das Framework erstellt.
Die Serialize-Funktion wird in der Dokument-Klasse vom Programmierer
implementiert, die Bereitstellung eines CFile- und CArchive-Objekte übernimmt
das Framework. Das Framework übernimmt auch die Darstellung des „Datei öffnen“oder des „Datei speichern (unter ...)“-Dialog um den Dateinamen vom Benutzer zu
erhalten
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 181
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
9 Multithreading
9.1 Keine Angst vor Threads!
9.1.1 Was sind Threads?
Begriffe wie „Threads“ oder „Multithreading“ sind in Zusammenhang mit
Betriebssystemen neueren Datums häufig zu hören. Worum handelt es sich dabei?
Threads sind alternative, (quasi-)parallel ablaufende Teile des Maschinencodes
eines Prozesses. Man bezeichnet sie deshalb auch oft als „Pseudoprozesse“,
„Prozeß im Prozeß“ oder „leichtgewichtige Prozesse“. Wörtlich übersetzt bedeutet
Thread etwa soviel wie „roter Faden“ oder „Ablauf“. Threads sind also ganz und gar
nichts bedrohliches (threat = engl. : Bedrohung), sondern ein äußerst nützliches und
mächtiges Feature moderner Betriebssysteme.
Threads werden im allgemeinen von einem prozeßinternen Threadscheduler
verwaltet und durchlaufen ähnlich wie Prozesse bestimmte definierte Zustände:
running
zerstören
zerstören
zerstören
ready
anhalten
erzeugen
dead
anhalten
Warteaufruf
Ereignis aufgetreten
zerstören
blocked
fortsetzten
suspended
anhalten
zerstören
Abbildung 70: Zustandsdiagramm eines Threads
Seite 182
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Der Threadscheduler arbeitet preemptiv, d.h. er ist in der Lage, zu jeder Zeit einem
Thread, der gerade ausgeführt wird (Zustand running), die CPU zu entziehen und
einem anderen Thread, der sich im Zustand ready befindet, die CPU zu überlassen.
Selbstverständlich werden dabei alle Register gesichert, damit der Thread, wenn er
das nächste Mal an die Reihe kommt, alles wieder so vorfindet, wie es war. Diesen
Vorgang nennt man Kontextwechsel. Ein sofortiger Kontextwechsel wird erzwungen,
wenn ein Thread während seiner Ausführung in den Zustand suspended, blocked
oder dead wechselt.
9.1.2 Besonderheiten des Multithreading unter Win32
Jeder Prozeß, der auf einer Win32-Plattform abläuft, hat mindestens einen Thread:
den Hauptthread mit der Nummer Null. Win32 unterscheidet eigentlich gar zwischen
einem Thread- und einem Prozeßscheduler, denn unter Win32 ist nicht der Prozeß
die kleinste Einheit, die Maschinencode ausführen kann, sondern der Thread. Ein
Prozeß zeichnet sich lediglich durch seinen abgegrenzten Adreßraum aus, in dem
der Maschinencode des Hauptthreads und evtl. weiterer Threads abgebildet sind,
und innerhalb dessen sie Ihre Daten verwalten.
Somit ist es auch möglich, zwei Threads eines Prozesses auf zwei (oder mehr)
verschiedenen CPUs (echt-)parallel ablaufen zu lassen, vorausgesetzt beide CPUs
„sehen“ den selben Adreßraum. Windows NT Workstation unterstützt standardmäßig
2 CPUs, die Server-Variante von Windows NT sogar 4 CPUs (Enterprise Edition: 8
CPUs). Für alle ist gegen Aufpreis ein Upgrade auf bis zu 32 CPUs erhältlich.
Es gibt keine „Vater-Sohn“-Beziehung zwischen Threads, d.h. anders als die mit
fork() erzeugten Sohnprozesse unter Unix, endet ein Thread nicht automatisch,
wenn der Thread endet, der ihn erzeugt hat. Lediglich wenn die ganze Applikation
(und damit der Hauptthread Nr. 0) beendet wird, beendet das Betriebssystem alle
anderen Threads zwangsweise.
Das Scheduling der Threads bei Win32 ist prioritätsgesteuert, d.h. wenn sich
mehrere Threads gleichzeitig im Zustand ready befinden, kommt derjenige zur
Ausführung, der die höchste Priorität hat.
9.1.3 Zusammenfassung
• Unter Win32 besitzt jeder Prozeß mindestens einen Thread: den Hauptthread.
• Der Hauptthread kann weitere Threads erzeugen, die ihrerseits wieder Threads
erzeugen können, usw.
• Es gibt keine „Vater-Sohn“-Beziehung zwischen Threads.
• Threads laufen parallel ab.
• Alle Threads teilen sich einen gemeinsamen virtuellen Adreßraum.
• Threads eines Prozesses können auf mehrere CPUs verteilt werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 183
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
9.2 Wozu Threads ?
Den meisten Lesern dürfte folgendes Szenario aus der Zeit der alten 16-Bit
Windows-Systeme in leidvoller Erinnerung sein: Immer wenn man ein Programm
(wissentlich oder nicht) veranlaßt hat, eine umfangreiche Berechnung durchzuführen
oder große Mengen an Daten zu transferieren, war das System so gut wie blockiert,
bis die Aufgabe abgeschlossen war. Gut programmierte Anwendungen zeigten
während der fraglichen Zeit einen kleinen Dialog mit einem „Abbrechen“-Knopf oder
sie reagierten auf das Drücken der ESC-Taste, aber selbst dann reagierte die
Anwendung in der Regel erst mit einigen Sekunden Verzögerung. Woher kam dieses
benutzerunfreundliche Verhalten? Ganz einfach: in der Zeit, in der die Applikation mit
der Bearbeitung der zeitintensiven Aufgabe beschäftigt ist, kann die
Nachrichtenschleife nicht durchlaufen werden, d.h. die Applikation holt keine
Nachrichten aus der Warteschlange und leitet Sie auch nicht durch den Aufruf von
DispatchMessage() an die entsprechenden Handlerfunktionen weiter. Unter
Win16 geschieht darüber hinaus noch was viel schlimmeres: Weil Win16 kein
preemptives Multitasking unterstützt, kann keiner der Prozesse seine Nachrichten
mehr abholen, was das ganze System (mit Ausnahme der interruptgesteuerten
Treiber)
zum Stehen bringt! Die gängige Implementation der bekannten
„Abbrechen“-Dialoge ruft einfach aus der Schleife heraus, in der die Berechnung
stattfindet, ab und zu GetMessage()/DispatchMessage() auf, und sorgt damit
für eine gewisse Aufrechterhaltung des Nachrichtenflusses.
Auf Unix-Plattformen umgeht man dieses Problem meist, indem man einen
Sohnprozeß erzeugt und diesem die zeitintensive Aufgabe überträgt. Die
Anwendung selbst kann dann sofort weiterarbeiten und auf Benutzereingaben
reagieren oder weitere Berechnungen auf ähnliche Art und Weise an Sohnprozesse
delegieren.
Unter Win32 ist diese Vorgehensweise prinzipiell genauso möglich, jedoch wird in
der Praxis meist ein Thread, statt einem Prozeß erzeugt.
Wann sollte man also Multithreading verwenden ?
• Immer, wenn eine Berechnung oder Aufgabe ansteht, die länger dauert, als Sie
ihrem Benutzer als Antwortzeit auf Benutzereingaben zumuten wollen.
• Immer, wenn eine Aufgabe kontinuierlich im Hintergrund durchgeführt werden
soll, wie z.B. die Überwachung eines Sensors oder einer Schnittstelle, der
automatische Zeilen- und Seitenumbruch bei einem Textprogramm usw.
• Immer, wenn ihre Anwendung statt auf den Rückgabewert einer Funktion zu
warten, zwischenzeitlich etwas sinnvolleres tun könnte.
• Für die Implementation von Diensten, die parallel mehrere Clients bedienen
können müssen (parallele Server)
Hüten Sie sich aber davor, vor lauter Begeisterung jede Methode einer Klasse in
einem eigenen Arbeitsthread zu stecken. Der Kontextwechsel zwischen zwei
Threads kostet zusätzliche Rechenzeit, d.h. die Zeit, die Ihre Berechnungen am
Ende insgesamt benötigt haben, ist größer, als wenn die selben Berechnungen
sequentiell von nur einem Thread ausgeführt würden. Außerdem ergeben sich durch
die Verwendung von mehreren Threads zusätzliche Schwierigkeiten, die in den
nachfolgenden Abschnitten genauer besprochen werden.
Seite 184
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Die Antwort auf die Frage, ob man Multithreading programmieren soll, oder nicht,
lautet also wie so oft: „Es kommt darauf an ...“. Überlegen Sie sich stets, ob sie von
der Parallelität wirklich so sehr profitieren, daß Sie den zusätzlichen Aufwand in Kauf
nehmen wollen.
9.3 Das erste MFC – Multithreading-Programm
9.3.1 Erzeugung von Threads
In der MFC wird ein Thread angelegt, indem man ein Objekt der Klasse
CWinThread oder einer davon abgeleiteten Klasse erzeugt. Am einfachsten
geschieht dies durch den Aufruf der Funktion AfxBeginThread(...), die ein
CWinThread-Objekt erzeugt, es richtig initialisiert und dann den CWinThread* Zeiger auf den neuen Thread zurückliefert.
AfxBeginThread(...) ist überladen: Die eine Version erzeugt einen sogenannten
Arbeitsthread
(worker
thread),
die
andere
Version
erzeugt
einen
Benutzeroberflächenthread (user interface thread). Der wichtigste Unterschied
zwischen den beiden Thread-Varianten ist, daß der worker thread im Gegensatz zum
user interface thread keine eigene Nachrichtenschleife verwaltet, also keine
Messages vom System oder einem anderen Fenster der selben Anwendung
empfangen kann.
9.3.2 Programmierung von Multithreading
Als Beispiel für die Verwendung eines worker threads soll im folgenden Schritt für
Schritt ein MFC-Programm entwickelt werden, das folgende Merkmale aufweisen
soll:
• Das Programm soll alle Primzahlen in einem gewissen Intervall bestimmen
• Der Benutzer gibt die obere und untere Grenze des Intervalls in CEdit-Felder ein
• Nachdem der Benutzer auf den „Start“-Knopf geklickt hat, soll die Berechnung der
Primzahlen beginnen. Wenn eine Primzahl gefunden wurde, soll diese in eine
ListBox ausgegeben werden (eine Zeile pro Zahl)
• Der Benutzer kann die Berechnung jederzeit durch klicken auf einen „Stop“-Knopf
beenden
• Das Programm soll der Einfachheit halber dialogfeldbasiert sein, d.h. sein
Hauptfenster ist ein Dialog
Wir benötigen also einerseits die Benutzeroberfläche, welche die Ein- und Ausgaben
realisiert, und andererseits den Primzahlengenerator, der alle Primzahlen in einem
gegebenen Intervall bestimmt. Die beiden Objekte interagieren miteinander, indem
die Oberfläche dem Primzahlengenarator mitteilt, welche Intervallgrenzen der
Benutzer eingegeben hat, und der Primzahlengenerator der Oberfläche jede
gefundene Primzahl zurückgibt.
Das Objektmodell unserer Anwendung sieht wie folgt aus:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 185
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
CObject
CCmdTarget
CWnd
CWinThread
CDialog
CWinApp
CMultithread1App
CMultithread1Dlg
CPrimeGenerator
<<Kommunikation>>
Abbildung 71: Objektmodel des Beispiels MULTITHREAD1.EXE
CMultithread1App ist die Hauptanwendungsklasse unseres Beispiels, sie ist wie
immer von CWinApp abgeleitet und hat von CWinThread die Fähigkeit zur
Abarbeitung von Programmcode, und von CCmdTarget die Fähigkeit zur
Nachrichtenverarbeitung geerbt.
CMultithread1Dlg ist die Dialogfeldklasse unserer Anwendung (und in diesem
Fall gleichzeitig das Hauptfenster). Ihre Eigenschaft als Dialog hat sie von CDialog
und die allgemeineren Eigenschaften eines Fensters von CWnd geerbt. Die
Hierarchie macht weiter deutlich, daß Dialoge (oder besser Fenster im Allgemeinen)
zwar die Fähigkeit haben, Nachrichten zu verarbeiten, aber nicht selbst zur
Ausführung von Code befähigt sind, denn sie sind nicht von CWinThread abgeleitet.
Damit also ein Dialogfeld (allgemein: ein Fenster) überhaupt etwas tun kann, muß es
in ein Objekt eingebettet werden, das Code ausführen kann, also in einem
Verwandten von CWinThread.
Wenn wir ein neues Projekt anlegen und dabei als Anwendungstyp „dialogbasiert“
angeben, erledigt der Anwendungsassistent alles bis zu diesem Punkt für uns.
Die dritte Klasse CPrimeGenerator kapselt die Berechnung der Primzahlen. Diese
Klasse hat keine Verwandten und muß mit Hilfe des Klassenassistenten neu erstellt
werden. Weil die Primzahlberechnung in einem parallel ablaufenden Thread
stattfinden soll, enthält CPrimeGenerator ein Exemplar der Klasse CWinThread.
Seite 186
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Wir erstellen also die Klasse CPrimeGenerator wie folgt:
CPrimeGenerator
public:
CPrimeGenerator(HWND hwndDialog);
// Konstruktor, der das
// Fensterhandle des Dialogs
// übergeben bekommt
void SetStartValue(long nStart);
// setzt den Anfangswert für
// die Berechnung
void SetEndValue(long nEnd);
// setzt den Endwert für die
// Berechung
void Start();
// startet die Berechnung
void Stop();
// stopt die Berechnung
private:
static UINT ThreadProc(LPVOID lpArg)
// die Threadfunktion des
// worker threads
BOOL IsPrime(long nToTest)
// liefert TRUE, wenn nToTest
// eine Primzahl ist
private:
long
m_nStart;
long
m_nEnd;
// Start- und Endwerte
HWND
m_hwndDialog;
// Handle auf das Dialogfeld,
// das die Ausgaben macht
CWinThread*
m_lpThread;
// Zeiger auf das aggregierte
// Threadobjekt
Abbildung 72: Klasse CPrimeGenerator
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 187
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
In der Funktion CMultithread1Dlg::OnInitDialog fügen wir folgende
Anweisung hinzu (fett gedruckt), um ein Objekt der gerade definierten Klasse
CPrimeGenerator zu erzeugen:
BOOL CMultithread1Dlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 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
// Eim Objekt der Klasse CPrimeGenerator erzeugen, dabei
// das Fensterhandle des Dialogs übergeben
m_pPrimeGen = new CPrimeGenerator(GetSafeHwnd());
return TRUE;
Fokus erhalten
}
// Geben Sie TRUE zurück, außer ein Steuerelement soll den
m_pPrimeGen ist eine Membervariable der Dialogklasse CMultithread1Dlg und
ist vom Typ CPrimeGenerator*.
Im Konstruktor der Klasse CPrimeGenerator erzeugen wir nun das aggregierte
Threadobjekt durch Aufruf der Funktion AfxBeginThread(...). Da die
Threadfunktion das umgebende Objekt kennen soll, übergaben wir über den 32-BitWert den this-Zeiger des CPrimeGenerator-Objekts.
CPrimeGenerator::CPrimeGenerator(HWND hwndDialog)
{
// Fensterhandle des Dialogfeldes in die Membervariable übernehmen
m_hwndDialog = hwndDialog;
// Thread erzeugen
m_lpThread = AfxBeginThread(ThreadProc,this);
// Übergabeparameter
}
// this als
Seite 188
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Info: die Funktion AfxBeginThread(...)
CWinThread*
AfxBeginThread(AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL );
AFX_THREADPROC pfnThreadProc
Funktionszeiger auf die Threadfunktion, welche folgenden Prototyp haben muß:
UINT IrgendEinName(LPVOID lpArg);
Wenn, wie in unserem Fall, die Threadfunktion im Innern einer Klasse verborgen
sein soll, muß sie natürlich static deklariert werden, damit der Compiler schon
bei der Übersetzung eine eindeutige Adresse vergeben kann.
LPVOID pParam
Ein void*, der an die Threadfunktion übergeben werden kann. Die Verwendung
dieses 32-Bit-Wertes ist dem Programmierer völlig freigestellt.
int nPriority
Ein optionaler Wert, um die Anfangspriorität des Threads festzulegen. Default ist
die normale Priorität.
UINT nStackSize
Ein optionaler Wert, um die Größe des Stacks des Threads festzulegen. Der
Defaultwert 0 bedeutet, daß der Stack des neuen Threads genauso groß sein
soll wie der Stack des Threads, der AfxBeginThread aufruft.
DWORD dwCreateFlags
Ein optionales Flag, daß entweder den Wert 0 oder den Wert
CREATE_SUSPENDED annehmen kann. Im Fall von 0 wird der Thread sofort nach
der Erzeugung „ready to run“, im zweiten Fall wird er im Zustand „suspended“
erzeugt, und muß manuell durch die Funktion ResumeThread in den Zustand
„ready to run“ versetzt werden.
LPSECURITY_ATTRIBUTES lpSecurityAttrs
Ein optionaler Zeiger auf eine SECURITY_ATTRIBUTES–Struktur, in der
Einstellungen über die Vererbbarkeit des Thread-Handles getroffen werden
können. Der Defaultwert 0 bedeutet, daß die selben Attribute wie beim
erzeugenden Thread verwendet werden sollen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 189
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Nun haben wir alle nötigen Objekte erstellt und
im Konstruktor des
CPrimeGenerator einen Arbeitsthread erzeugt, der sich im Zustand „ready to run“
befindet. Nun ist es an der Zeit, die Oberfläche zu entwerfen.
IDC_STARTVAL
IDC_ENDVAL
IDC_LB_PRIMZAHLEN
IDC_START
IDCANCEL
IDC_STOP
Abbildung 73: Erstellen der Benutzeroberfläche für MULTITHRED1.EXE
Das Bild zeigt einen Ausschnitt des Microsoft Resource-Editors. Die Beschriftungen
sind die ID’s, die an jedes Dialogelement vergeben wurden.
In der Dialogklasse CMultithread1Dlg ordnen wir mit Hilfe des
Klassenassistenten den Start- und Stopknöpfen je eine Membervariable vom Typ
CButton zu (m_StartButton bzw m_StopButton). Der ListBox ordnen wir eine
Variable m_LBPrimzahlen vom Typ CListBox zu und die beiden Eingabefelder
werden mit je zwei long-Variablen, m_nStartValue und m_nEndValue, verknüpft.
Seite 190
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
9.3.3 Inter-Thread-Kommunikation und Synchronisierung
Jetzt müssen wir uns der Frage widmen, wie der Dialog (und über ihn der Benutzer)
mit dem bereits laufenden Arbeitsthread kommuniziert. Dabei geht es vor allem um
folgende Punkte:
1. Wie liefert der Thread seine Ergebnisse nach außen ?
2. Wie erfährt der Thread von einer Benutzereingabe, z.B. davon, daß der Startoder der Stop-Knopf gedrückt wurde.
3. Was macht der Thread, wenn es gerade gar nichts für ihn zu tun gibt (bevor der
Startknopf gedrückt wurde bzw. nach Ende einer Berechnung) ?
4. Wie wird der Thread ordnungsgemäß beendet?
Die erste Frage ist am einfachsten zu beantworten: Da der primäre Thread (in
dessen Kontext unser Dialog läuft) ein user interface thread ist, kann er Nachrichten
empfangen und an das richtige Fenster zur Verarbeitung weiterleiten. Und da wir
unserem CPrimeGenerator-Objekt im Konstruktor das Fensterhandle des
Dialogfeldes übergeben haben, ist es möglich aus dem CPrimeGenerator-Objekt
heraus Nachrichten an den Dialog zu schicken. Der Thread widerum hat den thisZeiger auf das ihn umgebende CPrimeGenerator-Objekt und hat somit auch die
Möglichkeit über das Windows-Nachrichtensystem mit dem Dialog zu
kommunizieren.
In der anderen Richtung, vom Dialog zum Thread (Frage 2) ist die Sache etwas
komplizierter. Die Berechnung selbst wird sicher in irgend einer Art von Schleife
ablaufen, und die läßt sich von Außen beenden, in dem man das Abbruchkriterium
vorzeitig TRUE werden läßt, z.B. so:
m_nStart und m_nEnd sind Member vom CPrimeGenerator (s. oben). Sie
können von der Dialogklasse über die entsprechenden Set-Methoden gesetzt
werden. Wenn nun der Schleifenzähler auch ein Member von CPrimeGenerator
wäre, dann könnte die Stop()-Methode diesen Schleifenzähler auf einen Wert
größer oder gleich dem Endwert setzen und die Schleife in der Threadfunktion wird
beendet. Das funktioniert in der Praxis auch so ... meistens, jedenfalls.
void CPrimeGenerator::Start()
{
// Hier muß dem Thread irgendwie gesagt werden, daß er
// los laufen soll ...
}
void CPrimeGenerator::Stop()
{
// Hier wird der Zähler auf den Abbruchwert gesetzt
m_nCounter = m_nEnd + 1;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 191
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
// Threadfunktion -----------------------------------------------------UINT CPrimeGenerator::ThreadProc(void * lpArg)
{
// wenn wir auf Variablen außerhalb der static-Funktion zugreifen
// wollen, muß das über den this Zeiger gehen
CPrimeGenerator* lpOuterThis = (CPrimeGenerator*) lpArg;
BOOL bContinueLoop;
long nToTest;
// DenZähler auf den Anfangswert setzten
lpOuterThis->m_nCounter = lpOuterThis->m_nStart;
// Die Schleifenbedinung ist anfangs erfüllt
bContinueLoop = TRUE;
do // Beginn der Berechnungsschleife
{
// Prüfen, ob m_nCounter eine Primzahl ist,
// dazu wird m_nCounter in eine lokale Variable kopiert,
// die dann an IsPrime() übergeben wird
nToTest = lpOuterThis->m_nCounter;
// Wenn IsPrime() TRUE liefert, wird die Pimzahl mit
// PostMessage an den Dialog geschickt
if (lpOuterThis->IsPrime(nToTest))
PostMessage( lpOuterThis->m_hwndDialog,
STZ_PRIME_FOUND,
0,
nToTest);
// Hier wird geprüft, ob die Schleife weiterlaufen soll oder nicht
if (++(lpOuterThis->m_nCounter) > lpOuterThis->m_nEnd)
bContinueLoop = FALSE;
}while(bContinueLoop);
// Der Thread meldet den Abschluß der Berechnung an den Dialog
::PostMessage(lpOuterThis->m_hwndDialog,STZ_PRIME_FINISHED,0,0);
return 0;
}
BOOL CPrimeGenerator::IsPrime(long nToTest)
{
// Ein ganz einfacher Primzahlentest: einfach durchprobieren,
// ob es Teile gibt (Modulo ist dann == 0 )
for (long i = 2; i<=sqrt(nToTest); i++)
{
if ((nToTest % i) == 0)
return FALSE;
// Zwischendurch nachschauen, ob m_nCounter verändert wurde
if (nToTest != m_nCounter)
return FALSE;
}
// wenn die Funktion bis hierher durchläuft, ist nToTest eine Primzahl
return TRUE;
}
Seite 192
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
9.3.3.1 Probleme beim gemeinsamen Zugriff auf Systemressourcen
Wenn man das Programm wie oben gezeigt ausführt, wird man machmal
beobachten, daß die Schleife einfach weiterläuft, obwohl in der Stop()-Methode der
Zähler auf einen Wert größer als der Schleifenendwert gesetzt wurde.
Was geschieht in einem solchen Fall ?
Man kann das Problem leicht im disassemblierten Maschinencode des ifStatements erkennen (ohne Compiler – Optimierung):
108:
00401BBF
00401BC2
00401BC5
00401BC8
00401BCB
00401BCE
00401BD1
00401BD4
00401BD7
00401BDA
109:
00401BDC
mov
mov
add
mov
mov
mov
mov
mov
cmp
jle
mov
if (++(lpOuterThis->m_nCounter) > lpOuterThis->m_nEnd)
eax,dword ptr [lpOuterThis]
ecx,dword ptr [eax+34h]
ecx,1
edx,dword ptr [lpOuterThis]
dword ptr [edx+34h],ecx
eax,dword ptr [lpOuterThis]
ecx,dword ptr [lpOuterThis]
edx,dword ptr [eax+34h]
edx,dword ptr [ecx+40h]
CPrimeGenerator::ThreadProc(0x00401be3)+0DBh
bContinueLoop = FALSE;
dword ptr [bContinueLoop],0
Der Prozessor lädt zuerst den Wert von m_nCounter aus der Adresse
lpOuterThis+34h in das ecx – Register, addiert dann 1 und schreibt den Wert von
ecx dann zurück in die Speicherzelle lpOuterThis+34h.
Wenn nun der Thread-Scheduler die Ausführung nach dem add ecx,1 unterbricht,
und dann die Stop()-Methode zur Ausführung kommt, dann wurde der Wert von
m_nCounter zwar hochgesetzt, aber wenn die Ausführung wieder zu dem
Assembler-Abschnitt oben zurückkehrt (nach dem add), wird der Prozessor den
alten, nur um 1 erhöhten Wert in den Speicher zurückschreiben. Damit wird die
Bedingung auch beim nächsten Durchlauf nicht erfüllt !
Es scheint also kurze kritische Abschnitte im Maschinencode zu geben (oben fett
gesetzt), die unter keinen Umständen unterbrochen werden dürfen, wenn das
Programm zuverlässig arbeiten soll. Was also kann man dagegen tun, daß der
Scheduler an einer solchen Stelle die Ausführung unterbricht ?
Win32 stellt dazu die sogenannten Synchronisierungsobjekte zur Verfügung:
•
•
•
•
Event
CriticalSection
Mutex
Semaphore
Alle diese Objekte werden von der MFC-Klassenbibliothek in entsprechenden
Klassen abgebildet: CEvent, CCriticalSection, CMutex und CSemaphore.
Für unser Problem des kritischen Code-Abschnitts eignen sich die letzten drei der
genannten.
Das Prinzip ist bei allen das selbe: Wenn zwei Threads um den Zugriff auf eine
gemeisame Resource konkurrieren (in unserem Fall die gemeinsam genutzte
Variable m_nCounter), dann müssen sie sich vorher um das Recht zum Zugriff auf
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 193
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
die Resource bewerben. Das geschieht, in dem ein Thread den Besitz eines der
letztgenannten drei Synchronisierungsobjekte erlangt. Wenn ein erster Thread
bereits im Besitz des Objektes ist, und dann ein zweiter Thread ebenfalls versucht,
das Synchronisierungsobjekt zu übernehmen, dann wechselt der zweite Thread
zunächst in den Zustand blocked (worauf der Scheduler einen Kontextwechsel
ausführt) und wird erst dann wieder ready to run, wenn der erste Thread das
Synchronisierungsobjekt freigibt.
9.3.3.2 Wie kommt ein Thread in den Besitz eines Synchronisierungsobjektes ?
Dafür gibt es prinzipiell drei Möglichkeiten (im Beispiel sei cs eine Instanz der Klasse
CCriticalSection, mtx1 und mtx2 je ein CMutex-Objekt ) :
1. Der Thread ruft direkt die Memberfunktion lock() auf:
cs.Lock() // hier hält die Ausführung an, wenn ein anderer
// Thread Besitzer der CriticalSection ist
2. Der Thread erzeugt ein Objekt der Klasse CSingleLock und übergibt einen
Zeiger auf das Synchronisierungsobjekt im Konstruktor. Wenn als zweiter
Parameter TRUE übergeben wird, ruft der Konstruktor sofort cs.Lock() auf:
CSingleLock sLock(&cs, TRUE);
// auch hier hält die Ausführung an, wenn ein anderer
// Thread Besitzer der CriticalSection ist, weil
// im Konstruktor von CSingleLock cs.Lock()
// aufgerufen wird.
oder : Der Konstruktor erzeugt nur das CSingleLock-Objekt, versucht aber nicht
den Besitz der CriticalSection zu erlangen, der Thread ruft dann später die
Lock()-Methode des CSingleLock-Objektes auf:
CSingleLock sLock(&cs, FALSE);// nur konstruieren, nicht lock() aufrufen
[...]
sLock.Lock();
// hier hält die Ausführung an, wenn ein anderer
// Thread Besitzer der CriticalSection ist
Seite 194
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
3. Wie bei CSingleLock, jedoch wird hier im Konstruktor ein Array von Zeigern auf
Synchronisierungsobjekte übergeben, auf deren einzelne oder gemeinsame
Verfügbarkeit mit Hilfe der Lock() – Methode gewartet werden kann.
CSyncObject* SObjArray[3] = {&cs, &mtx1, &mtx2};
// CSyncObject ist die gemeinsame Basisklasse von
// CCriticalSection, CMutex und CSemaphore
CMultiLock mLock(SObjArray,3,FALSE);
// Parameter:
// 1. Array v. Zeigern auf Sync.-Obj.
// 2. Anzahl der Arrayelemente
// 3. TRUE: Lock() wird bereits im Konstruktor aufgerufen
//
FALSE: nur erzeugen
mLock.Lock(INFINITE,TRUE,0);
// Parameter:
// 1. TimeOut in ms oder INFINITE
// 2. alle (TRUE) bzw. mind. eines (FALSE) der Sync.-Obj.
//
müssen verfügbar sein, um weiterzumachen
// 3. Bitmaske, die weitere Optionen zum Abbruch festlegt
9.3.3.3 Wie gibt ein Thread ein Synchronisierungsobjekt wieder frei ?
Man
kann
zu
jeder
Zeit
die
Unlock()-Methode
des
jeweiligen
Synchronisierungsobjektes oder des CSingleLock- bzw. CMultiLock-Objekts
aufrufen. Die Unlock()-Methode wird auch automatisch im Destruktor von
CSingleLock bzw. CMultiLock aufgerufen.
9.3.3.4 Vorteile von CSingleLock und CMultiLock
Die Verwendung von CSingleLock bzw. CMultiLock erscheint zunächst etwas
umständlich, denn zuerst müssen die eigentlichen Synchronisierungsobjekte
geschaffen werden und dann auch noch ein Lock-Objekt. Ist es nicht viel einfacher,
die Lock()-Methode der Synchronisierungsobjekte direkt aufzurufen?
Einfacher schon, aber auch fehlerträchtig:
void Function1()
{
// m_pObjArr[] ist ein Array von Zeigern auf Objekte der KLasse MyObj
// g_Index ist eine globale Variable
// m_cs ist ein CCriticalSection-Object
m_cs.Lock();
// hier wird ein exklusiver
// Zugriff auf g_Index vorbereitet
if (m_pObjArr[g_Index] == NULL)
return;
// Zeiger sollte man immer vor
// dem Zugriff überprüfen
m_pObjArr[g_Index]->Method1();
m_pObjArr[g_Index]->Method2();
m_cs.Unlock();
// hier wird m_cs wieder freigegeben
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 195
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Erkennen Sie das Problem? Wenn in dem Array versehentlich ein Null-Zeiger
vorkommt, wird die Unlock()-Methode nicht mehr ausgeführt ! Die CriticalSection
bleibt für immer im Besitz dieses Threads ! Wenn statt dessen ein CSingleLockObjekt als lokale Variable erzeugt worden wäre, dann hätte deren Destruktor beim
Verlassen der Funktion m_cs.Unlock() automatisch aufgerufen.
9.3.3.5 Unterschied zwischen CCriticalSection, CMutex und CSemaphore
Die wichtigsten Merkmale dieser drei Synchronisierungsobjekte sind:
• CCriticalSection ist recht schnell, kann aber nicht über Prozeßgrenzen
hinaus genutzt werden
• CMutex ist etwas langsamer, kann aber auch Threads, die zu verschiedenen
Prozessen gehören, synchronisieren.
• CSemaphore kann eine begrenzte Zahl gleichzeitiger Besitzer haben und über
Prozeßgrenzen hinaus genutzt werden
9.3.3.6 Zusammenfassung
Um zu entscheiden, welche Synchronisierungsklassen verwendet werden sollen,
stellen Sie sich die folgenden Fragen:
Können mehrere Threads innerhalb derselben Anwendung auf diese Ressource
gleichzeitig zugreifen? (Beispiel: Ihre Anwendung unterstützt bis zu fünf Fenstern mit
Ansichten desselben Dokuments.)
Wenn ja, verwenden Sie CSemaphore.
Wenn nein: Können mehrere Anwendungen diese Ressource verwenden? (Beispiel:
Die Ressource dient der Kommunikation über eine Hardwareschnittstelle)
Wenn ja, verwenden Sie CMutex,
wenn nein verwenden Sie CCriticalSection.
CSyncObject wird nie direkt verwendet. Sie ist die Basisklasse für die anderen vier
Synchronisierungsklassen.
Seite 196
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Apropos andere vier Synchronisierungsklassen: Die vierte haben wir bis jetzt völlig
vernachlässigt, denn sie dient meist einem anderen Zweck: CEvent
9.3.3.7 Ereignisse als Mittel der Inter-Thread-Kommunikation
Ein CEvent-Objekt dient normalerweise nicht dazu, den Zugriff auf eine Ressource
zeitweilig zu sperren, sondern wird normalerweise dazu verwendet, einen gerade
„schlafenden“ Thread zu wecken. Ein Thread, der z.B. eine Kopierfunktion
durchführen soll, erzeugt dazu ein Objekt der Klasse CEvent und ruft dann die
Lock()-Methode dieses Events auf. Dadurch wirft sich der Thread selbst aus dem
Prozessor, denn solange das Event inaktiv ist, versetzt die Lock()-Methode den
Thread in den Zustand blocked. Wenn nun ein anderer Thread den Kopierthread
aufwecken will, weil Daten zum Kopieren bereitstehen, dann ruft der zweite Thread
einfach die SetEvent()-Methode des Events auf, wodurch das Event aktiv wird.
Der Kopierthread wird daraufhin ready to run und kann sein Werk beginnen.
9.3.4 Fortsetzung des Beispiels Primzahlengenerator
Nachdem wir nun die wichtigsten Zusammenhänge der Threadsynchronisierung
kennengelernt haben, können wir unser Beispiel fortsetzen.
Kehren wir zunächst zu dem Problem zurück, wie wir dem Thread das Signal zum
Start der Berechnung geben. Beim momentanen Stand wird der Thread im
Konstruktor der CPrimeGenerator-Klasse erzeugt und beginnt sofort mit der
Ausführung. Weil alle Member-Variablen Null sind, wird die Schleife nie durchlaufen
und der Thread beendet sich mit dem Rückgabewert 0, ohne je etwas sinnvolles
getan zu haben. Wenn der selbe Thread also mehrmals zur Primzahlenberechnung
herhalten soll, muß eine „Endlos“-Schleife um den Primzahlenalgorithmus gelegt
werden. Gleich nach Eintritt in diese Schleife legen wir den Thread schlafen, bis ein
Ereignis den Start der Berechnung auslöst.
Wir fügen der Klasse CPrimeGenerator ein Objekt der Klasse CEvent als Member
hinzu:
CEvent m_evtStart;
Danach ergänzen wir die Threadfunktion wie folgt (fett gedruckt)
// Threadfunktion -----------------------------------------------------UINT CPrimeGenerator::ThreadProc(void * lpArg)
{
// wenn wir auf Variablen außerhalb der static-Funktion zugreifen
// wollen, muß das über den this Zeiger gehen
CPrimeGenerator* lpOuterThis = (CPrimeGenerator*) lpArg;
BOOL bContinueLoop;
long nToTest;
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 197
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
while(TRUE)
// Endlosschleife des Threads
{
// warten auf das Start-Event
lpOuterThis->m_evtStart.Lock();
// DenZähler auf den Anfangswert setzten
lpOuterThis->m_nCounter = lpOuterThis->m_nStart;
// Die Schleifenbedinung ist anfangs erfüllt
bContinueLoop = TRUE;
do // Beginn der Berechnungsschleife
{
// Prüfen, ob m_nCounter eine Primzahle ist,
// dazu wird m_nCounter in eine lokale Variable kopiert,
// die dann an IsPrime() übergeben wird
nToTest = lpOuterThis->m_nCounter;
// Wenn IsPrime() TRUE liefert, wird die Pimzahl mit
// PostMessage an den Dialog geschickt
if (lpOuterThis->IsPrime(nToTest))
PostMessage( lpOuterThis->m_hwndDialog,
STZ_PRIME_FOUND,
0,
nToTest);
// Hier wird geprüft, ob die Schleife weiterlaufen
// soll oder nicht
if (++(lpOuterThis->m_nCounter) > lpOuterThis->m_nEnd)
bContinueLoop = FALSE;
}while(bContinueLoop);
// Der Thread meldet den Abschluß der Berechnung an den Dialog
::PostMessage(lpOuterThis->m_hwndDialog,STZ_PRIME_FINISHED,0,0);
} // Ende der Endlosschleife
return 0; // hierher kommt die Ausführung nie !
}
Somit wird nun jedesmal, wenn von außen das Ereignis m_evtStart ausgelöst
wird, die Berechnung gestartet. Solange das Ereignis inaktiv ist, bleibt der Thread
blockiert und verbraucht damit keine Rechenzeit.
Das Ereignis m_evtStart soll nun in
CPrimeGenerator-Objektes aktiviert werden:
der
Methode
Start()
des
void CPrimeGenerator::Start()
{
// Hier muß dem Thread irgendwie gesagt werden, daß er
// los laufen soll ...
m_evtStart.SetEvent();
}
Nun müssen wir noch das Problem mit dem konkurrierenden Zugriff auf die MemberVariablen lösen:
Wir fügen zunächst der CPrimeGenerator-Klasse eine weitere Membervariable
hinzu:
CCriticalSection m_cs;
Seite 198
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Dann erzeugen wir innerhalb der Threadfunktion ein Lock-Objekt auf diese
CriticalSection:
CSingleLock lck(&(lpOuterThis->m_cs));
Schließlich wird jeder Zugriff auf eine gemeinsam genutzte Variable mit einem
Rahmen aus Lock() und Unlock() umgeben, z.B.:
lck.Lock();
nToTest = lpOuterThis->m_nCounter;
lck.Unlock();
Außerdem muß dies natürlich auch in den Set()-Methoden für die Attribute
m_nStart und m_nEnd geschehen:
void CPrimeGenerator::SetStartValue(long nStart)
{
CSingleLock lck(&m_cs,TRUE);
nStart = max(nStart,3);
if (nStart % 2 == 0)
nStart++;
m_nStart = nStart;
}
void CPrimeGenerator::SetEndValue(long nEnd)
{
CSingleLock lck(&m_cs,TRUE);
m_nEnd = nEnd;
}
In diesem Fall kann auf einen expliziten Aufruf von Lock() und Unlock()
verzichtet werden, denn wenn dem Konstruktor von CSingleLock als zweitem
Parameter TRUE übergeben wird, ruft dieser Lock() bereits intern auf. Unlock()
wird automatisch im Destruktor von CSingleLock aufgerufen, wodurch die
CriticalSection bei der zweiten geschweiften Klammer automatisch freigegeben wird.
9.3.5 Beenden des Arbeits-Threads
Wir haben unseren Arbeits-Thread mit Hilfe der MFC-Funktion AfxBeginThread()
erzeugt und gestartet. Dabei hat das MFC-Framework dynamisch ein Objekt der
Klasse CWinThread erzeugt und alle notwendigen Initialisierungen durchgeführt.
Wenn unsere Anwendung durch klicken auf den „Schließen“-Button beendet wird,
dann wird neben dem Haupt-Thread auch der Arbeits–Thread zerstört. Da aber
unser Programm vorher keine Anstalten gemacht hat, das CWinThread-Objekt
ordnungsgemäß zu beseitigen, meldet die Microsoft-Entwicklungsumgebung beim
Beenden ein „Speicher-Leck“, da der von dem CWinThread-Objekt allokierte
Speicher nicht wieder freigegeben wurde. In unserem Fall ist dies höchstens
schlechter Stil, jedoch ungefährlich für die Stabilität des Programms oder gar des
Systems. Windows NT und Windows 95 beseitigen solchen Speichermüll sehr
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 199
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
zuverlässig, wenn ein Programm endet. Aber was wäre, wenn unser Programm
iterativ immer wieder Threads erzeugen würde, die sich nach der Benutzung als
Speicherleichen ansammeln? Dann würde der vom Programm belegte Speicherplatz
immer größer, bis die Obergrenze des Working-Sets erreicht wäre, und der Prozeß
keine weiteren Threads mehr erzeugen könnte.
Wir sollten also im Sinne guten Programmierstils immer dafür sorgen, das Threads
auch wieder geregelt beendet werden. Dies geschieht z.B. dadurch, daß man dem
Thread mit Hilfe eines Events signalisiert, daß er sich beenden soll. Dazu ändern wir
die Threadfunktion wie folgt:
Die Zeile
// warten auf das Start-Event
lpOuterThis->m_evtStart.Lock();
wird ersetzt durch ein Lock() auf ein CMultiLock-Objekt, das außer auf das StartEvent auch auf ein weiteres Event, das Shutdown-Event reagiert:
// Array der beiden Events erzeugen
CSyncObject* ppEvents[]={&(lpOuterThis->m_evtStart),
&(lpOuterThis->m_evtShutdown) };
// Ein CMultiLock auf die beiden Events erzeugen
CMultiLock lckStartShutdown(ppEvents, 2);
// Index des aktiven Events
DWORD dwIndex;
[ ... ]
while(TRUE)
// Endlosschleife des Threads
{
// warten auf das Start-Event oder das Shutdown-Event
dwIndex = lckStartShutdown.Lock(INFINITE, FALSE) – WAIT_OBJECT_0;
// Index 0 ist das Start-Event
if (dwIndex != 0)
AfxEndThread(0);
[ ... ]
}
Seite 200
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Weiter fügen wir der CPrimeGenerator-Klasse eine Methode hinzu, über die der
geregelte Shutdown des Threads abgewickelt wird:
void CPrimeGenerator::Shutdown()
{
// Das Shutdown-Event setzten
m_evtShutdown.SetEvent();
// 5 sec. auf die Beendigung des Threads warten, ansonsten
// Thread terminieren
if (WaitForSingleObject(m_lpThread->m_hThread, 5000) == WAIT_TIMEOUT)
TerminateThread(m_lpThread->m_hThread, -1);
}
Diese Methode kann von der Dialogklasse aufgerufen werden, bevor das
CPrimeGenerator-Objekt selbst gelöscht wird:
void CMultithread1Dlg::OnDestroy()
{
m_pPrimeGen->Shutdown();
delete m_pPrimeGen;
CDialog::OnDestroy();
}
9.3.5.1 Die endgültige Version der Threadfunktion
// Threadfunktion -----------------------------------------------------UINT CPrimeGenerator::ThreadProc(void * lpArg)
{
// wenn wir auf Variablen außerhalb der static-Funktion zugreifen
// wollen, muß das über den this Zeiger gehen
CPrimeGenerator* lpOuterThis = (CPrimeGenerator*) lpArg;
// Ein temoräres Lock-Objekt auf die CriticalSection m_cs erzeugen
CSingleLock lck(&(lpOuterThis->m_cs));
// Array der beiden Events erzeugen
CSyncObject* ppEvents[]={ &(lpOuterThis->m_evtStart),
&(lpOuterThis->m_evtShutdown) };
// Ein CMultiLock auf die beiden Events erzeugen
CMultiLock lckStartShutdown(ppEvents, 2);
// Index des aktiven Events
DWORD dwIndex;
BOOL bContinueLoop;
long nToTest;
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 201
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
while(TRUE)
// Endlosschleife des Threads
{
// warten auf das Start-Event oder das Shutdown-Event
dwIndex = lckStartShutdown.Lock(INFINITE, FALSE) – WAIT_OBJECT_0;
// Index 0 ist das Start-Event
if (dwIndex != 0)
AfxEndThread(0);
// DenZähler auf den Anfangswert setzten
lck.Lock();
lpOuterThis->m_nCounter = lpOuterThis->m_nStart;
lck.Unlock();
// Die Schleifenbedinung ist anfangs erfüllt
bContinueLoop = TRUE;
do // Beginn der Berechnungsschleife
{
// Prüfen, ob m_nCounter eine Primzahle ist,
// dazu wird m_nCounter in eine lokale Variable kopiert,
// die dann an IsPrime() übergeben wird
lck.Lock();
nToTest = lpOuterThis->m_nCounter;
lck.Unlock();
// Wenn IsPrime() TRUE liefert, wird die Pimzahl mit
// PostMessage an den Dialog geschickt
if (lpOuterThis->IsPrime(nToTest))
PostMessage( lpOuterThis->m_hwndDialog,
STZ_PRIME_FOUND,
0,
nToTest);
// Hier wird geprüft, ob die Schleife weiterlaufen
// soll, oder nicht
lck.Lock();
if (++(lpOuterThis->m_nCounter) > lpOuterThis->m_nEnd)
bContinueLoop = FALSE;
lck.Unlock();
}while(bContinueLoop);
// Der Thread meldet den Abschluß der Berechnung an den Dialog
::PostMessage(lpOuterThis->m_hwndDialog,STZ_PRIME_FINISHED,0,0);
}
return 0;
}
Seite 202
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
9.3.6 Thread-Prioritäten
Wenn Sie das Beispielprogramm MULTITHREAD1.EXE aus dem vorhergehenden
Abschnitt ausführen, werden Sie feststellen, daß das Programm die Anforderung,
jederzeit auf Benutzereingaben zu reagieren, nicht erfüllt. Es scheint fast so, als
würde nur noch der Primzahlen-Thread ablaufen, während der Benutzeroberflächen
Thread, der das Dialogverhalten bestimmt, blockiert zu sein scheint.
Was haben wir falsch gemacht ?
Ein Thread wird vom Threadscheduler nur unter folgenden Bedingungen in die CPU
gelassen:
• der Thread ist ready to run
• es gibt keinen höherprioren Thread, der ebenfalls ready to run ist
Wenn mehrere Threads gleicher Priorität ready to run sind, werden sie der Reihe
nach abgearbeitet. Jeder Thread erhält dann genau eine Zeitscheibe, dann folgt ein
Kontextwechsel zum nächsten gleichprioren Thread usw.
Die Thread-Priorität kann einen Wert von 1 bis 31 annehmen und wird stets relativ
zur Priorität des Prozesses angegeben. Prozesse werden dabei in eine der
folgenden Prioritätsklassen eingeordnet:
• IDLE_PRIORITY_CLASS für Prozesse, die nur ausgeführt werden sollen, wenn
das System im Leerlauf ist, wie z.B. Hintergrund-Virenscanner, Bildschirmschoner
usw. Die „normale“ Priorität der Threads in dieser Klasse ist 4
• NORMAL_PRIORITY_CLASS für Prozesse, die keine besonderen Ansprüche an
den Scheduler stellen, dies ist die Standardeinstellung für neu erzeugte
Prozesse. Die „normale“ Priorität der Threads in dieser Klasse ist 8
• HIGH_PRIORITY_CLASS für Prozesse, die zeitkritische Operationen ausführen
müssen. Mit Vorsicht zu verwenden ! Eventuell kann es sinnvoll sein, diese
Prioritätsklasse
nur
zeitweise
anzunehmen
und
danach
zu
NORMAL_PRIORITY_CLASS zurückzukehren. Die „normale“ Priorität der Threads
in dieser Klasse ist 13
• REALTIME_PRIORITY_CLASS - Prozesse können selbst wichtigste Betriebssystemprozesse verdrängen. Die „normale“ Priorität der Threads in dieser Klasse
ist 24
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 203
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Threads können in einer der folgenden relativen Prioritätsstufen eingeordnet werden:
• THREAD_PRIORITY_NORMAL: normale Priorität für die Prioritätsklasse des
Prozesses
• THREAD_PRIORITY_ABOVE_NORMAL:
um 1 Punkt höhere Priorität als für die
Prioritätsklasse des Prozesses normal ist.
• THREAD_PRIORITY_BELOW_NORMAL:
um 1 Punkt niedrigere Priorität als für
die Prioritätsklasse des Prozesses normal ist.
• THREAD_PRIORITY_HIGHEST:
um 2 Punkte höhere Priorität als für die
Prioritätsklasse des Prozesses normal ist.
• THREAD_PRIORITY_LOWEST:
Priorität 1, wenn der Prozeß zu einer der
Klassen
IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS,
oder
HIGH_PRIORITY_CLASS gehört, bzw. 16 für REALTIME_PRIORITY_CLASS –
Prozesse.
• THREAD_PRIORITY_TIME_CRITICAL: Priorität 15, wenn der Prozeß zu einer
der Klassen IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, oder
HIGH_PRIORITY_CLASS gehört, bzw. 31 für REALTIME_PRIORITY_CLASS –
Prozesse.
Zurück zu der Frage, was bei unserem Primzahlen-Beispiel schiefgelaufen ist:
In unserem Beispiel haben beide Threads, der Benutzeroberflächenthread und der
Arbeits-Thread, die gleiche relative Priorität, nämlich THREAD_PRIORITY_NORMAL.
Wenn der Arbeits-Thread eine Primzahl gefunden hat, schickt er sie mit
PostMessage(...) an das Dialogfenster, welches im Kontext des
Benutzeroberflächenthread läuft. Wenn nach einem Kontextwechsel dieser wieder an
die Reihe kommt, wird er die Message mittels der bekannten
GetMessage()/DispatchMessage()-Schleife
an
die
Handler-Funktion
weiterleiten. Diese Handler-Funktion, die im obigen Beispiel nicht gezeigt wurde,
macht nichts weiter als die Primzahl in einen String umzuwandeln, und diesen String
dann in die ListBox einzufügen. Dies geschieht MFC-intern ebenfalls über
Nachrichten, die an die ListBox gesendet werden. Ferner werden, um den
veränderten Inhalt der ListBox auch anzuzeigen, noch mehrere WM_PAINTNachrichten an die ListBox und das Dialogfenster verschickt. Alle diese Nachrichten
muß
der
Benutzeroberflächenthread
in
seiner
GetMessage()/
DispatchMessage()-Schleife verarbeiten. Da er aber mit der selben Priorität wie
der Arbeits-Thread läuft, bekommen beide (annähernd) gleich viel Rechenzeit. Sie
können sich leicht vorstellen, daß der Arbeiter-Thread viel schneller neue Primzahlen
an der Dialog melden kann als dieser bzw. sein Thread die vielen daraus
resultierenden Nachrichten aus der Nachrichtenwarteschlange entfernen kann. Die
Warteschlange füllt sich also immer mehr, und unsere BN_CLICKED-Nachricht, die
der Stop-Button abgesetzt hat, reiht sich ganz hinten ein. Wir müssen also dafür
sorgen, daß der Benutzeroberflächenthread genug Zeit erhält, seine Nachrichten
auch komplett abzuarbeiten. Wir setzen dazu die Priorität des Arbeits-Threads gleich
bei seiner Erzeugung um einen Punkt herab:
Seite 204
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
CPrimeGenerator::CPrimeGenerator(HWND hwndDialog)
{
// Fensterhandle des Dialogfelds in die Membervariable übernehmen
m_hwndDialog = hwndDialog;
// Thread erzeugen:
// this – Zeiger als Übergabeparameter
// Priorität: 1 Punkt unter normal
m_lpThread = AfxBeginThread(ThreadProc,this,THREAD_PRIORITY_BELOW_NORMAL);
}
Nun wird der Benutzeroberflächenthread solange alleine in den Prozessor kommen,
bis er sich selbst in den Zustand blocked versetzt. Dies geschieht automatisch immer
dann, wenn der Aufruf von GetMessage() erfolglos war, weil keine Nachricht in der
Nachrichtenschlange mehr ist. Wenn wieder eine Nachricht vorliegt, wird der Zustand
wieder ready to run. Der Thread darf nun wieder alle Nachrichten abarbeiten, bis die
Schlange erneut leer ist.
9.4 Benutzeroberflächenthreads: nicht nur an der Oberfläche
Der Thread, den wir im Beispiel MULTITHREAD1 erzeugt haben, war ein ArbeitsThread. Die Kommunikation von außen in diesen Thread hinein erfolgte über
Variablen bzw. Ereignisse. Manchmal wäre es allerdings wünschenswert, wenn man
mit einem selbst erzeugten Thread auch über Nachrichten verkehren könnte. Dazu
muß der Thread allerdings auch eine eigene Nachrichtenverarbeitung betreiben.
Benutzeroberflächenthreads haben eine eigene Nachrichtenschleife und bekamen
ihren Namen daher, weil natürlich alle Threads, die irgend eine Ausgabe auf den
Bildschirm machen wollen, von diesem Typ sein müssen, um die vielen
oberflächenbezogenen Systemnachrichten verarbeiten zu können. Wenn Sie also
daran denken, ein Fenster oder einen Dialog in einem eigenen Thread ablaufen zu
lassen, dann sind sie an diesen Threadtypus gebunden. Sie können aber einen
Benutzeroberflächenthread auch dann erzeugen, wenn Sie gar keine
Benutzeroberflächen-Objekte darin verwenden möchten. Ein solcher „fensterloser“
Benutzeroberflächenthread hat gegenüber einem Arbeitsthread den Vorteil, das man
mit ihm über Nachrichten kommunizieren kann. Das ist besonders dann von Vorteil,
wenn ein Thread auf viele verschiedene äußere Ereignisse oder „Aufträge“ reagieren
können soll. Durch die Windows-Nachrichtenwarteschlage bekommen sie sozusagen
eine Auftrags-Warteschlange nach dem FIFO-Prinzip samt den zugehörigen
Auftragsverteiler-Funktionen frei Haus mitgeliefert.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 205
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Im nun folgenden Beispiel MULTITHREAD2 werden Sie sehen, wie man einen
Thread mit eigener Nachrichtenschleife erzeugt.
In MULTITHREAD2 wird eine Dialogklasse, die das Benutzerinterface implementiert,
die Dienste einer „Server“-Klasse in Anspruch nehmen, in dem sie benutzerdefinierte
Auftragsnachrichten über das Windows-Nachrichtensystem an den „Server“ schickt.
Dieser liefert die Ergebnisse ebenfalls über Nachrichten zurück an den Dialog (den
„Client“).
Die Server-Klasse CStatistics soll folgende Aufträge verarbeiten können:
1. STZ_CREATE_ARRAY: Ein dynamisches Array aus Zufallszahlen erzeugen (ein
CDWordArray-Objekt)
2. STZ_CALCULATE_MEAN: Den Mittelwert aller Elemente in einem CDWordArray
berechnen
3. STZ_CALCULATE_STD_DEV: Die Standardabweichung aller Elemente in einem
CDWordArray berechnen
CStatistics antwortet darauf mit den Nachrichten STZ_ARRAY_CREATED,
STZ_MEAN_CREATED und STZ_STD_DEV_CREATED.
Abbildung 74: Multithread2 in Aktion
Der erste Schritt zur Erzeugung eines Benutzeroberflächenthreads ist stets, eine
Klasse von CWinThread abzuleiten:
Seite 206
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
Abbildung 75: Eine von CWinThread abgeleitete Klasse erzeugen
Sowohl die Dialogklasse als auch die „Server“-Klasse CStatistics bildet die
Nachrichten, die sie empfängt mit Hilfe der message map auf eine
Nachrichtenverarbeitungs-Methode ab.
Wenn Sie die Klasse wie in der Abbildung oben mit Hilfe des Klassenassistenten
erzeugen, fügt der Code-Generator von VisualStudio97 alle nötigen Makros für die
Definition der message map automatisch in den Code ein.
Nun muß der Dialog ein Objekt der Klasse CStatistics erzeugen. Dies geschieht
wieder durch Aufruf der überladenen Funktion AfxBeginThread, diesmal kommt
jedoch die andere Variante zur Anwendung:
BOOL CMultithread2Dlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 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
// Das Thread-Objekt erzeugen
m_pStatisticsThread = (CStatistics*)
AfxBeginThread(RUNTIME_CLASS(CStatistics),
THREAD_PRIORITY_BELOW_NORMAL);
m_pStatisticsThread->SetOwner(GetSafeHwnd());
m_pArray = NULL;
return TRUE; // Geben Sie TRUE zurück, außer ein Steuerelement soll den
// Fokus erhalten
}
Beachten Sie, daß diese Variante von AfxBeginThread keinen Parameter an eine
Threadfunktion übergeben kann. Da aber auch hier, wie schon im Beispiel
MULTITHREAD1, der Thread seinen Besitzer kennen muß, wurde in der
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 207
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
CStatistics-Klasse eine Methode SetOwner( ... ) implementiert, die das
Fenster-Handle des Dialogs entgegen nimmt.
Nachfolgend sind einige Beispiele für die Nachrichtenbearbeitungs-Methoden der
beiden Klassen abgedruckt. Wie alle Handler-Methoden für benutzerdefinierte
Nachrichten müssen sie zwei Parameter übernehmen, WPARAM wParam und
LPARAM lParam. Diese Parameter können nach Belieben für den Datenaustauch
zwischen Client und Server genutzt werden. Die Bedeutung der Parameter sollte
zusammen mit den benutzerdefinierten Nachrichten und außerdem im Quellcode bei
der Handlerfunktion dokumentiert werden.
Hier zunächst der Standard-Handler, der das Klicken auf den „Array erzeugen“Button abarbeitet:
void CMultithread2Dlg::OnCreateButtonPressed()
{
// den “Array erzeugen” – Button sperren
m_CreateButton.EnableWindow(FALSE);
// Falls noch ein altes Array vorhanden ist à löschen:
if (m_pArray != NULL)
delete m_pArray;
// den Befehl zur Erzeugung eines Zufallszahlen-Arrays abschicken
// 10000 Elemente
// im Bereich von 0 bis 100000
PostThreadMessage( m_pStatisticsThread->m_nThreadID,
STZ_CREATE_ARRAY, 10000, 100000);
}
OnCreateButtonPressed() schickt in seiner letzten Zeile den Auftrag zum
Erzeugen eines neuen Arrays an den Statistics-Thread. Dazu kann hier natürlich
nicht PostMessage verwendet werden, weil der Server-Thread kein Fenster und
damit auch kein Fenster-Handle besitzt. Statt dessen wird die Variante
PostThreadMessage benutzt, die als ersten Parameter statt dem Fenster-Handle
die Thread-ID übergeben bekommt. Es folgt die Nachricht STZ_CREATE_ARRAY
sowie die beiden benutzerdefinierten Parameter.
Nun die Methode der Klasse CStatistics, die diese Nachricht entgegen nimmt:
// Parameter:
// wParam :(unsigned short) Zahl der zu erzeugenden Zufallszahlen (Arraygröße)
// lParam :(DWORD) obere Grenze des Zufallszahlen-Intervalls
void CStatistics::OnCreateArray(WPARAM wParam, LPARAM lParam)
{
// Zufallszahl
DWORD dwRand;
// Größe des Arrays
USHORT nSize = (USHORT) wParam;
// obere Intervallgrenze für die Zufallszahlen
DWORD dwUpper = (DWORD) lParam;
// Arrayobjekt auf dem Heap erzeugen
CDWordArray* pArray = new CDWordArray;
Seite 208
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Multithreading
// Zufallsgenerator initialisieren
srand((unsigned)time(NULL));
for (UINT i = 0; i < wParam; i++)
{
dwRand = (DWORD) rand() * float(dwUpper) / RAND_MAX;
pArray->Add(dwRand);
}
// Den Zeiger auf das fertige Array an den Dialog-Thread melde
n
PostMessage(m_hwndOwner, STZ_ARRAY_CREATED, 0, (LPARAM) pArray);
}
Beachten Sie, wie die Funktion des Servers das von ihr generierte Array an den
Client übergibt: Das CDWordArray-Objekt wird dynamisch auf dem Heap erzeugt
und dann der Zeiger auf das Array mit der Nachricht STZ_ARRAY_CREATED im
LPARAM mitgeschickt.
Der Message-Handler, der auf Seiten des Dialoges (des Clients) das Ergebnis
verarbeitet, sieht wie folgt aus:
// Parameter:
// wParam: ohne Bedeutung
// lParam: (CDWordArray*) auf das erzeugte Array
void CMultithread2Dlg::OnArrayCreated(WPARAM wParam, LPARAM lParam)
{
m_pArray = (CDWordArray*) lParam;
// den “Array erzeugen” – Button wieder freigeben
m_CreateButton.EnableWindow(TRUE);
}
Bei den anderen Message-Handlern (für die Berechnung des Mittelwertes und der
Standardabweichung) gilt ebenfalls das oben gezeigte Prinzip: Wenn ein Nutzdatum
in den WPARAM oder LPARAM hineinpaßt, wird es direkt übergeben, andernfalls
allokiert der Aufrufende den Speicher auf dem Heap und übergibt den Zeiger auf das
Speicherobjekt im LPARAM an den Aufgerufenen. Dieser ist dann für die weitere
Verarbeitung der Daten sowie für die Freigabe des Heap-Speichers zuständig.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 209
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10 Einführung in die Speicherverwaltung von Win32
Zu verstehen, auf welche Art und Weise ein Betriebssystem den Arbeitsspeicher
verwaltet, ist oft der Schlüssel zur erfolgreichen Planung und Implementation
effizienter Anwendungen. Dabei bezieht sich „effizient“ sowohl auf die
Speichernutzung als auch auf das Laufzeitverhalten eines Programms.
Das folgende Kapitel gibt daher einen grundlegenden Einblick in die
Speicherverwaltung der wichtigsten Win32-Implementationen, natürlich ohne
Anspruch auf Vollständigkeit erheben zu wollen. Wenn Sie an einer tiefgreifenden
Erläuterung der Win32-Speicherarchitektur interessiert sind, sei Ihnen hiermit das
Buch „Windows – Programmierung für Experten“ von Jeffrey Richter (Microsoft
Press) empfohlen. Das vorliegende Kapitel ist im wesentlichen eine
Zusammenfassung der über 200 Seiten umfassenden Darstellung in Richters Buch.
10.1 Virtuelle Adressräume
Alle Adressen in Win32-Programmen haben eine Breite von 32 Bit. Daraus ergibt
sich ein Adressraum von 232 = 4G Adressen. Da alle Prozessoren, für die Win32
bisher implementiert wurde, eine Adressierungsgranularität von 8 Bit = 1 Byte
besitzen, ergibt sich also ein Adressraum von 4 Gigabyte.
Unter MS-DOS und 16-Bit-Windows liefen alle Prozesse in einem gemeinsamen
Adressraum ab, was bedeutet, dass jeder Prozess Zugriff auf den Speicher eines
anderen Prozesses hatte. Dieser Umstand ist einer der Hauptgründe für die
mangelhafte Systemstabilität des 16-Bit-Windows, denn ein Prozess kann über einen
gar nicht oder falsch initialisierten Zeiger Daten eines anderen Prozesses oder sogar
des Betriebssystem-Kernels überschreiben .
Unter Win32 wird dieses Problem dadurch gelöst, dass jeder Prozess seinen
eigenen, privaten 4GB-Adreßraum erhält. Die Speicherbereiche anderer Prozesse
sind für die Threads eines Prozesses weder sichtbar noch erreichbar.
Auf dem Rechner, auf dem dieser Text geschrieben wird, laufen in diesem Moment
26 Prozesse, jeder davon hat vom Betriebssystem seinen privaten 4GB
umfassenden Adressraum erhalten, jedoch ist der tatsächlich verfügbare Speicher
auf diesem Computer natürlich weitaus kleiner als 26 x 4 GB = 104 GB. Es muss
also einen Mechanismus geben, der den Prozessen einen scheinbaren (virtuellen)
Adressraum von 4GB vorgaukelt, während sich in Wirklichkeit viele Prozesse einen
viel kleineren tatsächlichen (physischen) Speicher teilen. Genau das ist die Aufgabe
der Speicherverwaltung (virtual memory management unit) des Betriebsystems.
Seite 210
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.1.1 Abbildung von virtuellem auf physischen Speicher
Was passiert eigentlich genau, wenn ein Win32-Prozess eine Anweisung wie die
folgende ausführt?
*lpLongVal = 5L;
// lpLongVal ist ein gültiger Zeiger auf
// long mit dem Wert 0x00411A80
Die Adresse 0x00411A80, in die der long-Wert 5 geschrieben wird, ist nicht etwa
die Adresse der Speicherzelle im RAM, in der der Wert schließlich abgelegt wird,
sondern eine virtuelle Adresse im Adressraum des Prozesses, die in einem anderen
Prozess durchaus gleichzeitig zu einem anderen Zweck verwendet werden kann.
Wie findet nun das Betriebssystem die eindeutige Zuordnung von dieser virtuellen
Adresse auf eine tatsächlich vorhandene physische Adresse im Speicher des
Rechners?
Die Speicherverwaltung hält für jeden Prozess eine Datenstruktur bereit, deren
Anfangsadresse sie bei jedem Prozesskontextwechsel in das CR3-Register der CPU
schreibt. Genaugenommen zeigt das CR3-Register auf den Anfang des
Seitentabellen-Verzeichnisses (page table directory). Dieses Verzeichnis besteht aus
210 = 1024 Einträgen, die jeweils auf eine weitere Tabelle, die Seitentabelle zeigen.
Die Seitentabellen enthalten ebenfalls 1024 Einträge, die ihrerseits jeweils auf einen
Block, die sogenannte Seite (page), im physischen Speicher zeigen. Die Größe einer
Seite ist bei ix86, MIPS R4000 und PowerPCs 4KB, bei DEC-Alpha-Rechnern
beträgt die Seitengröße 8KB.
Die nachfolgende Grafik veranschaulicht, wie z.B. eine Intel x86-CPU mit Hilfe der
oben genannten Tabellen unseren long-Zeiger aus dem Beispiel in eine RAMAdresse umwandelt:
virtuelle Adresse:
0x00411A80
00000000 01000001 00011010 10000000
physische
Speicherseite
(4KB)
page table
(bis zu 1024 mal
je Prozeß vorhanden,
jeweils 1024 Zeilen)
page table directory
(einmal je Prozeß
vorhanden,
1024 Zeilen )
Byte 2688
...
Byte 0
Eintrag 17
...
Eintrag 0
...
Eintrag 1
Eintrag 0
CR3
Abbildung 76: Umsetzung von virtuellen in physische Adressen (Intel CPU)
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 211
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Bitte beachten Sie, dass man als Programmierer die physikalische Adresse nie zu
Gesicht bekommt, da die Umwandlung erst unmittelbar beim Zugriff auf die virtuelle
Adresse von der CPU vorgenommen wird. Es geht sogar noch weiter: Sie können
normalerweise noch nicht einmal sicher sein, dass sich die physikalische Seite, die
Ihre virtuelle Adresse verkörpert, überhaupt im Speicher befindet!
Um nämlich den teuren Halbleiter-Speicher effizienter nutzen zu können, lagert das
Betriebssystem Seiten, die längere Zeit nicht benutzt wurden, in eine Datei auf der
Festplatte aus. Der physikalische Speicher wird also um einen Bereich auf der
Festplatte erweitert. Ein Flag in jedem Eintrag der Seitentabellen zeigt an, ob die
entsprechende Seite sich im RAM oder auf der Festplatte befindet. Versucht Ihr
Programm auf eine Seite zuzugreifen, deren „Anwesenheitsbit“ 0 ist, erzeugt der
Prozessor eine sog. „page fault exception“, die vom Betriebssystem abgefangen
wird. Das Betriebssystem lädt dann die fehlende Seite von der Festplatte in den
Speicher, aktualisiert die Seitentabelle und lässt dann die CPU mit dem
Speicherzugriff fortfahren. Was passiert aber, wenn für das Nachladen der Seite gar
kein RAM zur Verfügung seht? Dann sucht sich das Betriebssystem eine
Speicherseite aus, die im Moment nicht benötigt wird, um diese freizugeben. Zuerst
wird geprüft, ob der Inhalt der freizugebenden Seite verändert wurde (über ein
weiteres Flag der Seitentabelleneinträge), falls ja, schreibt das Betriebssystem diese
Seite in die Auslagerungsdatei. Nun wird die Seite, auf die ursprünglich ein Zugriff
erfolgen sollte, an die Stelle der gerade freigegebenen Seite geladen.
Zugriffsversuch
auf eine virtuelle
Adresse
Seite im
RAM ?
NEIN
Seite in
Auslagerungsdatei ?
NEIN
Zugriffsverletzung
JA
JA
freie Seite
im RAM
vorhanden
NEIN
Suche nach Seite,
die freigegeben
werden kann
NEIN
Inhalt der
Seite
verändert ?
JA
Umwandlung
virtuelle à phys.
Adresse (CPU)
Seite aus der
Auslagerungsdatei
laden
JA
Zugriff auf die
Speicherzelle
erfolgt
Seite in die
Auslagerungsdatei
schreiben
Abbildung 77: Ablauf beim Zugriff auf eine virtuelle Adresse
Seite 212
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.1.2 Aufteilung des virtuellen Speichers unter Windows 95
0xFFFFFFFF
.
.
.
1 GByte großer Bereich für virtuelle Gerätetreiber (VxD),
Speicherverwaltung, Dateisystem.
Wird von allen Win32-Prozessen gemeinsam verwendet
Zugriff: lesen / schreiben - aber Hände weg !!
0xC0000000
0xBFFFFFFF
.
.
.
1 GByte großer Bereich für speicherbasierte Dateien, gemeinsam
verwendete Win32-DLLs, 16-Bit-Anwendungen
Wird von allen Win32-Prozessen gemeinsam verwendet
Zugriff: lesen / schreiben - verwendbar
0x80000000
0x7FFFFFFF
.
.
.
2.143.289.344 Byte (2 GByte – 4 MByte) privater Adreßraum für Win32Prozesse
Von anderen Prozessen aus nicht sichtbar
nicht reserviert, verwendbar
0x00400000
0x003FFFFF
.
.
.
4.190.208 Byte (4 MByte – 4 KByte) für MS-DOS und 16-Bit-Windows –
Kompatibilität
Win32-Prozesse sollten hier nicht zugreifen
Zugriff: lesen / schreiben - aber Hände weg !!
0xC0001000
0x00000FFF
.
.
.
4096 Byte vollkommen gesperrter Speicher
Jeder Zugriff (schreibend oder lesend) erzeugt eine Schutzverletzung
Nützlich beim Auffinden von NULL-Zeiger-Zugriffen
0xC0000000
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 213
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.1.3 Aufteilung des virtuellen Speichers unter Windows NT
0xFFFFFFFF
.
.
.
.
.
.
.
.
.
.
.
.
2 GByte großer Bereich für das Betriebssystem
Jeder Zugriff (schreibend oder lesend) erzeugt eine Schutzverletzung
0x80000000
0x7FFFFFFF
0x7FFF0000
0x7FFEFFFF
.
.
.
.
.
.
.
.
.
.
.
64 KByte vollkommen gesperrter Speicher
Jeder Zugriff (schreibend oder lesend) erzeugt eine Schutzverletzung
Nützlich beim Auffinden von Zugriffen über ungültige Zeiger von KernelMode-Code
2.147.352.576 Byte (2 GByte – 128 KByte) privater Adreßraum des
Prozesses
Von anderen Prozessen aus nicht sichtbar
nicht reserviert, verwendbar
0x00010000
0x0000FFFF
64 KByte vollkommen gesperrter Speicher
0x00000000
Jeder Zugriff (schreibend oder lesend) erzeugt eine Schutzverletzung
Nützlich beim Auffinden von NULL-Zeiger-Zugriffen
Seite 214
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2 Verwenden von virtuellem Speicher in eigenen Anwendungen
Anwendungen benutzen
verschiedene Arten:
das
oben
beschriebene
Speichermodell
auf
drei
• direkte Anforderung einer oder mehrerer Seiten des virtuellen Adressraumes
zur freien Verwendung durch die Anwendung, nützlich für die Speicherung
größerer Objekte
• Speicherbasierte Dateien, also Dateien, die direkt in den virtuellen Adressraum
eines Prozesses eingeblendet werden
• Heaps, der aus der klassischen C/C++ - Welt bekannte Raum für dynamisch
allokierten Speicher, nützlich für die Speicherung vieler kleiner Objekte
10.2.1 Direkte Anforderung von Seiten des virtuellen Adressraumes
Wenn eine Anwendung große zusammenhängende Datenblöcke im Speicher
ablegen will, empfiehlt es sich, direkt eine oder mehrere Seiten des virtuellen
Adressraumes von der Speicherverwaltung anzufordern. Dazu stellt die Win32-API
u.a. folgende Funktionen zur Verfügung:
LPVOID VirtualAlloc(LPVOID lpAddress,
// address of region to
// reserve or commit
DWORD dwSize,
// size of region
DWORD flAllocationType,
// type of allocation
DWORD flProtect
// type of access
// protection
);
VirtualAlloc übernimmt
• im ersten Parameter lpAddress die gewünschte Startadresse des
Speicherblocks (wird auf die nächste 64k-Grenze abgerundet), wenn NULL
übergeben wird, sucht die Speicherverwaltung von unten nach oben nach einem
geeigneten Bereich;
• im zweiten Parameter dwSize die Größe des gewünschten Speicherbereiches in
Byte (wird auf die nächste Seitengrenze aufgerundet),
• ein Bit-Feld, in dem angegeben wird, ob der Speicher nur reserviert und/oder
auch belegt werden soll (Erklärung s. unten)
• ein Bit-Feld, in dem die Zugriffsrechte für den Speicherbereich festgelegt werden.
Der Rückgabewert ist ein Zeiger auf die Basisadresse des Speicherbereichs, für den
die gewünschte Funktion ausgeführt wurde.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 215
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.1.1
Unterschiede zwischen reserviertem und belegtem Speicher
Der dritte Parameter von VirtualAlloc ist ein Bitfeld, das aus einer oder mehreren
(ver-oderten) der Konstanten MEM_RESERVE, MEM_COMMIT, MEM_RESET und
MEM_TOP_DOWN
zusammengesetzt wird.
• MEM_RESERVE: Der Speicher wird nur innerhalb des virtuellen Adressraumes
reserviert, aber nicht belegt. Ein Thread kann so einen großen
zusammenhängenden Speicherbereich reservieren, um zu verhindern, dass
andere Threads sich mit ihren Speicheranforderungen „dazwischen drängen“ und
den Speicher fragmentieren. Die Reservierung allein belegt den physischen
Speicher nicht, sie verhindert nur, dass in dem reservierten Bereich des virtuellen
Adressraumes weitere Reservierungen oder Belegungen vorgenommen werden.
• MEM_COMMIT: Mit diesem Flag wird ein vorher reservierter Bereich tatsächlich
physisch belegt. Erst nach der Belegung kann auf den Speicher lesend oder
schreibend zugegriffen werden. Dieses Flag kann mit MEM_RESERVE ver-odert
werden, um einen Bereich in einem Schritt zu reservieren und zu belegen.
• MEM_RESET: (nur Windows NT) löscht das „dirty“-Flag der angegebenen
Speicherseiten. Das „dirty“-Flag einer Speicherseite wird von der CPU bei einem
Schreibzugriff gesetzt, um veränderte von nicht veränderten Seiten unterscheiden
zu können.
• MEM_TOP_DOWN: Wenn als gewünschte Startadresse NULL übergeben wird, sucht
die Speicherverwaltung normalerweise beginnend mit der niedrigsten erlaubten
Adresse (von unten nach oben) nach einem geeigneten freien Bereich. Mit dem
Flag MEM_TOP_DOWN beginnt die Suche bei der höchsten erlaubten Adresse (von
oben nach unten).
Bemerkung: Windows 95 ignoriert das Flag MEM_TOP_DOWN
10.2.1.2
Zugriffsrechte auf Speicherseiten
Jeder Eintrag in der Seitentabelle , also jede virtuelle Speicherseite, die für einen
Prozess belegt wird, erhält Flags, mit denen die Zugriffsart auf die jeweilige Seite
begrenzt werden kann. Beim Belegen von Speicher (MEM_COMMIT) kann der
Funktion
VirtualAlloc
im
vierten
Parameter
die
gewünschte
Zugriffsbeschränkung übergeben werden:
• PAGE_READONLY: nur Lesezugriff erlaubt. Falls die CPU zwischen dem Lesen
von Daten (Data-Read) und dem Lesen von Code (Operation-Fetch)
unterscheidet, ist hier nur das Lesen von Daten gemeint.
• PAGE_READWRITE: (Daten-) Lese- und Schreibzugriff erlaubt.
• PAGE_EXECUTE: Nur Operation-Fetch-Zugriff erlaubt, falls die CPU dies von
Daten-Zugriffen unterscheidet.
• PAGE_EXECUTE_READ: Data-Read und Operation-Fetch erlaubt.
• PAGE_EXECUTE_READWRITE: Data-Read, Data-Write und Operation-Fetch
erlaubt.
• PAGE_NOACCESS: keinerlei Zugriff erlaubt.
Seite 216
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
• PAGE_GUARD: jeder Zugriff erzeugt eine Guard-Page-Exception, dann wird das
Guard-Flag gelöscht. Das Guard-Flag wird oft bei der letzten Seite eines
größeren Bereichs gesetzt (bei der „Wächterseite“). Die Guard-Page-Exception
wird dann als „Warnschuss“ abgefangen, der anzeigt, dass der belegte
Speicherbereich gleich erschöpft ist.
• PAGE_NOCACHE: dieses Flag weist die Speicherverwaltung an, die Seite niemals
auszulagern. Mit Vorsicht zu verwenden ! (Sollte Treibern vorbehalten bleiben)
Bemerkung: Windows 95 unterscheidet grundsätzlich nicht zwischen Data-Read
und Operation-Fetch. PAGE_EXECUTE und PAGE_EXECUTE_READ entspricht
PAGE_READONLY und PAGE_EXECUTE_READWRITE entspricht PAGE_READWRITE.
10.2.2 Freigeben von Speicherseiten: VirtualFree
Speicher, der mit VirtualAlloc reserviert oder belegt wurde, kann mit der
Funktion
BOOL VirtualFree(LPVOID lpAddress,
DWORD dwSize,
DWORD dwFreeType
);
// address of region of
// committed pages
// size of region
// type of free operation
wieder freigegeben werden.
VirtualFree übernimmt
• im ersten Parameter die Startadresse des Speicherbereichs
• im zweiten Parameter die Größe des Speicherbereichs
• und im dritten Parameter die Operation, die mit dem
Speicherbereich ausgeführt werden soll
angegebenen
Die Operation, die im dritten Parameter angegeben werden muss, ist entweder:
• MEM_DECOMMIT: Die Belegung des Speichers durch einen früheren Aufruf von
VirtualAlloc mit der Option MEM_COMMIT wird aufgehoben, der Speicher
bleibt jedoch reserviert.
• MEM_RELEASE: Die Reservierung einer Seite oder eines Bereiches von mehreren
Seiten wird aufgehoben. In diesem Fall muss als Startadresse genau die Adresse
angegeben werden, die VirtualAlloc beim Reservieren zurückgeliefert hat
und als Größe muss 0 eingetragen werden. Außerdem müssen alle Seiten im
gleichen Zustand (belegt oder nicht belegt) sein.
Beide Flags können auch ver-odert werden, um für die evtl. noch belegten Seiten
erst die Belegung aufzuheben und dann die Reservierung rückgängig zu machen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 217
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.3 Speicherbasierte Dateien (memory mapped files)
Wenn eine Anwendung große Mengen von zusammenhängenden Daten (ein Bild,
digitalisierte Videos oder Töne, Daten einer Messvorrichtung) zur Bearbeitung in den
Speicher holen muss, stellt sich üblicherweise die Frage: Sollen alle Daten auf
einmal in den Speicher geladen werden (was viel Speicher verschlingt, lange dauert,
aber schnelle Zugriffe gestattet) oder soll jeweils nur der gerade benötigte Teil der
Daten
im
Speicher
gehalten
werden
(ressourcenschonend,
wenig
Anfangsverzögerung aber langsame Folgezugriffe) ?
Die Win32-API stellt für dieses Problem eine weitere Alternative zur Verfügung: die
speicherbasierten Dateien. Darunter versteht man Dateien, die als Ganzes oder
teilweise in den virtuellen Adressraum eines Prozesses eingeblendet werden, so
dass sie dann wie physikalischer RAM aussehen und sich auch mit den üblichen
Mitteln (Zeiger, Speicherzugriffsfunktionen) lesen oder beschreiben lassen. Sogar die
oben beschriebenen Schutzattribute für Speicherseiten sind auf die Seiten einer
speicherbasierten Datei anwendbar.
Für speicherbasierte Dateien gibt es drei Anwendungsgebiete:
• Windows NT verwendet speicherbasierte Dateien zum Laden und zur Ausführung
von EXE- und DLL- Code, d.h. wenn eine Anwendung gestartet wird, lädt
Windows NT nicht etwa den Programmcode von der Platte in den Speicher,
sondern bildet die EXE- und DLL- Dateien direkt im virtuellen Adressraum des
Prozesses ab. Dadurch verringert sich der benötigte Platz in der
Auslagerungsdatei und die zum Start der Anwendung benötigte Zeit.
• Anwendungen benutzen speicherbasierte Dateien für den Zugriff auf Nutzdaten.
Die Anwendung wird damit von der Aufgabe des Dateizugriffs und des
Zwischenspeicherns befreit.
• Speicherbasierte Dateien sind ein Äquivalent zum UNIX-typischen shared
memory, denn sie können von mehreren Prozessen gleichzeitig geöffnet, gelesen
und beschrieben werden.
Im Folgenden soll eine kurze Schritt-für-Schritt-Anleitung für die Verwendung von
speicherbasierten Dateien gegeben werden. Die API-Funktionen sind hier nur
verkürzt beschrieben, lesen Sie auf jeden Fall die offizielle Microsoft-Dokumentation
zu den verwendeten Funktionen, bevor Sie speicherbasierte Dateien in Ihren
eigenen Projekten verwenden.
Seite 218
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.3.1
Schritt 1: Erzeugen oder Öffnen des Dateiobjekts
Wie bei anderen Dateien auch, geschieht dies über CreateFile(...)
HANDLE CreateFile(LPCTSTR lpFileName,
// pointer to name of
// the file
DWORD dwDesiredAccess,// access (read-write)
// mode
DWORD dwShareMode,
// share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
// pointer to security
// attributes
DWORD dwCreationDisposition,// how to create
DWORD dwFlagsAndAttributes,// file attributes
HANDLE hTemplateFile // handle to file
// with attributes
// to copy);
Erklärung der wichtigsten Parameter:
lpFileName
dwDesiredAccess
Zeiger auf String mit dem Dateinamen
Flags, die den gewünschten Zugriff auf die Datei beschreiben.
Mögliche Werte sind:
0
kein Zugriff
GENERIC_READ
GENERIC_WRITE
Lesezugriff
Schreibzugriff
Die Werte können auch OR-verknüpft werden.
dwShareMode
Flags, die dem Betriebssystem mitteilen, ob die Datei von
mehreren Prozessen gemeinsam genutzt werden soll.
Mögliche Werte sind:
0
kein weiterer Zugriff (exklusiv für
diesen Prozess geöffnet)
FILE_SHARE_READ
kann ein weiteres mal zum Lesen
geöffnet werden.
FILE_SHARE_WRITE kann ein weiteres mal zum
Schreiben
geöffnet werden
Die Werte können auch OR-verknüpft werden.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 219
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Beispiel:
HANDLE hFile =
CreateFile(
// Dateiname
“Beispiel.dat”,
// sowohl Lese- als auch Schreibzugriff
GENERIC_READ | GENERIC_WRITE,
// andere Prozesse dürfen zum Lesen öffnen
FILE_SHARE_READ,
// lpSecurityAttributes nicht verwendet
NULL,
// Datei nur öffnen, wenn schon vorhanden
OPEN_EXISTING,
// keine besonderen Attribute oder Flags
FILE_ATTRIBUTE_NORMAL,
// keine Musterdatei für die Attribute
NULL
);
Schritt 2: Erzeugen eines internen Dateiabbildungsobjekts
Nachdem Sie im vorherigen Schritt dem Betriebssystem die physische Position der
Daten im Dateisystem mitgeteilt haben, müssen Sie jetzt festlegen, wie viel
physischer Speicher für das Dateiabbildungsobjekt benötigt wird. Dies geschieht mit
der Funktion CreateFileMapping
HANDLE CreateFileMapping(
HANDLE hFile,
// handle to file to map
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
// optional security attributes
DWORD flProtect,
// protection for mapping object
DWORD dwMaximumSizeHigh,
// high-order 32 bits of object size
DWORD dwMaximumSizeLow,
// low-order 32 bits of object size
LPCTSTR lpName
// name of file-mapping object
);
CreateFileMapping reserviert oder belegt keinen Speicher, sondern dient nur der
Vorbereitung für die Belegung, die dann im nächsten Schritt erfolgt.
Seite 220
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Erklärung der wichtigsten Parameter:
hFile
flProtect
Handle des Dateiobjekts (Rückgabewert von CreateFile)
Flags, die analog zu VirtualAlloc die Schutz- bzw.
Zugriffsattribute der virtuellen Speicherseiten festlegen
nur Lesezugriff
Lese- und Schreibzugriff
Beim Schreiben auf die Seite wird
eine
private
Kopie
in
der
Auslagerungsdatei
erzeugt,
der
Schreibzugriff erfolgt nicht auf die
Originaldatei
dwMaximumSizeHigh Die maximale Größe der Datei in Bytes als 64-Bit-Zahl,
dwMaximumSizeLow aufgeteilt in zwei 32-Bit DWORD Parameter
lpName
Symbolischer Name des Dateiabbildungsobjektes, kann von
anderen Prozessen referenziert werden, um das Objekt
gemeinsam zu verwenden.
PAGE_READONLY
PAGE_READWRITE
PAGE_WRITECOPY
Beispiel:
HANDLE hMap =
CreateFileMapping( hFile,
NULL,
PAGE_READ_WRITE,
0x0,
0x100000,
NULL);
//
//
//
//
//
//
Datei-Handle aus Schritt 1
nicht benutzt
wir wollen Schreib- und Leserecht
Größe 1 MegByte
Objekt soll namenslos bleiben
10.2.3.2
Schritt 3: Abbilden des Dateiinhalts in den Adressraum
In diesem Schritt wird nun dem Betriebssystem mitgeteilt wie viel virtueller Speicher
belegt werden soll und ab welcher Position relativ zum Dateianfang die Daten in
diesen Speicherbereich eingeblendet werden sollen. Es wird also hier die „Ansicht“
des Dateiabbildungsobjektes im virtuellen Speicher definiert.
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
//
//
//
//
//
//
//
//
file-mapping object
to map into address space
access mode
high-order 32 bits
of file offset
low-order 32 bits of
file offset
number of bytes to map
Seite 221
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Erklärung der wichtigsten Parameter:
hFileMappingObject
dwDesiredAccess
dwFileOffsetHigh
dwFileOffsetLow
dwNumberOfBytesToMap
Handle des Dateiabbildungsobjektes (Rückgabewert
von CreateFileMapping)
Flags, die analog zu VirtualAlloc die Schutz- bzw.
Zugriffsattribute der virtuellen Speicherseiten festlegen
FILE_MAP_READ
nur Lesezugriff
FILE_MAP_WRITE bzw. Lese- und Schreibzugriff
FILE_MAP_ALL_ACCESS
FILE_MAP_COPY
Beim Schreiben auf die
Seite wird eine private
Kopie
in
der
Auslagerungsdatei
erzeugt, der Schreibzugriff
erfolgt nicht auf die
Originaldatei
Datei-Offset in Byte, Beginn der Abbildung relativ zum
Dateianfang. 64-Bit-Zahl, aufgeteilt in zwei 32-Bit
DWORD Parameter
Zahl der Bytes, die abgebildet werden sollen. Wenn
hier 0 übergeben wird, wird versucht, die ganze Datei
abzubilden.
Beispiel:
LPVOID lpMemoryMappedFile = MapViewOfFile(
hMap,
// Handle des Dateiabbildungsobjekts
// aus Schritt 2
FILE_MAP_ALL_ACCESS, // wir wollen vollen Zugriff
0x0,
0x100,
// die ersten 255 Byte wollen wir
// nicht abbilden
0x10000
// 64kByte sollen auf einmal eingeblendet
);
// werden
Nun kann auf die sichtbaren Daten der Datei wie auf „normalen“ Speicher zugegriffen
werden.
Seite 222
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.3.3
Schritt 4: Entfernen der Dateiansicht aus dem Adressraum
Wenn alle gewünschten Operationen mit dem eingeblendeten Bereich der Datei
beendet sind, muss die Ansicht aus dem Adressraum entfernt werden.
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
// address where mapped
// view begins
);
Der einzige Parameter ist der Zeiger auf die Basisadresse der Dateiansicht, die wir
im Schritt zuvor von MapViewOfFile erhalten haben.
Beispiel:
UnmapViewOfFile(lpMemoryMappedFile);
Beachten Sie: UnmapViewOfFile entfernt lediglich die Ansicht des
Dateiabbildungsobjektes aus dem Speicher und gibt den von ihr belegten virtuellen
Speicher frei. Das Objekt selbst ist immer noch gültig, d.h. man könnte jetzt sofort
eine neue Ansicht, z.B. mit anderem Offset, erzeugen.
10.2.3.4
Schritt 5 und 6: Schließen des Dateiabbildungsobjekts und des
Dateiobjekts
Immer wenn Sie von einer API-Funktion ein Handle gleich welcher Art erhalten
haben, müssen Sie es schließen, nachdem Sie es nicht mehr brauchen. Wir
beenden unsere Verwendung der Datei „Beispiel.dat“ als speicherbasierte Datei nun
also, indem wir zuerst das Handle des Dateiabbildungsobjektes und dann das der
Datei selbst schießen.
CloseHandle(hMap);
CloseHandle(hFile);
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 223
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.4 Win32 – Heaps
Die dritte Möglichkeit, virtuellen Speicher in eigenen Anwendungen einzusetzen, ist
die Verwendung von Heaps. Ein Heap ist ein Speicherbereich des virtuellen
Adressraumes, der insbesondere für die Speicherung von vielen kleinen Objekten
gedacht ist, z.B. Listen oder Bäume von Exemplaren kleiner Strukturen oder
Klassen.
Die Funktion malloc() und der Operator new arbeiten z.B. auf dem Heap und
verbergen damit den ziemlich großen Verwaltungsaufwand, den der Heapmanager
im Hintergrund zu bewältigen hat. Für jedes der vielen kleinen Speicherobjekte wird
nämlich protokolliert, wo im Speicher es liegt und wir groß es ist, um bei weiteren
Anfragen eine mehrfache Vergabe von Speicher zu verhindern.
Wen Sie aus der DOS oder UNIX-Programmierung kommen, wundern sie sich
vielleicht über den Plural des Wortes „Heaps“, denn in den meisten klassischen
Speichermodellen gibt es ein Heapsegment je Prozess, dass von allen Operationen,
die dynamisch Speicher allokieren, gemeinsam verwendet wird. Windows hatte
jedoch schon zu 16-Bit-Zeiten zwei Heaps, nämlich den lokalen Heap, der für jeden
Prozess angelegt wurde, und einem globalen Heap, der für alle Prozesse
gemeinsam zugänglich war.
Wegen der privaten virtuellen Adressräume, die Prozesse unter Win32 zugewiesen
bekommen, fällt diese Unterscheidung in der 32-Bit-Welt weg. Zwar gibt es noch die
alten Win16-API-Funktionen LocalAlloc(...) und GlobalAlloc(...), sie
sollen jedoch nur die Portierung von altem 16-Bit-Quellcode erleichtern. In
Wirklichkeit arbeiten beide Funktionen auf dem Standardheap, den jeder Win32Prozeß automatisch bekommt.
Über diesen Standardheap hinaus kann ein Prozess aber weitere Heaps anlegen.
Was ist der Vorteil von der Verwendung von mehreren Heaps ?
Stellen Sie sich folgendes Szenario vor: Eine Applikation verwaltet zwei verkettete
Listen, die eine besteht aus Elementen „A“ , die 100 Bytes groß sind, die andere
besteht aus Elementen „B“, die 48 Bytes groß sind. Zu einem bestimmten Zeitpunkt
ergibt sich für einen kleinen Ausschnitt des Heaps folgendes Bild:
B
B
A
B
B
A
B
Nun werden alle Objekte der Klasse B gelöscht.
A
A
Insgesamt wurden 240 Bytes freigegeben, und trotzdem kann in dem gezeigten
Speicherabschnitt kein neues Objekt der Klasse A erzeugt werden, weil nirgends 100
Bytes zusammenhängender Speicher vorhanden ist.
Würde jede der beiden Listen in ihrem “privaten” Heap angelegt werden, dann würde
jedesmal, wenn ein Objekt aus der Liste entfernt wird, eine Lücke entstehen, die
exakt so groß ist, dass ein neu erzeugtes Objekt hineinpaßt.
Das verbessert die Speicherausnutzung und verringert die mittlere Zugriffszeit auf
ein Element der Liste, weil Page-Faults weniger wahrscheinlich werden.
Seite 224
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
10.2.4.1
Erzeugen eines zusätzlichen Heaps
Eine Anwendung kann durch die Funktion HeapCreate(...) theoretisch beliebig
viele zusätzliche Heaps erzeugen.
HANDLE HeapCreate(
DWORD flOptions,
DWORD dwInitialSize,
DWORD dwMaximumSize
);
// heap allocation flag
// initial heap size
// maximum heap size
Der erste Parameter beinhaltet mehrere Flags, mit denen man die Eigenschaften des
Heaps beeinflussen kann, in der Regel wird hier aber 0 übergeben. Die beiden
folgenden Parameter bezeichnen die anfängliche und die maximale Größe des
Heaps. die Werte, die hier übergeben werden, werden auf ein Vielfaches der
Seitengröße aufgerundet. Der Rückgabewert ist das Handle auf den neu erzeugten
Heap. Über dieses Handle werden alle nun folgenden Zugriffe abgewickelt.
10.2.4.2
Allokieren von Speicher auf einem zusätzlichen Heap
Mit der Funktion HeapAlloc(...) kann Speicher auf einem zusätzlichen Heap
allokiert werden. Falls die Größe des allokierten Speichers nachträglich geändert
werden soll, geschieht dies durch Aufruf der Funktion HeapReAlloc(...)
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
DWORD dwBytes
);
// handle to the private heap block
// heap allocation control flags
// number of bytes to allocate
LPVOID HeapReAlloc(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem,
DWORD dwBytes
);
//
//
//
//
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
handle to a heap block
heap reallocation flags
pointer to the memory to reallocate
number of bytes to reallocate
Seite 225
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Beschreibung der Parameter:
hHeap
dwFlags
lpMem
(nur
HeapReAlloc)
dwBytes
HANDLE auf den privaten Heap (Rückgabewert von
HeapCreate())
HEAP_GENERATE_EXCEPTIONS Legt fest, dass
Heap(Re)Alloc
im
Fehlerfall
eine
Exception
werfen
soll, anstatt NULL
zurückzugeben
HEAP_NO_SERIALIZE
legt
fest,
dass
die
Schutzmechanismen für
die Threadsicherheit des
Aufrufs
umgangen
werden sollen (Vorsicht !)
HEAP_ZERO_MEMORY
legt fest, dass der
Speicher mit 0 initialisiert
werden soll
Zeiger auf den bereits bestehenden Block, der nun
bei reallokiert werden soll.
Größe des angeforderten Speicherblocks in Byte
Beachten sie, dass der Heap, auf den die Operationen angewandt werden sollen.
stets im ersten Parameter als Handle übergeben wird.
10.2.4.3
Freigabe eines Speicherbereichs
Mit der Funktion HeapFree(...) wird Speicher, der mit HeapAlloc(...) oder
HeapReAlloc(...) belegt wurde, wieder freigegeben.
BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem
);
// handle to the heap
// heap freeing flags
// pointer to the memory to free
Seite 226
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
Beschreibung der Parameter:
hHeap
dwFlags
lpMem
HANDLE auf den privaten Heap (Rückgabewert von
HeapCreate())
HEAP_NO_SERIALIZE
legt
fest,
dass
die
Schutzmechanismen für
die Threadsicherheit des
Aufrufs
umgangen
werden sollen (Vorsicht !)
Zeiger auf den bestehenden Block, der gelöscht werden soll.
10.2.4.4
Abbau eines zusätzlichen Heaps
Ein Heap, der mit HeapCreate(...) zusätzlich erzeugt wurde, kann mit der
Funktion HeapDestroy(...) wieder abgebaut werden:
BOOL HeapDestroy(
HANDLE hHeap
// handle to the heap
);
HeapDestroy(...) erhält als einzigen Parameter das Handle des Heaps.
CloseHandle(...) braucht hier nicht zusätzlich aufgerufen zu werden !
10.2.4.5
Verwendung von zusätzlichen Heaps in C++
Der Polymorphismus der Sprache C++ macht es besonders einfach, die Vorteile
privater Heaps auszunutzen. Stellen Sie sich vor, Sie sollten eine verkettete Liste
mit Elementen der Klasse CQuader verwalten. CQuader enthält als Attribute die
long – Werte m_nHoehe, m_nBreite, m_nLaenge. Aus den oben genannten
Gründen sollen alle Elemente der Liste in einem eigens dafür reservierten Heap
gespeichert werden.
Dafür führen wir die Klassenvariable ULONG sm_nCount und HHEAP sm_hHeap ein
und überschreiben die Operatoren new und delete der Klasse CQuader wie folgt:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 227
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Einführung in die Speicherverwaltung von Win32
// Datei Quader.h
class CQuader
{
public: // Attribute ------------------------------long m_nHoehe;
long m_nBreite;
long m_nLaenge;
private:
static ULONG sm_nCount;
static HHEAP sm_hHeap;
public: // Methoden ------------------------------void* operator new(size_t size);
void operator delete(void* pObj);
// weitere Methoden
}
// Datei Quader.cpp
CQuader:: sm_nCount = 0;
CQuader:: sm_hHeap = NULL;
void* CQuader::operator new(size_t size)
{
// Falls der Heap noch nicht erzeugt wurde -> HeapCreate()
if (sm_hHeap == NULL)
{
sm_hHeap = CreateHeap(0,0,0);
// prüfen, ob’s gutgegangen ist
if (sm_hHeap == NULL)
return NULL;
}
// Speicher auf dem privaten Heap belegen
LPVOID pObj = HeapAlloc(sm_hHeap, 0, size);
// Hat’s geklappt ?
if (pObj == NULL)
{
return NULL;
}
else
{
// mitzählen, wieviele Objekte erzeugt wurden
sm_nCount ++;
// gültigen Zeiger zurückgeben
return pObj;
}
}
void CQuader::operator delete(void* pObj()
{
if (HeapFree(sm_hHeap,0,pObj) != NULL)
{
// Objekt beseitigt -> Zähler dekrementieren
sm_nCount--;
}
if (sm_nCount == 0)
{
// kein Objekt mehr da -> Heap abbauen
if (HeapDestroy(sm_hHeap) != NULL)
{
sm_hHeap = NULL;
}
}
}
Seite 228
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11 Dynamische Bibliotheken
Modularität und Wiederverwendbarkeit werden gemeinhin als Errungenschaft der
objektorientierten Programmierung dargestellt. Tatsächlich aber bezieht sich dies nur
auf die Ebene des Quellcodes. In der Ebene des kompilierten Maschinencodes
waren Programmbibliotheken (also mehrfach verwendbare binäre Module) in der
Windows- wie in der Unix-Welt schon etabliert, als die objektorientierten Sprachen
noch keine Rolle spielten. Eine besondere Gattung dieser Programmbibliotheken
sind die dynamisch bindbaren Bibliotheken, die in der Windows-Welt normalerweise
die Dateiendung .dll (für dynamic link library) erhalten.
DLLs sind in gewissem Sinne Diensterbringer („Server“), denn eine DLL kann von
mehreren Dienstnehmern („Clients“) geladen werden, wodurch die Clients die
Funktionen benutzen können, ohne über die Details der Implementierung Bescheid
wissen zu müssen.
Windows selbst beruht auf diesem Prinzip, denn die wichtigsten Teile des
Betriebssystems befinden sich in DLLs, die zum größten Teil selbst wieder DLLs
laden.
11.1 Grundlagen
Eine DLL ist im wesentlichen eine Datei auf einem Datenträger, die globale Daten,
kompilierte Funktionen und Ressourcen enthält. Sie unterscheidet sich von einem
Programm dadurch, dass sie nicht selbst als eigenständiger Prozess geladen und
ausgeführt werden kann. Vielmehr muss ein Prozess die DLL laden, d.h. das
Betriebsystem auffordern, die DLL in seinen Adressraum einzublenden. Erst jetzt
kann der Code der DLL ausgeführt werden, indem der ladende Prozess (der ClientProzess) die Funktionen der DLL aufruft. Woher kennt nun der Client-Prozess die
Funktionen der DLL ?
Die DLL gibt die Funktionen, die von außen aufgerufen werden sollen, in einer sog.
Export-Tabelle bekannt. Der Client hält im Gegenzug eine Import-Tabelle bereit, in
der er vermerkt, welche Funktionen aus welcher DLL er benutzt. Beide Tabellen
enthalten dabei den symbolischen Namen der Funktion oder die Funktionen werden
durch eine laufende Nummer (Ordinalzahl) identifiziert. Beim Laden der DLL gleicht
das Betriebssystem die beiden Tabellen gegeneinander ab und wird, falls der Client
eine Funktion importieren möchte, die die DLL gar nicht exportiert, einen Fehler
melden.
Bemerkung: Unter Win32 kann eine DLL auch globale Daten exportieren, das
Datensegment wird aber dann für jeden Client-Prozess getrennt angelegt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 229
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Im Quelltext der DLL müssen Sie jede exportierte Funktion als solche kennzeichnen:
/*
allgemein:
__declspec(dllexport)<Rückgabewert><Funktionsname>(<Parameterliste>);
Beispiel:
*/
__declspec(dllexport) int MyFunction(int x, int y);
Sie können statt dessen auch die exportierten Funktionen in der Moduldefinitionsdatei auflisten, was aber normalerweise sehr viel aufwendiger ist.
Analog dazu müssen Sie auf der Client-Seite die Importe deklarieren:
/*
allgemein:
__declspec(dllimport)<Rückgabewert><Funktionsname>(<Parameterliste>);
Beispiel:
*/
__declspec(dllimport) int MyFunction(int x, int y);
Achtung: Wenn sie die DLL mit einem C++-Compiler übersetzen, wird dieser alle
Funktionsnamen in compilerspezifischer Art und Weise mit Namenserweiterungen
versehen („name decorating“), um gleichnamige Methoden verschiedener Klassen
und evtl. überladene Methoden auch symbolisch unterscheiden zu können. DLLs
deren Export-Tabelle aber dekorierte Namen enthält, kann nur noch von Clients
geladen werden, die ebenfalls in C++ geschrieben wurden, und evtl. sogar auf dem
selben Compiler übersetzt werden müssen. Sie können den C++-Compiler anweisen,
die C-üblichen Symbole ohne Dekoration zu erzeugen, in dem sie die
Compileranweisung extern “C“ voranstellen.
extern “C” __declspec(dllimport) int MyFunction(int x, int y);
extern “C” __declspec(dllexport) int MyFunction(int x, int y);
Noch ein Fallstrick: Die in C und C++ übliche Art der Parameterübergabe sieht vor,
dass die aufrufende Funktion die Parameter auf den Stack legt, und sie nach der
Rückkehr der Funktion zusammen mit dem Rückgabewert auch wieder
herunternimmt. Manche Client-Sprachen erwarten aber, dass die aufgerufene
Funktion die Parameter bereits herunternimmt, so dass der Aufrufer nur noch den
Rückgabewert vom Stack herunterholt. Diese letztere Aufrufkonvention hieß früher
„Pascal-Calling-Convention“ (Schlüsselwort PASCAL oder __pascal), wird aber
heute meist als „Standard-Calling-Convention“ bezeichnet (Schlüsselwort
__stdcall)
Seite 230
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11.2 Implizite und explizite Bindung
Beim Erstellen der DLL hat der Linker eine weitere Datei mit der Endung .lib
erzeugt. Diese kleine Bibliothek enthält nur die Exporttabelle und den Namen der
DLL, jedoch keinen Code. Sie fungiert als Platzhalter oder Ersatz („Surrogat“) für die
DLL während der Erstellung des Clients und muss deshalb als zusätzliche Bibliothek
beim Linker angemeldet werden. Später, wenn der Client-Prozess dann gestartet
wird, kann das Betriebssystem die DLL gleich beim Programmstart laden, denn alle
so gebundenen DLLs stehen ja in der Import-Tabelle der Clients. Dies bezeichnet
man als implizite Bindung.
Im Gegensatz dazu steht die explizite Bindung, bei der der Client mittels eines
Win32-API-Aufrufs die DLL an einer beliebigen Stelle während der Programmlaufzeit
nachlädt.
// explizites Binden einer DLL
// einen Funktionstyp definieren, der zu der gewünschten Funktion paßt
typedef int (FuncType)(int,int);
// ein HANDLE auf das DLL-Modul anlegen (Typ HINSTANCE oder HMODULE)
HINSTANCE hDLL;
// den Funktionszeiger anlegen
FuncType* pFunc;
// Laden der DLL
hDLL = ::LoadLibrary(“MyDLL.dll”);
// ermitteln der Adresse der gewünschten Funktion
pFunc = (FuncType*) ::GetProcAddress(hDLL,”MyFunction”);
// jetzt kann die Funtion int MyFunction(int,int); benutzt werden
int a = pFunc(4,18);
// [ ... ] weitere Anweisungen
// Freigeben der DLL nach Gebrauch
::FreeLibrary(hDLL);
Wie Sie sehen, ist die explizite Bindung viel aufwendiger, als die implizite und wird
deshalb entsprechen selten verwendet. Ein sinnvoller Grund für die Verwendung der
expliziten Bindung wäre aber z.B. eine Sprachumschaltung „on the fly“, also im
laufenden Programm, indem man alle Ressourcen und sprachabhängigen
Funktionen in mehreren DLLs bereithält, von der jeweils nur die eine explizit
gebunden wird, die zur eingestellten Sprache passt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 231
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11.3 Suchreihenfolge bei der dynamischen Bindung
Wenn Sie eine DLL mit ::LoadLibrary(...) explizit binden, können Sie als
Parameter den vollen Pfadnamen angeben. Wenn Sie, wie in dem Beispiel oben, nur
den Dateinamen angeben oder die implizite Bindung mit Hilfe der Surrogat-Bibliothek
verwenden, wird das Betriebssystem in der folgenden Reihenfolge nach Ihrer DLL
suchen:
1.
2.
3.
4.
5.
In dem Verzeichnis, das die Client-EXE enthält
Im aktuellen Arbeitsverzeichnis des Clients
Im Windows-Systemverzeichnis (X:\WinNT\system32\)
Im Windows-Verzeichnis (X:\WinNT\)
In den Verzeichnissen, die in der Umgebungsvariablen PATH angegeben sind
11.4 DLL-Typen
Der Microsoft Visual C++-Compiler kann in Verbindung mit der MFC vier
verschiedene Typen von DLLs erzeugen
1.
2.
3.
4.
Win32-DLLs ohne Verwendung der MFC
normale DLLs, die intern MFC verwenden und die MFC selbst statisch binden
normale DLLs, die intern MFC verwenden und die MFC selbst dynamisch binden
MFC-Erweiterungs-DLLs, die nicht nur Funktionen und Daten, sondern ganze
Klassen exportieren können. Hier wird die MFC immer dynamisch gebunden.
Beachten Sie, dass die Typen 2 und 3 zwar intern die MFC benutzen, aber nach
außen nur Funktionen im C-Stil exportieren können. Dieser Typ von DLL eignet sich
daher gut für die Verwendung in anderen Sprachen, wie z.B. Visual Basic.
11.5 Initialisieren einer DLL
Oft wird es vorkommen, dass Sie innerhalb Ihrer DLL einige Initialisierungsarbeit
vornehmen müssen, bevor die DLL verwendet werden kann. Diese, und das spätere
Aufräumen, geschieht je nach Typ der DLL an verschiedenen Stellen.
11.5.1 Initialisieren einer Win32-DLL
Der Linker legt automatisch die Funktion _DllMainCRTStartup als Einstiegspunkt
in die DLL fest. Diese Funktion wird von der CRTLib (C-Runtime-Library) zur
Verfügung gestellt und ruft nach der Initialisierung der globalen Daten die Funktion
DllMain auf. Eine Minimalversion von DllMain ist ebenfalls in der CRTLib
enthalten, wenn Sie jedoch Initialisierungen vornehmen wollen, müssen Sie Ihre
eigene DllMain schreiben.
extern “C” int APIENTRY DllMain ( HINSTANCE hInstance,
DWORD dwReason,
LPVOID lpReserved );
Seite 232
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
DllMain wird aus verschiedenen Gründen zu verschiedenen Zeitpunkten
aufgerufen, die Sie am Wert des zweiten Parameters dwReason voneinander
unterscheiden können. Nachfolgend sehen Sie das Grundgerüst der Funktion
DllMain mit den verschiedenen Werten von dwReason.
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason,
LPVOID lpReserved)
{
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
// Hier per-Prozeß-Initialisierung vornehmen
// wird beim ersten Laden der DLL ausgeführt
break;
case DLL_THREAD_ATTACH:
// Hier per-Thread-Initialisierung vornehmen
// wird beim Starten jedes Threads ausgeführt
break;
case DLL_THREAD_DETACH:
// Hier per-Thread-Aufräumarbeiten erledigen
// wird beim Beenden jedes Threads ausgeführt
break;
case DLL_PROCESS_DETACH:
// Hier per-Prozeß-Aufräumarbeiten erledigen
// wird beim Beenden des Prozesses ausgeführt
break;
}
return TRUE;
}
11.5.2 Initialisieren einer normalen DLL, die MFC verwendet
Die sog. „normalen“ DLLs, die auf der MFC aufbauen, haben als zentrales Objekt
eine Instanz einer von CWinApp abgeleiteten Klasse. In diesem Fall erfolgt die
Initialisierung deshalb am besten in InitInstance und das Aufräumen in
ExitInstance, zwei virtuelle CWinApp – Methoden, die Sie in Ihrer abgeleiteten
Klasse überschreiben sollten.
InitInstance und ExitInstance werden von der DllMain aufgerufen, die das
MFC-Anwendungsgerüst zur Verfügung stellt. Verändern Sie die MFC-Version von
DllMain nicht !
Wie innerhalb der InitInstance und ExitInstance Methoden threadspezifische
Initialisierungen vorgenommen werden können, ist in der Online-Hilfe der Funktion
TlsAlloc() beschrieben.
Achtung bei der dynamischen Bindung der MFC an die DLL: Weil in den meisten
Fällen sowohl die DLL als auch der Client-Prozess die MFC dynamisch binden, die
MFC aber intern globale Daten (den Modul-Zustand, AFX_MODULE_STATE )
verwaltet, muss als allererste Anweisung in jeder exportierten Funktion folgende
Zeile stehen:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 233
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11.5.3 Initialisierung einer MFC-Erweiterungs-DLL
MFC-Erweiterungs-DLLs sind selbst geschriebene binäre Klassenbibliotheken auf
Grundlage der MFC. Sie besitzen kein CWinApp-Objekt und müssen daher wie
Win32-DLLs in der Funktion DllMain initialisiert werden. Wenn Sie das DLL-Projekt
mit Hilfe des MFC-Anwendungsassistenten anlegen, erzeugt der Codegenerator
bereits die DllMain, die Sie nur noch Ihren Anforderungen anzupassen brauchen.
11.6 Beispiel 1: Eine normale DLL, die MFC verwendet
In diesem Beispiel soll eine DLL entwickelt werden, die einen dynamischen FIFO(first in first out) Datenpuffer realisieren soll. Die DLL soll folgende Schnittstelle
bieten:
BOOL AddElement
(void* lpElement);
void* GetElement();
void SetMaxQueueSize
(DWORD nMaxSize)
DWORD GetMaxQueueSize();
DWORD GetActualQueueSize();
Fügt einen Zeiger auf ein Heap-Objekt in
den FIFO ein. FALSE, wenn die maximale
Größe erreicht ist.
Gibt den Zeiger auf das nächste Element
in der Schlange zurück. NULL, wenn kein
Element mehr da ist.
Setzt die maximale Zahl an Elementen,
die in der FIFO-Schlange gespeichert
werden können
Gibt die maximale Länge der Schlange
zurück.
Gib die tatsächliche Länge der Schlange
zurück.
11.6.1 Erster Schritt: Anlegen des Projekts
Starten Sie den MFC-Anwendungsassistenten und geben Sie als Projekttyp „MFCAnwendungsassistent (dll)“ an.
Abbildung 78: Anlegen des DLL-Projekts
Seite 234
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Als nächstes werden Sie nach dem Typ der DLL gefragt. Wir wählen „normale DLL,
gemeins. MFC-DLL verwendend“.
Abbildung 79: Festlegen des DLL-Typs
Der Codegenerator erzeugt daraufhin den Rahmenquelltext für das DLL-Projekt mit
der Klasse CFifoApp, die von CWinApp abgeleitet ist.
Abbildung 80: Vom Codegenerator erstelltes Rahmenprojekt der DLL
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 235
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11.6.2 Zweiter Schritt: Deklarieren der exportierten Funktionen
In der in Abbildung 80 gezeigten Datei fifo.h fügen wir unten (nach der
Klassendeklaration) die Deklaration unserer exportierten Funktionen hinzu.
extern
extern
extern
extern
extern
"C"
"C"
"C"
"C"
"C"
__declspec(dllexport)
__declspec(dllexport)
__declspec(dllexport)
__declspec(dllexport)
__declspec(dllexport)
BOOL AddElement(void* lpElement);
void* GetElement();
void SetMaxQueueSize(DWORD nMaxSize);
DWORD GetMaxQueueSize();
DWORD GetActualQueueSize();
Intern soll unsere DLL die Daten in einer verketteten Liste von void-Zeigern
verwalten. Eine solche Liste ist in der MFC als CPtrList bereits implementiert. Die
Deklaration von CPtrList befindet sich in afxcoll.h, die wir in der Datei
stdafx.h zur #include-Liste hinzufügen müssen.
[ ... ]
#include <afxwin.h>
#include <afxext.h>
#include <afxcoll.h>
[ ... ]
// MFC-Kern- und -Standardkomponenten
// MFC-Erweiterungen
// Collection-Classes, eingefügt für CPtrList
11.6.3 Dritter Schritt: Implementation der FIFO-Queue-Klasse
Jetzt legen wir mit Hilfe des Klassenassistenten eine Klasse CQueue an, die wir von
CObject ableiten. In dieser Klasse legen wir als Membervariable eine CPtrList,
und zum Schutz vor konkurrierendem Zugriff, ein CMutex-Objekt m_mtx an. (Für
CMutex muss in stdafx.h die Zeile #include <afxmt.h> eingefügt werden.)
Außerdem wird in einem long-Wert die maximale Länge der Queue gespeichert.
Wir fügen der Klasse dann Member-Methoden hinzu, die wir für die Fifo-Anwendung
brauchen. In unserem Fall heißen die Methoden genauso wie die Funktionen, die die
DLL exportiert.
class CQueue : public CObject
{
public:
BOOL AddElement(void* lpElement);
void* GetElement();
void SetMaxQueueSize(DWORD nMaxSize);
DWORD GetMaxQueueSize();
DWORD GetActualQueueSize();
CQueue();
virtual ~CQueue();
private:
CPtrList m_List;
CMutex m_mtx;
long m_nMaxSize;
};
Seite 236
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Nachfolgend sehen Sie die Implementation der Methoden:
CQueue::CQueue()
{
m_nMaxSize = 10;
}
CQueue::~CQueue()
{
CSingleLock lck(&m_mtx,TRUE);
// Falls die Queue bei der Zerstörung nicht leer war: jetzt leeren
POSITION pos1, pos2;
void* pObj;
pos1 = m_List.GetHeadPosition();
while((pos2 = pos1) != NULL)
{
m_List.GetNext( pos1 );
pObj = m_List.GetAt( pos2 );
m_List.RemoveAt( pos2 );
delete pObj;
}
}
BOOL CQueue::AddElement(void* lpElement)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
CSingleLock lck(&m_mtx,TRUE);
try
{
if (m_List.GetCount() >= m_nMaxSize)
return FALSE;
m_List.AddTail(lpElement);
}
catch (CMemoryException e)
{
return FALSE;
}
}
void* CQueue::GetElement()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
CSingleLock lck(&m_mtx,TRUE);
if (m_List.IsEmpty())
return NULL;
else
return m_List.RemoveHead();
}
void CQueue::SetMaxQueueSize(DWORD nMaxSize)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
CSingleLock lck(&m_mtx,TRUE);
m_nMaxSize = nMaxSize;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 237
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
DWORD CQueue::GetMaxQueueSize()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
CSingleLock lck(&m_mtx,TRUE);
return m_nMaxSize;
}
DWORD CQueue::GetActualQueueSize()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
CSingleLock lck(&m_mtx,TRUE);
return m_List.GetCount();
}
11.6.4 Vierter Schritt: Implementation der exportierten Funktionen
Da die normale DLL nicht die ganze Klasse als solche exportieren kann, sondern nur
Funktionen, die wir der Kompatibilität mit anderen Sprachen wegen auch noch mit
export “C“ deklariert haben, müssen wir nun unsere exportierten Funktionen als
„Wrapper-Funktionen“ um die Klasse CQueue herum implementieren.
Wir fügen zuerst der Klasse CFifoApp ein Datenmember vom Typ CQueue* hinzu:
#include "Queue.h"
[ ...]
private:
CQueue* m_pQueue;
Dann überschreiben wir die virtuellen Funktionen InitInstance
ExitInstance der Klasse CWinApp in unserer abgeleiteten Klasse wie folgt:
und
BOOL CFifoApp::InitInstance()
{
m_pQueue = new CQueue;
if (NULL != m_pQueue)
return CWinApp::InitInstance();
else
return FALSE;
}
int CFifoApp::ExitInstance()
{
if (NULL != m_pQueue)
delete m_pQueue;
return CWinApp::ExitInstance();
}
Seite 238
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Jetzt implementieren wir die exportierten Wrapper-Funktionen so, dass sie einfach
die entsprechenden Funktionen des CQueue-Objekts aufrufen. theApp ist wie bei
einer MFC-Anwendung die globale Instanz der Klasse CFifoApp.
// exportierte Funktionen -------------------------------------------------BOOL AddElement(void* lpElement)
{
return theApp.m_pQueue->AddElement(lpElement);
}
void* GetElement()
{
return theApp.m_pQueue->GetElement();
}
void SetMaxQueueSize(DWORD nMaxSize)
{
theApp.m_pQueue->SetMaxQueueSize(nMaxSize);
}
DWORD GetMaxQueueSize()
{
return theApp.m_pQueue->GetMaxQueueSize();
}
DWORD GetActualQueueSize()
{
return theApp.m_pQueue->GetActualQueueSize();
}
11.6.5 Der Client
Als kleines Testprogramm soll eine dialogbasierte Anwendung dienen, die den FIFOPuffer benützt. Auf die Implementation der Anwendung FIFOClient1 soll hier nur kurz
eingegangen werden.
Abbildung 81: FIFOClient1.exe in Aktion
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 239
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Zuerst fügen wir in die Datei stdafx.h die Import-Deklarationen für unsere
Funktionen ein:
extern
extern
extern
extern
extern
"C"
"C"
"C"
"C"
"C"
__declspec(dllimport)
__declspec(dllimport)
__declspec(dllimport)
__declspec(dllimport)
__declspec(dllimport)
BOOL AddElement(void* lpElement);
void* GetElement();
void SetMaxQueueSize(DWORD nMaxSize);
DWORD GetMaxQueueSize();
DWORD GetActualQueueSize();
Nun müssen wir die Surrogat-Bibliothek beim Linker anmelden:
Abbildung 82: Aufnehmen der Surrogat-Bibliothek in die Linker-Liste
Die Message-Handler, in denen die Funktionen der DLL aufgerufen werden, sollen
nun kurz dargestellt werden.
Der Handler für den „hinzufügen“ – Knopf:
void CFIFOClient1Dlg::OnAdd()
{
UpdateData(TRUE);
// Den Inhalt der Variablen m_strText in die FIFO einfügen
int nBufferSize = m_strText.GetLength()+1;
char* lpstrText = new char[nBufferSize];
if (NULL == lpstrText)
return;
strncpy(lpstrText,m_strText.GetBuffer(nBufferSize),nBufferSize);
if (!AddElement(lpstrText))
AfxMessageBox("Das Hinzufügen zur FIFO-Queue ist fehlgeschlagen !");
m_dwActualCount = GetActualQueueSize();
UpdateData(FALSE);
}
Seite 240
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
Der Handler für den „ändern“-Knopf:
void CFIFOClient1Dlg::OnChangeMaxCount()
{
UpdateData(TRUE);
SetMaxQueueSize(m_dwMaxCount);
}
Der Handler für den „auslesen“-Knopf
void CFIFOClient1Dlg::OnReadOut()
{
char* lpszText;
while(NULL != (lpszText = (char*) GetElement()))
{
m_ListBox.AddString(lpszText);
delete lpszText;
}
m_dwActualCount = GetActualQueueSize();
UpdateData(FALSE);
}
11.7 Beispiel 2: Eine MFC-Erweiterungs-DLL
Wenn sicher ist, dass die Clients, die die DLL importieren sollen, selbst nur MFCAnwendungen oder DLLs sind, die die MFC ihrerseits dynamisch binden, dann kann
man die DLL als MFC-Erweiterungs-DLL schreiben. Diese Form der DLL hat den
Vorteil, dass sie nicht nur Funktionen und Daten exportieren kann, sondern auch
ganze C++-Klassen, die normalerweise von einer MFC-Klasse abgeleitet sind.
in diesem Beispiel soll gezeigt werden, wie man eine MFC-Klasse aus einem
bestehenden Projekt in eine DLL auslagert und schließlich das Projekt so abändert,
dass die Klasse aus der DLL geladen wird.
Wir bauen dazu auf der Anwendung „Primzahlen-Generator“ auf, die im Kapitel 9.3
beschrieben wurde.
Zuerst erzeugen wir ein neues DLL-Rahmenprojekt „PrimGen“, dann kopieren wir die
Dateien in das neue Projektverzeichnis und fügen die Klasse dem Projekt hinzu.
Schließlich ändern wir den Rest der Primzahlenanwendung so ab, dass sie die DLL
verwendet.
11.7.1 Schritt 1: Anlegen des Projekts „PrimeGen“
Dieser Schritt entspricht dem Anlegen des Projekts im ersten Beispiel. Lediglich beim
DLL-Typ geben sie „Erweiterungs-MFC-DLL“ an.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 241
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Dynamische Bibliotheken
11.7.2 Schritt 2: Einbinden und Anpassen der vorhandenen Klasse
Nachdem
Sie
die
Klassendateien
PrimeGenerator.h
und
PrimeGenerator.cpp in das Projektverzeichnis der MFC-Erweiterungs-DLL
„PrimeGen“ kopiert haben, fügen Sie die Dateien dem Projekt hinzu (Menü Projekt |
Dem Projekt hinzufügen | Dateien ... ). Dann ändern Sie die Klassendeklaration von
CPrimeGenerator wie folgt ab:
class AFX_EXT_CLASS CPrimeGenerator
{
[ ... ]
};
Das war’s schon! Wenn Sie jetzt das Projekt erstellen lassen, erzeugt der Linker eine
MFC-Erweiterungs-DLL PrimeGen.dll und die zugehörige Surrogat-Bibliothek
PrimeGen.lib.
11.7.3 Schritt 3: Anpassen des Client-Projekts
Kopieren Sie den ganzen Projektordner Multithreading1 um in einen Ordner mit
anderem Namen z.B. Multithreading1 mit DLL dann löschen Sie die Dateien
PrimeGenerator.h und PrimeGenerator.cpp, denn diese Klasse soll jetzt aus
der DLL importiert werden.
Nun kopieren Sie die veränderte PrimeGenerator.h aus dem DLL-Projekt in das
Client-Projektverzeichnis.
Der
Eintrag
der
nun
obsoleten
Datei
PrimeGenerator.cpp muss noch aus dem FileView entfernt werden (unter
„Source Files“ die Datei suchen und markieren, dann die Taste „Entf“ drücken).
Statt dessen muss wie im ersten Beispiel gezeigt, die Surrogat-Bibliothek in die
Linker-Liste aufgenommen werden.
Nun nur noch das Projekt neu erstellen. Fertig !
Seite 242
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
12 Entwicklung von Komponentensoftware mit MFC
12.1 Was ist Komponentensoftware ?
Die Technologie der Komponentensoftware ermöglicht eine Anwendung aus
einzelnen Softwarekomponenten zusammenzusetzen oder auch den Dienst, den
diese Komponenten zur Verfügung stellen, in anderen Anwendung wieder zu
verwenden. Wobei eine Softwarekomponente eine binäre Programmeinheit darstellt,
die über standardisierte Schnittstellen verfügt. Über die standardisierten
Schnittstellen wird ermöglicht, dass die Softwarekomponente in einer beliebigen
Programmiersprache erstellt werden kann. Einziges Kriterium, dass die
Programmiersprache erfüllen muss, ist die Fähigkeit die spezielle Schnittstelle
erzeugen zu können.
Die Softwarekomponente als binäre Programmeinheit, ist somit entweder eine
Dynamische Bibliothek oder ein eigenständiges ausführbares Programm. Für die
Anwendung selbst, welche den Dienst einer Softwarekomponente in Anspruch
nimmt, ist das binäre Format der Softwarekomponente ohne Bedeutung. Die
Softwarekomponente kann sogar, ohne irgendwelche Vorkehrungen in der
Anwendung selbst treffen zu müssen, auf einem anderen Rechner ausgeführt
werden.
12.2 Die Technologie
Die Technologien, die Microsoft zur Umsetzung der Komponentensoftware
bereitstellt, unterteilt sich in zwei Schichten. Die Basisschicht bildet das Component
Object Model, kurz COM. Auf die Basisschicht aufsetzend und als Basisschicht
erweiternde Schicht zu sehen, ist OLE (Object Linking and Embedding).
COM stellt die Architektur zur Verfügung, die eine Kommunikation zwischen
Anwendung und Softwarekomponente bzw. zwischen Softwarekomponente und
Softwarekomponente überhaupt ermöglicht. Bei einer verteilten Umgebung wird auch
von DCOM, Distributed COM, gesprochen. Die Verwendung von zwei Begriffen für
eine Technologie rührt daher, da COM in den Anfängen nur lokal verwendet werden
konnte. Eine Verteilung auf unterschiedliche Rechner wurde erst später ermöglicht,
als die Betriebssysteme von Microsoft entsprechend nachgerüstet bzw. erweitert
wurden. Um anzudeuten, dass es sich um die verteilte Variante von COM handelt,
kann nun der Begriff DCOM verwendet werden.
OLE, heute auch als ActiveX bezeichnet, erweitert die Kommunikation der binären
Programmeinheiten in vieler Hinsicht. Einige der bekanntesten OLE Services sind:
• OLE Clipboard
Unterstützt die allseits bekannten Operationen „Cut, Copy and Paste“.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 243
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
• OLE Drag and Drop
Maus basierender Datenaustausch zwischen Anwendungen.
• OLE Automation
Die Möglichkeit Methoden und Attribute von Objekten für andere Anwendungen
mittels einer späten Bindung zur Verfügung zu stellen. Sowie die Steuerung einer
Anwendung mit Hilfe von Makroskript Dateien.
• OLE Document
Ermöglicht das Erstellen von Verbunddokumenten. Wobei Verbunddokumente
neben Text verschiedene Elemente, wie Tabellenkalkulationen, Audiosamples,
Videoclips oder Grafiken beinhalten können.
• OLE Controls
Eigene Bedienelemente, wie Schaltflächen oder Eingabefelder, die in
Verbunddokumente, in die Benutzeroberfläche von Programmen oder in
Webseiten eingebracht werden können.
12.3 Grundlagen
12.3.1 Objekte
Eine Softwarekomponente setzt sich aus ein oder mehreren Objekten zusammen,
die letztendlich die Funktionalität der Komponente beinhalten. Wobei ein Objekte die
kleinste Einheit im Sinne der Komponentensoftware darstellt. Eine Anwendung,
welche die Dienste einer Softwarekomponente in Anspruch nimmt, kommuniziert
tatsächlich nicht mit Softwarekomponente, sondern mit den in der
Softwarekomponente enthaltenen Objekten.
Ein Objekt setzt sich zusammen aus ein oder mehreren standardisierten
Schnittstellen, auch Interfaces genannt n, sowie einem Kern, welcher den Code
enthält, der letztendlich die über die Schnittstellen nach Außen geführten Dienste des
Objektes ausführt.
Für die Darstellung eines Objektes wird eine spezielle Notation verwendet, siehe
Abbildung 83.
Objektkern
Interface
Abbildung 83: Objekt-Notation
Seite 244
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Wobei der Kreis mit durchgezogener Linie ein Interface des Objektes symbolisieren
soll und das Rechteck mit den abgerundeten Kanten den Objektkern darstellt.
Eine Softwarekomponente, die zwei Objekte enthält könnte dann folgendermaßen
dargestellt werden:
Softwarekomponente
Abbildung 84: Mögliche Darstellung einer Softwarekomponente
12.3.2 Eindeutige Identifizierung von Objekten und Interfaces mittels GUIDs
Um Objekte und Interfaces weltweit eindeutig identifizieren zu können, wird jedes
Objekt und jedes Interface mit einem GUID (Globally Unique Identifier) assoziiert.
Dabei handelt es sich um einen 128-Bit Wert, der weltweite Eindeutigkeit besitzt.
Spezielle Werkzeuge sind vorhanden, mit deren Hilfe man sich solch ein 128-Bit
Wert berechnen lassen kann. So zum Beispiel verfügt die Visual C++
Entwicklungsumgebung über die Tools guidgen.exe oder uuidgen.exe zum Erzeugen
von GUIDs, wobei guidgen.exe über eine grafische Benutzeroberfläche verfügt und
uuidgen.exe von der Kommandozeile aus gestartet wird.
Ein GUID sieht aus wie folgt:
fadb2910-9e70-11d2-9c69-b04705d05001
Ein GUID assoziiert mit einem Objekt wird auch als CLSID (Class Identifier)
bezeichnet.
Ein GUID assoziiert mit einem Interface wird als IID (Interface Identifier) bezeichnet.
Zudem wird ein GUID auch UUID (Universal Unique Identifier) genannt.
12.3.3 Das Interface, die standardisierte Schnittstelle
Um nun eine Sprachunabhängigkeit von Softwarekomponenten zu erzielen, muss die
Schnittstelle einem speziellen binären Format entsprechen. Die Schnittstelle setzt
sich zusammen aus dem Interface und einer Tabelle von Funktionszeigern. Das
Interface ist nichts anderes als ein Zeiger, der auf die Tabelle von Funktionszeiger
verweist und die in der Tabelle enthaltenen Funktionszeiger, verweisen schließlich
auf die im Objektkern implementierten Funktionen der Schnittstellendefinition,
Abbildung 85.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 245
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Interface
Zeiger auf Funktion1
Zeiger auf Funktion2
Implementierte
Funktionen 1-3
im Objektkern
Zeiger auf Funktion3
Abbildung 85: Aufbau der standardisierten Schnittstelle
Ein Client, sei es eine Anwendung oder auch eine Softwarekomponente, der die
Dienste eines Objektes verwenden möchte, kommuniziert mit dem Objekt nicht direkt
über das Interface, sondern mittels eines Zeigers, der auf das Interface zeigt. Der
Aufbau der Schnittstelle entspricht dem einer virtuellen Methodentabelle in C++. Aus
diesem Grund, bedarf es bei C++ keiner besonderen Vorkehrung zur Erstellung der
Schnittstelle, sondern es genügt lediglich eine abstrakte Klasse zu erstellen, die nur
rein virtuelle Methoden enthält. Des weiteren muss für die Methoden der Schnittstelle
die Aufrufkonvention __stdcall festgelegt werden. Das heißt, dass die aufgerufene
Funktion den Stack wieder aufräumt und nicht die aufrufende Funktion wie bei der
Standart-Aufrufkonvention von C/C++.
Durch die Verwendung von Interfaces kann Polymorphismus in der
Komponentensoftware im gleichen Sinne angewandt werden, wie bei
objektorientierten Programmiersprachen, da sich Interfaces von anderen Interfaces
ableiten lassen.
Es gilt als eine Festlegung, dass Interfaces von Objekten, welche sich bereits im
praktischen Einsatz befinden, nicht mehr verändert werden dürfen. Soll einem
bestehenden Interface eine Funktion hinzugefügt werden, so soll ein neues Interface
dem Objekt hinzugefügt werden, das die neue Funktion repräsentiert. Durch diese
Vorgehensweise wird sichergestellt, dass keine Versionskonflikte entstehen, da
ältere Clienten nach der Erweiterung eines Objektes immer noch das alte Format
eines ihnen bekannten Interfaces erwarten. Beispiel: Besitzt eine Komponente ein
Interface IMyInterface, das um eine zusätzliche Funktion ergänzt werden soll, so
muss die Komponente um ein komplett neues Interface, IMyInterface2, erweitert
werden.
12.3.4 Das Basisinterface IUnknown
IUnknown ist das Basisinterface, von dem sämtliche Interfaces abgeleitet werden
müssen. Die Besonderheit dieses Interfaces ist, dass es Methoden für die
Lebenszeitüberwachung von Objekten und für die Anfrage, ob ein Objekt ein
bestimmtes Interface besitzt, definiert.
Seite 246
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
IUnknown ist wie folgt aufgebaut:
class IUnknown
{
public:
virtual __stdcall HRESULT QueryInterface(REFIID iid,
void** ppvObj) = 0;
virtual __stdcall ULONG AddRef() = 0;
virtual __stdcall ULONG Release() = 0;
};
Für die Lebenszeitüberwachung verantwortlich sind die Methoden AddRef und
Release. Mittels AddRef wird innerhalb des Objekt ein Referenzzähler
inkrementiert. Dekrementiert wird dieser Referenzzähler durch einen Aufruf von
Release. Ist der Wert des Referenzzähler gleich Null, entfernt sich das Objekt aus
dem Arbeitsspeicher und muss bei Wiederverwendung neu instanziert werden. Die
Rückgabewerte von AddRef und Release sind jeweils der aktuelle Wert des
Referenzzähler.
Mit der Methode QueryInterface kann ein Objekt gefragt werden, ob es ein
bestimmtes Interface besitzt. Wobei der erste Parameter von QueryInterface die
IID des gewünschten Interfaces repräsentiert und der zweite Parameter, bei einer
erfolgreichen Anfrage, den Zeiger auf das gewünschte Interface enthält. Über den
Rückgabewert vom Typ HRESULT kann schließlich ausgewertet werden, ob der
Aufruf von QueryInterface erfolgreich war. Für die Auswertung sind spezielle
Makros vorgesehen, wie SUCCEEDED oder FAILED, deren Anwendung noch später
gezeigt wird.
Der Referenzzähler eines Objektes muss erhöht werden, sobald ein Client einen
Zeiger auf ein Interface erhält. Hat der Client keinen Bedarf mehr die Funktionalität
des Interfaces zu nutzen, liegt es in seiner Verantwortung den Referenzzähler des
Objektes durch einen Aufruf von Release wieder zu erniedrigen. Erfolgt im ClientCode eine Kopie eines Interfaces, liegt es ebenfalls in der Verantwortung des
Clients, den Referenzzähler um eins zu erhöhen. Demzufolge muss der
Referenzzähler auch wieder um eins erniedrigt werden, wenn die Kopie des
Interfaces an Bedeutung verliert.
Beispiel:
IUnknown
IMyInterface
*pUnk;
*pIMyInterface;
// Pseudo Funktion zum instanzieren des Objektes „MyObject“
CreateMyObject(&pUnk);
HRESULT hr;
// besitzt das Objekt überhaupt das Interface „IMyInterface“
hr=pUnk->QueryInterface(IID_IMyInterface, (void**)&pIMyInterface);
if (SUCCEEDED(hr))
{
// Aufruf der Interface-Funktionen von IMyInterface
pIMyInterface->MyFunction();
// MyInterface wird nicht mehr benötigt
pIMyInterface->Release();
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 247
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
12.3.5 Erzeugen von Objekten
Das Erzeugen von Objekten erfolgt wiederum über ein spezielles Objekt, das
Klassenfabrik genannt wird. Für jedes Objekt existiert eine Klassenfabrik, die für
dessen Instanzierung zuständig ist.
Eine Klassenfabrik wird in der Softwarekomponente implementiert, in der auch das
entsprechende Objekt erzeugt werden soll. Ein Client, erzeugt niemals ein Objekt
direkt, also mit new, was sowieso unmöglich ist, wenn Prozeßgrenzen zwischen
Client und Softwarekomponente liegen. Sondern er verwendet stets dazu eine
Klassenfabrik. Die Klassenfabrik muss, damit sie ihren Zweck erfüllen kann, neben
IUnknown auch noch über das Interface IClassFactory verfügen:
class IClassFactory : public IUnknown
{
public:
virtual __stdcall CreateInstance(IUnknown *pUnknown,
REFIID riid, void** ppv) = 0;
virtual __stdcall LockServer(BOOL fLock) = 0;
};
Wie schon aus den Namen der Methoden ersichtlich, dient CreateInstance zum
Instanzieren von Objekten. Der erste Parameter von CreateInstance, ein Zeiger
auf IUnknown, dient für Aggregation. Aggregation ist ein spezieller Mechanismus in
COM, um die Eigenschaften und Implementierung bereits bestehender Objekte
wiederverwenden zu können. Es soll ein Ersatz für die Vererbung darstellen, die in
COM nicht vorhanden ist. Der zweite Parameter ist die IID des Interfaces, das von
dem zu instanzierenden Objekt als erstes bezogen werden soll. Der letzte Parameter
enthält, falls das gewünschte Interface vorhanden ist, einen Zeiger auf jenes. Am
sichersten ist es bei der Erzeugung des Objektes stets einen Zeiger auf IUnknown,
als erstes zu beziehendes Interface zu verlangen, da man sicher gehen kann, dass
jedes Objekt dieses Interface besitzt bzw. besitzen muss.
Die Methode LockServer wird verwendet, um die Softwarekomponente im
Arbeitsspeicher zu halten, auch wenn momentan der Dienst von den enthalten
Objekten nicht gewünscht wird. Dadurch kann der Vorgang des Instanzieren von
weiteren Objekten beschleunigt werden. Ohne die Verwendung von LockServer
würde, falls die Softwarekomponente als Prozeß realisiert wäre, und die
Referenzzähler sämtlicher Objekte den Wert Null besitzen würden, der Prozeß
beendet werden. Neues Instanzieren eines Objektes in der Softwarekomponente
würde demzufolge das starten eines neuen Prozesses bedeuten.
Neben der Klassenfabrik müssen noch ein Einträge in der Windows-Registry
vorhanden sein, um COM mitzuteilen, in welcher ausführbaren Programmeinheit sich
das zu instanzierende Objekt befindet und wo diese Programmeinheit sich befindet.
Um beim Instanzieren innerhalb des Client-Codes die Verwendung der CLSID des
gewünschten Objektes zu vermeiden, kann die CLSID des Objektes mit einem String
assoziiert werden, was in den unteren Abbildungen dargestellt ist. So kann das
Objekt, dass die CLSID 7A5E3B51-9383-11D2-9C5F-BB4B45DC5501 besitzt
auch mittels des Strings „TGame.Document“identifiziert werden.
Seite 248
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Abbildung 86: Class Identifier der Klasse TGame.Document unter
HKEY_CLASSES_ROOT
Unter dem Schlüssel:
„HKEY_CLASSES_ROOT\CLSID\7A5E3B51-9383-11D2-9C5F-BB4B45DC5501“
der Windows-Registry wird dann letztendlich auf die binären Programmeinheit
verwiesen, in der das Objekt „TGame.Document“instanziert werden kann.
Abbildung 87: Verweis auf binäre Programmeinheit des Objektes unter
HKEY_CLASSES_ROOT\CLSID\7A5E3B51-9383-11D2-9C5F-BB4B45DC5501
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 249
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Nun zum Vorgang des Instanzierens eines Objektes:
5. Aufruf der Interface-Members
Client
1. Erzeuge Objekt
„TGame.Document“
Objekt
4. Übergabe des
Zeigers auf das
gewünschte
Interface
Klassenfabrik
Softwarekomp.
3. COM verwendet
Klassenfabrik, um
Objekt zu erzeugen
COM
2. Falls Softwarekomponente noch
nicht im Arbeitsspeicher, lokalisiert
Komponente und lädt sie in
Arbeitsspeicher
Windows-Registry
Abbildung 88: Instanzieren eines Objektes
Mit der entsprechenden CLSID des Objektes und der IID des zuerst gewünschten
Interfaces als Übergabeparameter, beispielsweise durch die COM API-Funktion
CoCreateInstance, äußert ein Client den Wunsch an COM ein Objekt zu
verwenden. Anhand der in CoCreateInstance übergebenen CLSID erhält COM
über die Einträge in der Windows-Registry die Informationen, wo sich die Objekt
enthaltende Komponente befindet und ob es sich um einen Prozeß oder um eine
dynamische Bibliothek handelt. Falls die Komponente in Form eines Prozesses
vorliegt, wird dieser Prozeß gestartet, im Falle einer dynamische Bibliothek, wird die
dynamische Bibliothek in den Adreßraum des Clients eingeblendet. Die Verbindung
von Klassenfabrik und Objekt wird beim Starten der Komponente COM
bekanntgegeben. Dieser Vorgang erfolgt innerhalb der Softwarekomponente. COM
verwendet schließlich die mit dem Objekt verbundene Klassenfabrik, um das Objekt
zu instanzieren und benutzt für diesen Vorgang das Interface IClassFactory der
Klassenfabrik. Präziser ausgedrückt, erfolgt die Instanzierung über die Member
Funktion CreateInstance des Interfaces IClassFactory mit der in
CoCreateInstance übergebenen IID. Der durch die IID gewünschte InterfaceZeiger wird nun abschließend an den Client übergeben, der letztendlich über diesen
Zeiger Member-Funktionen des Interfaces aufrufen kann. Werden Prozessgrenzen
überwunden, so schaltet COM noch eine Interprozesskommunikationsschicht
zwischen Client und Softwarekomponente, die auf Remote Procedure Call basiert.
Seite 250
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
12.4 Wiederverwendung durch Aggregation
Die Funktionalität von bestehenden Objekten kann von neu zu erstellenden Objekten
wieder verwendet werden. Dabei wird das Objekt, dessen Dienste wiederverwendet
werden sollen (auch als inneres Objekt bezeichnet) vom wieder verwendenden
Objekt (das äußere oder umschließende Objekt) aggregiert. Die Interfaces des
inneren Objektes werden dabei für Clienten als Interfaces des äußeren Objektes
sichtbar. Ein Client kann somit nicht mehr erkennen, zu welchem Objekt welches
Interface gehört Abbildung 89.
Äußeres Objekt
Inneres
Objekt
Interfaces des inneren
Objektes werden nach
außen gereicht
Abbildung 89: Wiederverwendung durch Aggeration
Im Falle einer Aggregation wird die Lebenszeitüberwachung und Interfaceanfrage
von innerem und äußerem Objekt innerhalb des äußeren Objektes zentralisiert. Dies
bedeutet, dass Aufrufe von AddRef, Release und QueryInterface innerhalb des
inneren Objektes stets in Aufrufe selbiger IUnknown-Funktionen des äußeren
Objektes enden. Das IUnknown-Interface des äußeren Objektes wird auch als
Controlling-Unknown bezeichnet, da über die Implementierung dieses Interfaces
entschieden werden kann, welche Interfaces von inneren Objekten außenstehenden
zur Verfügung gestellt werden (public) und welche nicht (private).
Ein Objekt das aggregierbar sein soll, muss dies auch programmtechnisch
unterstützen. Der zusätzliche Implementierungsaufwand ist jedoch geringfügig, siehe
Kapitel 12.5.5.
12.5 Implementieren eines COM-Objektes mit MFC
Als Beispiel, wie Objekte mit MFC implementiert werden, soll ein einfaches Objekt
dienen, dass neben IUnknown über ein einzelnes Interface verfügt. Das Objekt
bekommt den Namen TheObject und das Interface den Namen IMyInterface.
Bildlich dargestellt sieht TheObject folgendermaßen aus, Abbildung 90:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 251
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
IMyInterface
TheObject
IUnknown
Abbildung 90: TheObject
12.5.1 Definition des Interfaces
Zuallererst wird das Interface IMyInterface definiert:
#ifndef _IMyInterface_H
#define _IMyInterface_H
#include <afxwin.h>
#include <afxole.h>
class IMyInterface : public IUnknown
{
public:
virtual HRESULT __stdcall Function1() = 0;
};
#endif
Da sämtliche Interfaces von IUnknown abgeleitet werden müssen, bildet
IMyInterface auch keine Ausnahme. Function1 ist, neben den drei IUnknownMembers, die einzige Methode, die dieses Interface besitzt. Wichtig ist, dass die
Methode als virtual deklariert wird, um die binäre Schnittstelle zu erzeugen. „=0“
am Ende der Methodendeklaration deutet darauf hin, dass es sich um eine rein
virtuelle Methode handelt, die keine Implementierung besitzt. In C++ würde
IMyInterface als abstrakte Klasse bezeichnet werden, das heißt es repräsentiert
eine Klasse, von der keine Instanzen gebildet werden können.
Seite 252
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Neben der Definition der Schnittstelle wird auch noch deren IID festgelegt. Um die
CLSID und IID von Objekt und dessen Interface zentral zu halten, erfolgt die
Deklaration sowie die Implementierung der CLSID des Objektes TheObject und der
IID des Interfaces IMyInterface in der selben Header- und Quelltext-Datei. Dieses
Vorgehen ist nicht zwingend und kann individuell Gestaltet werden.
Header-Datei:
#ifndef _GUIDS_H
#define _GUIDS_H
#include <afxole.h>
extern const IID IID_IMyInterface;
extern const CLSID
CLSID_TheObject;
#endif
Quelltext-Datei:
#include "GUIDS.h"
const IID IID_IMyInterface = {
0x7fc50540,
0x9f13,
0x11d2,
{0x9c, 0x6a, 0xac, 0xc3, 0x48, 0xc2, 0x22, 0x01}};
const CLSID CLSID_TheObject = {
0x860e7a30,
0x9f16,
0x11d2,
{0x9c, 0x6a, 0xac, 0xc3, 0x48, 0xc2, 0x22, 0x01}};
Die GUIDs von Objekt und Interface wurden durch einen Aufruf von uuidgen –s
berechnet.
Im Code des Clients wird außer dieser Schnittstellendefinition und der GUIDs von
Interface und Objekt, nichts weiteres benötigt um den Dienst des Objektes nutzen zu
können. Dies beutet zudem, dass wenn sich Änderungen in Function1 ergeben
und der Client Code schon übersetzt wurde, dieser nicht neu Übersetzt werden
muss, sondern es genügt, solange die Schnittstellendefinition eingehalten wird, bei
Änderungen nur den Code der Softwarekomponente neu zu übersetzen.
12.5.2 Implementieren der Funktionalität des Objektes
Nachdem nun die Schnittstelle sowie die GUIDs definiert worden sind, muss die
Funktionalität des Objektes implementiert werden. Bei der Implementierung eines
Objektes muss unter Verwendung der MFC eine bestimmte Vorgehensweise
eingehalten werden.
Sämtliche Klassen, die Objekte implementieren, werden von der MFC Klasse
CCmdTarget abgeleitet. CCmdTarget besitzt bereits eine Implementierung der
Funktionen AddRef und Release des Interfaces IUnknown, so dass diese
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 253
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
verwendet werden können. Überdies spielt CCmdTarget eine entscheidende Rolle
bei der Automatisierung mit OLE, worauf später noch eingegangen wird. Soll ein
Objekt über Verbindungspunkte verfügen, besitzt CCmdTarget ebenfalls den nötigen
Code, um diese recht komfortabel zu implementieren. Verbindungspunkte werden
dann benötigt, wenn die Softwarekomponente Code des Clients aufrufen muss.
Neben der Ableitung von CCmdTarget werden sämtliche Interfaces, die das Objekt
nach außen gibt, als eingebettete Klassen der Objekt implementierenden Klasse
realisiert. Zur Veranschaulichung dieser Vorgehensweise soll folgender Code dienen,
wobei die Objekt implementierende Klasse noch nicht von CCmdTarget abgeleitet
ist:
class CTheObject : public IUnknown
{
public:
CTheObject();
STDMETHODIMP QueryInterface(REFIID iid, void** ppvObj);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// zentraler Referenzzähler
DWORD m_dwRef;
// eingebettete Klasse, die Funktionalität des Interfaces
// implementiert
class XMyInterface : public IMyInterface
{
public:
CTheObject *m_pParent;
STDMETHODIMP QueryInterface(REFIID iid, void** ppvObj);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG)Release();
STDMETHOD Function1();
} m_xMyInterface;
};
Die Klasse CTheObject implementiert vollständig die Funktion des Interfaces
IUnknown. In der Klasse XMyInterface werden die Aufrufe der IUnknownMembers einfach an die Klasse CTheObject mittels der Zeigers m_pParent
weitergeleitet. Das ‚X‘ in der Klassenbezeichnung von XMyInterface soll darauf
hindeuten, dass es sich hierbei um eine eingebettete Klasse handelt. Die Makros
STDMETHODIMP und STDMETHODIMP_(type) sind eine verkürzte Schreibweise für
HRESULT __export __stdcall für STDMETHODIMP und type __export
__stdcall für STDMETHODIMP_(type). Wobei __export bedeutet, dass die
Memberfunktion außerhalb des Moduls aufgerufen werden kann.
Die Verwendung der eingebetteten Klassen hat den Vorteil, dass die
Lebenszeitüberwachung und Interfaceabfrage an zentraler Stelle erfolgen kann, was
die Implementierung vor allem bei mehreren Interfaces erleichtert. Anhand der
Implementierung der obigen Deklaration kann diese Vorgehensweise nochmals
nachvollzogen werden:
CTheObject::CTheObject()
{
m_xMyInterface.m_pParent = this;
m_dwRef = 0;
}
Seite 254
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
ULONG CTheObject::AddRef()
{
return ++m_dwRef;
}
ULONG CTheObject::Release()
{
if (--m_dwRef == 0)
{
delete this;
return 0;
}
return m_dwRef;
}
HRESULT CTheObject::QueryInterface(REFIID iid, void** ppvObj)
{
if (iid == IUnknown)
{
ppvObj = this;
AddRef();
return NOERROR;
}
else if (iid == IID_IMyInterface)
{
ppvObj = &m_xMyInterface;
AddRef();
return NOERROR;
}
return ResultFromScode(E_NOINTERFACE);
}
ULONG CTheObject::XMyInterface::AddRef()
{
return m_pParent->AddRef();
}
ULONG CTheObject::XMyInterface::Release()
{
return m_pParent->Release();
}
ULONG CTheObject::XMyInterface::QueryInterface(REFIID iid,
void** ppvObj)
{
return m_pParent->QueryInterface(iid, ppvObj);
}
HRESULT CTheObject::XMyInterface::Function1()
{
// Do something
return S_OK;
}
Die MFC bietet zum einen CCmdTarget und zum anderen zahlreiche Makros, um
die Implementierung eines Objektes zu erleichtern bzw. zu vereinfachen. Die
Funktion von CCmdTarget ist bereits schon bekannt; nun zu den Makros:
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 255
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Tabelle 23: Makros für Interfaces
DECLARE_INTERFACE_MAP
BEGIN_INTERFACE_MAP
END_INTERFACE_MAP
INTERFACE_PART
BEGIN_INTERFACE_PART
END_INTERFACE_PART
METHOD_PROLOGUE
Muss im Zusammenhang mit den unteren beiden
Makros in die Header-Datei der Objekt-Klasse
eingefügt werden, ähnlich dem MessageMap-Makro
DECLARE_MESSAGE_MAP.
Werden in die Quelltext-Datei der Objekt-Klasse
eingefügt,
ähnlich
der
MessageMap-Makros
BEGIN_MESSAGE_MAP, END_MESSAGE_MAP.
Für jedes Interface, welches das Objekt
implementiert, muss dieses Makro zwischen
BEGIN_INTERFACE_MAP
und
END_INTERFACE_MAP eingefügt werden, um die
Interfaceanfragen mittels QueryInterface für die
unterstützten IIDs eines Objektes beantworten zu
können.
Werden
verwendet,
zur
Deklaration
einer
eingebetteten
Klasse,
die
ein
Interface
implementiert.
Dient zum Zugriff auf den Zeiger der Objekt-Klasse
innerhalb einer eingebetteten Klasse des Objektes.
Unter Verwendung der MFC für die Implementierung eines Objektes, kann der
Programmieraufwand erheblich verringert werden. Die Deklaration der Objekt
implementierenden Klasse von TheObject mit der Interface implementierenden
Klasse XMyInterface sieht nun aus wie folgt:
#ifndef _CTheObject_H
#define _CTheObject_H
#include
#include
#include
#include
<afxwin.h>
<afxole.h>
"IMyInterface.h"
"GUIDS.h"
class CTheObject : public CCmdTarget
{
public:
CTheObject();
virtual ~CTheObject();
protected:
DECLARE_DYNCREATE(CTheObject)
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(MyInterface, IMyInterface)
// Funktionen des Interfaces werden hier eingefügt,
// IUnknown Members wurden durch Makros bereits deklariert
STDMETHODIMP Function1();
END_INTERFACE_PART(MyInterface)
};
#endif
Seite 256
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Die Makros BEGIN_INTERFACE_PART sowie END_INTERFACE_PART erstellen eine
eingebettete Klasse deren Name sich zusammensetzt aus dem ersten Parameter
des Makros BEGIN_INTERFACE_PART und einem vorangestellten ‚X‘; somit X +
MyInterface
=
XMyInterface.
Der
zweite
Parameter
von
BEGIN_INTERFACE_PART ist das Interface, das XMyInterface implementieren
soll. Zudem wird durch diese Makro ein Attribut zur Klasse CTheObject vom Typ
XMyInterface hinzugefügt, dessen Bezeichnung sich im obigen Beispiel
zusammensetzt aus m_x + MyInterface = m_xMyInterface. Dieses Attribut findet
beispielsweise Verwendung in QueryInterface, um bei gegebener IID einen
Zeiger auf das entsprechende Interface liefern zu können.
Die Quelltext-Datei mit MFC:
#include "CTheObject.h"
#include "resource.h"
IMPLEMENT_DYNCREATE(CTheObject, CCmdTarget)
BEGIN_INTERFACE_MAP(CTheObject, CCmdTarget)
INTERFACE_PART(CTheObject, IID_IMyInterface, MyInterface)
END_INTERFACE_MAP()
CTheObject::CTheObject()
{
AfxOleLockApp();
}
CTheObject::~CTheObject()
{
AfxOleUnlockApp();
}
ULONG FAR EXPORT CTheObject::XMyInterface::AddRef()
{
METHOD_PROLOGUE(CTheObject, MyInterface)
return pThis->ExternalAddRef();
}
ULONG FAR EXPORT CTheObject::XMyInterface::Release()
{
METHOD_PROLOGUE(CTheObject, MyInterface)
return pThis->ExternalRelease();
}
HRESULT FAR EXPORT CTheObject::XMyInterface::QueryInterface(
REFIID iid, void FAR* FAR* ppvObj)
{
METHOD_PROLOGUE(CTheObject, MyInterface)
return (HRESULT)pThis->ExternalQueryInterface(&iid,
ppvObj);
}
HRESULT FAR EXPORT CTheObject::XMyInterface::Function1()
{
METHOD_PROLOGUE(CTheObject, MyInterface)
CDialog dlg(IDD_DIALOG1, NULL);
dlg.DoModal();
return S_OK;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 257
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
In
der
Quelltext-Datei
muss
nun
die
deklarierte
Interface-Map
(DECLARE_INTERFACE_MAP)
unter
Verwendung
der
Makros
BEGIN_INTERFACE_MAP,
END_INTERFACE_MAP
und
INTERFACE_PART
implementiert werden. Die Interface-Map realisiert letztendlich die IUnknownFunktion QueryInterface. Interfaces werden dieser Map mit Hilfe von
INTERFACE_PART hinzugefügt. Der erste Parameter von INTERFACE_PART ist der
Name der Klasse, in welcher die Interface implementierende Klasse eingebettet ist.
Als zweiten Parameter wird die IID des Interfaces dem Makro übergeben. Der dritte
und letzte Parameter ist der Name der Interface implementierenden Klasse selbst.
Trotz
der
vollständigen
Deklaration
der
IUnknown-Members
durch
BEGIN_INTERFACE_PART und END_INTERFACE_PART, müssen diese in der
Quelltext-Datei noch implementiert werden. Dabei ist das Vorgehen dafür relative
einfach. Man holt sich den Zeiger auf die umschließende Klasse mit Hilfe des Makros
METHOD_PROLOGUE und ruft anschließend die entsprechenden Methoden
ExternalAddRef, ExternalRelease oder ExternalQueryInterface der
Klasse CCmdTarget auf.
Zum Abschluß müssen noch die Interfacemethoden implementiert werden. Auch
wenn der Zeiger auf die umschließende Klasse nicht benötigt wird, sollte auf alle
Fälle METHOD_PROLOGUE an erster Stelle der Interfacemethode eingefügt werden,
wenn Aufrufe der MFC innerhalb der Interfacemethode verwendet werden.
Der Aufruf von AfxOleLockApp soll verhindern, dass die Objekt enthaltende
Anwendung nicht vorzeitig beendet werden kann wenn Objekte noch in Gebrauch
eines Clients sind. Diese Sperrung muss selbstverständlich wieder innerhalb des
Destruktors mit AfxOleUnlockApp aufgehoben werden.
12.5.3 Klassenfabrik des Objektes
Für die Klassenfabrik, welche die Instanzierung eines Objektes übernimmt, besitzt
die MFC eine Klasse namens COleObjectFactory. Da es sich bei einer
Klassenfabrik wiederum um ein Objekt handelt, ist auch COleObjectFactory von
CCmdTarget abgeleitet.
Neben dem instanzieren von Objekten besitzt COleObjectFactory auch
Methoden, um das assoziierte Objekt in die Windows-Registry einzutragen und die
Verbindung zwischen Objekt und Klassenfabrik COM mitzuteilen.
Das Objekt wird der Klassenfabrik
COleObjectFactory zugewiesen:
innerhalb
des
Konstruktors
von
COleObjectFactory( REFCLSID clsid,
CRuntimeClass* pRuntimeClass,
BOOL bMultiInstance, LPCTSTR lpszProgID );
Seite 258
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Der erste Parameter clsid ist die CLSID des Objektes, welches instanziert werden
soll. pRuntimeClass als zweiter Parameter ist der Zeiger, der auf die
Laufzeitinformation der Objekt implementierenden Klasse verweist. Er dient zum
Erzeugen einer Instanz der Objekt-Klasse. Die Laufzeitinformation wird bereitgestellt,
wenn in der Header-Datei der Klasse das Makro DECLARE_DYNCREATE und in der
Quelltext-Datei das Makro IMPLEMENT_DYNCREATE eingefügt wird. Über den dritten
Parameter kann gesteuert werden, ob für jedes erzeugte Objekt eine eigene Instanz
der Softwarekomponente gestartet werden soll. Beispielsweise befindet sich das
Objekt TheObject innerhalb eines Prozesses, so wird bei jedem Aufruf von
CoCreateInstance ein neuer Prozeß gestartet. Anhand des letzten Parameters
lpszProgID kann ein String angegeben werden, über den das zu erzeugende
Objekt, neben dessen CLSID, identifiziert werden kann, seihe Kapitel 12.3.2.
Das Eintragen des Objektes in die Windows-Registry erfolgt durch einen Aufruf von
COleObjectFactory::UpdateRegistryAll. Dabei handelt es sich um eine
statische Methode von COleObjectFactory, die für sämtliche Objekte innerhalb
der Komponente, welche mit einer Klassenfabrik verbunden wurden, die WindowsRegistry Eintragungen durchführt. Die Mitteilung sämtlicher Verbindungen zwischen
Objekt und Klassenfabrik innerhalb der Komponente an COM, erfolgt durch einen
Aufruf der ebenfalls statischen Methode COleObjectFactory::RegisterAll.
Die Verwendung von COleObjectFactory innerhalb einer Softwarekomponente,
welche als dynamische Bibliothek realisiert wurde sieht folgendermaßen aus:
CMainApp::CMainApp()
{
m_pFactoryTheObject = new OleObjectFactory(CLSID_TheObject,
RUNTIME_CLASS(CTheObject), TRUE, "TheObject.Class");
}
CMainApp::~CMainApp()
{
delete m_pFactoryTheObject;
}
BOOL CMainApp::InitInstance()
{
COleObjectFactory::RegisterAll();
return TRUE;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,
LPVOID* ppv)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllGetClassObject(rclsid, riid, ppv);
}
STDAPI DllCanUnloadNow(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllCanUnloadNow();
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 259
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
STDAPI DllRegisterServer(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (COleObjectFactory::UpdateRegistryAll());
return S_OK;
return E_FAIL;
}
Innerhalb des Konstruktors der Applikations-Klasse erfolgt die Verknüpfung von
Klassenfabrik und Objekt. Im obigen Falle wird dem Objekt TheObject eine
Klassenfabrik zugeordnet, die eine Instanz der MFC Klasse COleObjectFactory
ist. Beim Aufruf der Methode InitInstance, wird unter Verwendung der
COleObjectFactory Methode RegisterAll die Verbindung zwischen
TheObject und der Klassenfabrik COM bekanntgegeben.
Die drei exportierten Funktionen DllGetClassObject, DllCanUnloadNow und
DllRegisterServer werden von COM benötigt, falls es sich bei einer
Softwarekomponente um eine dynamische Bibliothek handelt. Der Aufruf von
AFX_MANAGE_STATE(AfxGetStaticModuleState()) zu
Beginn
dieser
Funktionen, muss vorhanden sein, wenn in einer exportierten Funktion einer
dynamischen Bibliothek, Aufrufe von Funktionen erfolgen, die auf den ModuleHandle der dynamischen Bibliothek zugreifen müssen. Dies ist der Fall zum Beispiel,
wenn eine exportierte Funktion einen Dialog aufruft, dessen Ressource sich
innerhalb des Moduls der DLL befindet. Würde der Aufruf von
AFX_MANAGE_STATE(AfxGetStaticModuleState()) fehlen, so würde die
Anwendung, zum Beispiel der DLL verwendende Prozeß, versuchen, die Ressource
des Dialoges innerhalb ihres eigenen Moduls zu lokalisieren.
Über DllGetClassObject besorgt sich COM einen Zeiger auf das Interface
IClassFactory der Klassenfabrik, die mit dem Objekt assoziiert wurde, dessen
CLSID in rclsid von DllGetClassObject übergeben wird. Der zweite Parameter
riid, besitzt in der Regel den Wert der IID von IClassFactory. Der letzte
Parameter von DllGetClassObject enthält, falls die Klassenfabrik gefunden
wurde, den Zeiger auf deren Interface IClassFactory. Mit diesem Interface-Zeiger
können nun Instanzen des Objektes gebildet werden. AfxDllGetClassObject
erledigt dabei vollständig, die Übergabe des Zeigers auf IClassFactory der
entsprechenden Klassenfabrik.
DllCanUnloadNow wird zyklisch von CWinApp::OnIdle der Anwendung
aufgerufen, um festzustellen, ob die dynamische Bibliothek aus dem Arbeitsspeicher
ausgeblendet werden kann. Die dynamische Bibliothek kann ausgeblendet werden,
wenn keine Objekte mehr in Gebrauch des Clients sind. Der Aufruf von
AfxOleLockApp
inkrementiert
einen
globalen
Zähler,
der
in
AfxDllCanUnloadNow überprüft wird. AfxOleUnlockApp dekrementiert diesen
Zähler wiederum. Besitzt dieser Zähler den Wert Null, so ist erst die Möglichkeit
gegeben, dass AfxDllCanUnloadNow den Wert S_OK zurück gibt und die
dynamische Bibliothek entladen werden kann.
Seite 260
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Wird die Verwendung von AfxOleLockApp(AfxOleUnlockApp) vergessen, kann
dies zu einem vorzeitigen entladen der DLL führen, obwohl Objekte vom Clienten
noch verwendet werden.
DllRegisterServer wird von Installationsprogrammen verwendet, um die in der
Softwarekomponente verfügbaren Objekte in die Windows-Registry einzutragen.
Deshalb der Aufruf von COleObjecFactory::UpdateRegistryAll an dieser
Stelle. Eines dieser Programme ist beispielsweise RegSvr32.exe, Bestandteil der
Windows-Betriebssysteme Windows 95/98 und Windows NT.
12.5.4 Verwendung des Objektes durch einen Client
Ein Client kann nun TheObject und dessen Interface IMyInterface verwenden,
wenn diesem die CLSID bzw. der mit der CLSID assoziierte String des Objektes
sowie die IID und Interfacebeschreibung von IMyInterface bekannt sind.
Die Verwendung von TheObject und IMyInterface innerhalb eines C++ Clients
kann somit implementiert werden wie folgt:
BOOL DisplayTheDialog()
{
IMyInterface *pMyInterface = NULL;
if (FAILED(CoCreateInstance(CLSID_TheObject, NULL,
CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER, IID_IMyInterface,
(void**)&pMyInterface)))
{
return FALSE;
}
// display the dialog of the software-component
pMyInterface->Function1();
pMyInterface->Release();
return TRUE;
}
Das instanzieren des Objektes erfolgt mit der COM API-Funktion
CoCreateInstance, die schon in Kapitel 12.3.5 beschrieben wurde. Ergänzend
sollen noch kurz die Bedeutung des zweiten und dritten Übergabeparameter dieser
Funktion erklärt werden. Der zweite Parameter, ein Zeiger auf ein Interface
IUnknown wird im Zusammenhang mit Aggregation verwendet. Falls die
Funktionalität eines Objektes innerhalb eines anderen Objektes wiederverwendet
werden soll, so muss hier nicht NULL sondern ein Zeiger auf IUnknown des
wiederverwendenden Objektes übergeben werden. Der dritte Parameter beschreibt
den Execution-Context. Hier kann über die Flags CLSCTX_INPROC_SERVER,
CLSCTX_LOCAL_SERVER oder CLSCTX_REMOTE_SERVER gesteuert werden, ob es
sich bei der Softwarekomponente um eine dynamische Bibliothek, um einen Prozeß
oder um eine entfernte Komponente auf einem anderen Rechner handeln soll. Die
bitweise
Oder-Verknüpfung
von
CLSCTX_INPROC_SERVER
und
CLSCTX_LOCAL_SERVER bedeutet, dass es gleichgültig ist, ob die
Softwarekomponente letztendlich als dynamische Bibliothek oder als Prozeß vorliegt.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 261
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
12.5.5 Aggregation mit MFC
Einziger Aufwand mit MFC, um ein Objekt aggregierbar zu machen, ist der Aufruf von
CCmdTarget::EnableAggregation() innerhalb des Konstruktors der Objekt
implementierenden Klasse. Bei der Implementierung eines äußeren Objektes sind
jedoch mehrere Schritte notwendig.
Ein Objekt das ein anderes aggregieren möchte muss dieses explizit instanzieren.
Die
Instanzierung
erfolgt
in
der
überschriebenen
Methode
BOOL
OnCreateAggregates() von CCmdTarget. Zudem muss ein Zeiger auf
IUnknown des aggregierten Objektes als Member der Objekt implementierenden
Klasse definiert werden, der während der Instanzierung des aggregierten Objektes
von CoCreateInstance übergeben wird. Dieser Zeiger ist der zweite Parameter
des Makros INTERFACE_AGGREGATE, das für die Eintragung der Aggregation
innerhalb der Interface-Map verwendet wird. Der erste Parameter von
INTERFACE_AGGREGATE ist der Name der Objekt implementierenden Klasse. Sollen
nur bestimmte Interfaces des inneren Objektes nach außen gereicht werden, so kann
dies durch Überschreibung der Methode GetInterfaceHook von CCmdTarget
erzielt werden.
Als Beispiel eines äußeren Objektes dient das Objekt TheObjectAgt.
TheObjectAgt verwendet als inneres Objekt TheObject. Zudem verfügt
TheObjectAgt über ein eigenes Interface namens IMyInterfaceAgt, siehe
Abbildung 91.
IMyInterfaceAgt
TheObjectAgt IUnknown
IUnknown
IMyInterface
TheObject
Abbildung 91: TheObjectAgt
Die Header-Datei der Objekt implementierenden Klasse von TheObjectAgt:
#ifndef _CTheObjectAgt_H
#define _CTheObjectAgt_H
#include
#include
#include
#include
<afxwin.h>
<afxole.h>
"IMyInterfaceAgt.h"
"GUIDS.h"
Seite 262
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
class CTheObjectAgt : public CCmdTarget
{
public:
CTheObjectAgt();
virtual ~CTheObjectAgt();
protected:
IUnknown
*m_pAggrInner;
virtual BOOL OnCreateAggregates();
DECLARE_DYNCREATE(CTheObjectAgt)
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(MyInterfaceAgt, IMyInterfaceAgt)
STDMETHODIMP Function1();
END_INTERFACE_PART(MyInterfaceAgt)
};
#endif
m_pAggrInner ist der Zeiger auf das innere Objekt TheObject.
Die Quelltext-Datei:
#include "CTheObjectAgt.h"
#include "resource.h"
IMPLEMENT_DYNCREATE(CTheObjectAgt, CCmdTarget)
BEGIN_INTERFACE_MAP(CTheObjectAgt, CCmdTarget)
INTERFACE_PART(CTheObjectAgt, IID_IMyInterfaceAgt, MyInterfaceAgt)
INTERFACE_AGGREGATE(CTheObjectAgt, m_pAggrInner)
END_INTERFACE_MAP()
CTheObjectAgt::CTheObjectAgt() : m_pAggrInner(NULL)
{
AfxOleLockApp();
}
CTheObjectAgt::~CTheObjectAgt()
{
if (m_pAggrInner) m_pAggrInner->Release();
AfxOleUnlockApp();
}
BOOL CTheObjectAgt::OnCreateAggregates()
{
CoCreateInstance(CLSID_TheObject, GetControllingUnknown(),
CLSCTX_INPROC_SERVER, IID_IUnknown,
(void**)&m_pAggrInner);
if (m_pAggrInner == NULL) return FALSE;
return TRUE;
}
ULONG FAR EXPORT CTheObjectAgt::XMyInterfaceAgt::AddRef()
{
METHOD_PROLOGUE(CTheObjectAgt, MyInterfaceAgt)
return pThis->ExternalAddRef();
}
ULONG FAR EXPORT CTheObjectAgt::XMyInterfaceAgt::Release()
{
METHOD_PROLOGUE(CTheObjectAgt, MyInterfaceAgt)
return pThis->ExternalRelease();
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 263
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
HRESULT FAR EXPORT CTheObjectAgt::XMyInterfaceAgt::QueryInterface(REFIID iid,
void FAR* FAR* ppvObj)
{
METHOD_PROLOGUE(CTheObjectAgt, MyInterfaceAgt)
return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj);
}
HRESULT FAR EXPORT CTheObjectAgt::XMyInterfaceAgt::Function1()
{
METHOD_PROLOGUE(CTheObjectAgt, MyInterfaceAgt)
CDialog dlg(IDD_DIALOG2, NULL);
dlg.DoModal();
return S_OK;
}
Bei der Instanzierung des inneren Objektes mit CoCreateInstance wird als zweiter
Parameter der COM API-Funktion ein Zeiger auf das Controlling-Unknown
übergeben. Wobei der Zeiger auf das Controlling-Unknown Interface über die
Methode GetControllingUnknown von CCmdTarget bezogen werden kann. Bei
erfolgreicher Instanzierung des inneren Objektes wird der Zeiger auf dessen
IUnknown-Interface in der Member-Variable m_pAggrInner gespeichert und TRUE
zurückgegeben. Schlägt die Instanzierung fehl, so ist der Rückgabewert von
OnCreateAggregates FALSE, was zur Folge hat, dass das äußere Objekt
ebenfalls nicht instanziert werden kann.
12.6 Zugriff auf Objekte mittels OLE Automation
OLE Automation ermöglicht den Zugriff auf Attribute sowie das Aufrufen von
Methoden eines Objektes mit Hilfe von Interpreter wie Visual Basic. Es wird vor allem
verwendet, um eine Anwendung anhand eines Visual Basic Skriptes zu steuern. So
zum Beispiel besitzen sämtliche Microsoft Office Produkte oder auch Msdev 5.0 die
Möglichkeit über Visual Basic eigene Programme zu schreiben, welche die Arbeit mit
diesen Produkten unterstützen oder immer wieder auftretende Arbeitsschritte – wie
das Einfügen eines bestimmten Textes an einer markierten Stelle und
anschließendes Speichern der Datei in einem bestimmten Verzeichnis – zu
automatisieren.
Die Basis für OLE Automation bildet das Interface IDispatch.
12.6.1 Das Interface IDispatch
Der Zugriff auf Objekte die OLE Automation unterstützen erfolgt über das Interface
IDispatch. Jedes Objekt, das IDispatch implementiert, kann mittels des späten
Bindens seine Funktionalität außenstehenden zur Verfügung stellen. Der
Mechanismus des späten Bindens bedeutet, dass erst zur Laufzeit entschieden wird,
welche Methode des Objektes aufgerufen oder welches Attribut des Objektes gesetzt
oder gelesen wird. Bei herkömmlichen Interfaces wird der Aufruf einer Methode von
einem Interface durch den Compiler in eine Aufrufanweisung des Offsets der
Methode bezüglich der Basis der virtuellen Methodentabelle des Interfaces übersetzt.
Für interpretierende Sprachen ist diese Vorgehensweise des frühen Bindens jedoch
unbrauchbar.
Seite 264
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Unter Verwendung von IDispatch erfolgt der Aufruf einer Methode über einen
numerischen Wert, der sogenannten dispID:
pDispInterface1
IDispatchMechanismus
DISPID 0
= Function1
DISPID 1
= Function2
CDispObject::
XDispInterface1::
Function1()
CDispObject::
XDispInterface1::
Function2()
Abbildung 92: spätes Binden durch IDispatch
Die dispID wird zusätzlich noch mit einem logischen Namen verknüpft, so dass die
Kenntnis der dispID einer Funktion oder eines Attributes nicht nötig ist. Innerhalb
von Visual Basic würde der Aufruf der Interfacefunktionen eines OLE Automation
unterstützenden Objektes durch:
Dim obj As Object
‘ instanzieren des Objektes, dessen CLSID durch “DispObject.Document” assoziiert
‘ wird
Set obj = CreateObject(„DispObject.Document“)
‘ Aufruf der Interfacefunctionen
obj.Function1
obj.Function2
erfolgen.
Die Methoden des Interfaces IDispatch werden verwendet, um die mit einer
dispID verknüpften Funktion aufzurufen, um die dispID einer Funktion anhand des
logischen Namens der Funktion zu beziehen und letztendlich um eine komplette
Beschreibung des Interfaces, wie Namen der enthaltenen Funktionen sowie Typ und
Anzahl derer Übergabeparameter bzw. Typ des Rückgabewertes, zu erhalten. Eine
Beschreibung des Interfaces kann aus einer Type Library extrahiert werden. Eine
Type Library, erzeugt vom Entwickler der Softwarekomponente, bietet detaillierte
Information über sämtliche enthaltene Objekte der Komponente. Sie wird entweder
als separate Datei ausgeliefert oder als Ressource der Komponente hinzugebunden.
Spezielle Entwicklungstools, wie midl.exe und die Interface Definition Language,
kurz IDL, der Visual C++ Entwicklungsumgebung stehen zur Verfügung um eine
Type Library zu erzeugen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 265
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Definition des Interfaces IDispatch:
class IDispatch : public IUnknown
{
public:
virtual HRESULT __stdcall GetTypeInfoCount(unsigned int
*pctinfo) = 0;
virtual HRESULT __stdcall GetTypeInfo(unsigned int itinfo, LCID
lcid, ITypeInfo **pptinfo) = 0;
virtual HRESULT __stdcall GetIDsOfName(REFIID riid, OLECHAR
**rgszNames, unsigned int cNames,
LCID lcid, DISPID *rgdispid) = 0;
virtual HRESULT __stdcall Invoke(DISPID dispID, REFIID riid,
LCID lcid, unsigned short wFlags,
DISPPARAMS *pDispParams,
VARIANT *pVarResult,
EXCEPINFO *pExcepInfo,
unsigned int *puArgErr) = 0:
};
Statt einer kompletten Beschreibung des Interfaces IDispatch soll hier nur eine
kurze Beschreibung der Interfacemethoden erfolgen, was bei der Verwendung der
MFC zur Implementierung und zur Verwendung eines OLE Automation
unterstützenden Objektes auch völlig ausreicht.
Invoke
GetIDsOfNames
GetTypeInfoCount
GetTypeInfo
Anhand einer dispID und einer Liste von
Übergabeparameter werden mittels Invoke die Methoden
des Objektes aufgerufen bzw. man erhält Zugriff auf die
Attribute des Objektes.
Konvertiert die Namen der Attribute und Methoden des
Objektes in die entsprechende dispID.
Entscheidet ob eine Beschreibung des Interfaces vorliegt
(*pctinfo = 1) oder nicht (*pctinfo = 0).
Bezieht die Interfacebeschreibung des DispatchInterfaces.
Zu bemerken ist, dass der Zugriff auf Attribute eines Objektes bei OLE Automation
nicht direkt erfolgt, sondern stets über Set- und Get-Methoden. In Visual Basic wird
jedoch die Verwendung der Set- und Get-Methoden verborgen, so dass dort der
Eindruck des direkten Zugriffes auf die Attribute des Objektes entsteht. Bei der
Implementierung eines Objektes müssen für Attribute, die außenstehenden zur
Verfügung stehen sollen, je ein Set-/Get-Methodenpaar bereitgestellt werden.
12.6.2 Datentypen von OLE Automation
Mit OLE Automation ist die Verwendung von speziell definierten Datentypen in
Methoden und Attributen, auf die von außen zugegriffen werden soll, verbunden.
Ein String ist in OLE Automation generell vom Typ BSTR (Basic STRing). Die
Besonderheit eines BSTR ist, dass er neben einem Zeiger auf eine Zeichenkette, die
Anzahl der Zeichen des Strings als Präfix besitzt. Die Zeichenkette ist zudem ein
Seite 266
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Unicode String. Ein Zeichen eines Unicode Strings besitzt statt einem Byte eine
Länge von zwei Byte. Die Verwendung von Unicode ermöglicht aufgrund seiner
Größe von 16-Bit, sämtliche Zeichensätze der Weltsprachen zu unterstützen.
Arrays müssen vom Typ SAFEARRAY sein. Wobei ein SAFEARRAY eine Struktur ist,
die neben einer Referenz auf die Daten des Arrays noch zusätzliche Felder für die
Größe des Arrays und der Größe der Elemente des Arrays besitzt.
Die Rückgabewert und Übergabeparameter der Methoden müssen vom Typ
VARIANT sein. Ein VARIANT ist eine Struktur, die eine Union beinhaltet, die
Datenfelder für jeden OLE Automation Datentyp besitzt:
typedef struct tagVARIANT
{
VARTYPE
vt; // identifiziert den Typ
unsigned short wReserved1;
unsigned short wReserved2;
unsigned short wReserved3;
union
{
// by value
short
iVal;
long
lVal;
float
fltVal;
double
dblVal;
VARIANT_BOOL bool;
SCODE
scode;
CY
cyVal; // Währung
DATE
date;
BSTR
bstrVal;
IUnknown*
punkVal;
IDispatch*
pdispVal;
SAFEARRAY*
parray;
// by reference
short*
piVal;
long*
plVal;
float*
pfltVal;
double*
pdblVal;
VARIANT_BOOL*pbool;
SCODE*
pscode;
CY*
pcyVal;
DATE*
pdate;
BSTR*
pbstrVal;
IUnknown**
ppunkVal;
IDispatch** ppdispVal;
VARIANT*
pvarVal;
void*
byref;
};
};
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 267
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Das Datenfeld VARTYPE der Struktur VARIANT identifiziert letztendlich den in der
Union enthaltenen Datentyp. Für die Identifikation existieren spezielle Bezeichner der
VARENUM Enumeration. Die Bezeichner sind (Kommentar aus einer OLE – HeaderDatei):
/*
* VARENUM usage key,
*
* * [V] - may appear in a VARIANT
* * [T] - may appear in a TYPEDESC
* * [P] - may appear in an OLE property set
* * [S] - may appear in a Safe Array
* * [C] - supported by class _variant_t
*
*
* VT_EMPTY
[V]
[P]
nothing
* VT_NULL
[V]
[P]
SQL style Null
* VT_I2
[V][T][P][S][C] 2 byte signed int
* VT_I4
[V][T][P][S][C] 4 byte signed int
* VT_R4
[V][T][P][S][C] 4 byte real
* VT_R8
[V][T][P][S][C] 8 byte real
* VT_CY
[V][T][P][S][C] currency
* VT_DATE
[V][T][P][S][C] date
* VT_BSTR
[V][T][P][S][C] OLE Automation string
* VT_DISPATCH
[V][T][P][S][C] IDispatch *
* VT_ERROR
[V][T]
[S][C] SCODE
* VT_BOOL
[V][T][P][S][C] True=-1, False=0
* VT_VARIANT
[V][T][P][S]
VARIANT *
* VT_UNKNOWN
[V][T]
[S][C] IUnknown *
* VT_DECIMAL
[V][T]
[S][C] 16 byte fixed point
* VT_I1
[T]
signed char
* VT_UI1
[V][T][P][S][C] unsigned char
* VT_UI2
[T][P]
unsigned short
* VT_UI4
[T][P]
unsigned short
* VT_I8
[T][P]
signed 64-bit int
* VT_UI8
[T][P]
unsigned 64-bit int
* VT_INT
[T]
signed machine int
* VT_UINT
[T]
unsigned machine int
* VT_VOID
[T]
C style void
* VT_HRESULT
[T]
Standard return type
* VT_PTR
[T]
pointer type
* VT_SAFEARRAY
[T]
(use VT_ARRAY in VARIANT)
* VT_CARRAY
[T]
C style array
* VT_USERDEFINED
[T]
user defined type
* VT_LPSTR
[T][P]
null terminated string
* VT_LPWSTR
[T][P]
wide null terminated string
* VT_FILETIME
[P]
FILETIME
* VT_BLOB
[P]
Length prefixed bytes
* VT_STREAM
[P]
Name of the stream follows
* VT_STORAGE
[P]
Name of the storage follows
* VT_STREAMED_OBJECT
[P]
Stream contains an object
* VT_STORED_OBJECT
[P]
Storage contains an object
* VT_BLOB_OBJECT
[P]
Blob contains an object
* VT_CF
[P]
Clipboard format
* VT_CLSID
[P]
A Class ID
* VT_VECTOR
[P]
simple counted array
* VT_ARRAY
[V]
SAFEARRAY*
* VT_BYREF
[V]
void* for local use
*/
Seite 268
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
12.6.3 Implementieren eines OLE Automation Objektes mit MFC
Klassen, die OLE Automation fähige Objekte implementieren, müssen ebenfalls von
CCmdTarget abgeleitet werden, da diese Klasse der MFC eine Implementierung des
Interfaces IDispatch unterstützt. Speziell auf die Automatisierung einer
Anwendung bezogen, wird unter Verwendung der Document/View Architektur das
Dokument als OLE Automation unterstützendes Objekt realisiert. CDocument, die
Basisklasse eines Dokuments ist zudem direkt von CCmdTarget abgeleitet.
Unter Verwendung der Document/View Architektur soll die Implementierung eines
OLE Automation unterstützendes Objektes gezeigt werden.
Das OLE Automation unterstützende Objekt bzw. das Dokument DispObject
besitzt das Dispatch-Interface IMyDispInterface. Über dieses Interface können
die zwei Attribute FigureOne sowie FigureTwo des Objektes DispObject gesetzt
und gelesen werden. Zusätzlich verfügt IMyDispInterface über die Methode
Product, die das Produkt der Attribute FigureOne und FigureTwo bildet und das
Ergebnis als Rückgabewert liefert.
Zuerst werden wieder die benötigten GUIDs für das Objekt und das Interface
definiert.
Header-Datei:
#ifndef _GUIDS_H
#define _GUIDS_H
#include <afxole.h>
extern const CLSID
CLSID_DispObject;
extern const IID IID_IMyDispInterface;
#endif
Quelltext-Datei:
#include "GUIDS.h"
const CLSID CLSID_DispObject = {
0xb970f6f0,
0xa3ed,
0x11d2,
{0x9c, 0x6f, 0xf7, 0xb1, 0x30, 0xf5, 0x03, 0x01}};
const IID IID_IMyDispInterface = {
0x8deb2630,
0xa3f4,
0x11d2,
{0x9c, 0x6f, 0xf7, 0xb1, 0x30, 0xf5, 0x03, 0x01}};
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 269
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Nun zur Deklaration der Objekt implementierenden Klasse:
#ifndef _CDispDocument_H
#define _CDispDocument_H
#include <afxwin.h>
class CDispDocument : public CDocument
{
protected:
CDispDocument();
DECLARE_DYNCREATE(CDispDocument)
public:
virtual BOOL OnNewDocument();
virtual ~CDispDocument();
// functionaltity of document ...............
double m_FigureOne, m_FigureTwo;
afx_msg double Product();
// ..........................................
protected:
DECLARE_DISPATCH_MAP()
DECLARE_INTERFACE_MAP()
};
#endif
Da die Document/View Architektur verwendet wird, ist nun diese Klasse nicht direkt
von CCmdTarget sondern von CDocument abgeleitet. Die Funktionalität des
Dokumentes wird repräsentiert durch die Attribute m_FigureOne und
m_FigureTwo, sowie durch die Methode Product. Diese Attribute und Methode
werden über OLE Automation außenstehenden zur Verfügung gestellt, was aber erst
in der Quelltext-Datei des Dokuments ersichtlich wird.
Das Makro DECLARE_DISPATCH_MAP muss im Zusammenhang mit den Makros
BEGIN_DISPATCH_MAP und END_DISPATCH_MAP in der Deklaration der Objekt
implementierenden Klasse eingebracht werden. Da das Objekt neben IUnknown
über das Dispatch-Interface IMyDispInterface verfügt, muss dafür wieder eine
Interface-Map erzeugt werden, die bei einem QueryInterface mit der IID
IID_IMyDispInterface einen Zeiger auf ein IDispatch Interface zurück gibt.
Deshalb das Makro DECLARE_INTERFACE_MAP. Das Interface IDispatch muss
nicht speziell deklariert werden, da dies schon in CCmdTarget erfolgte.
Quelltext-Datei der Objekt implementierenden Klasse:
#include "CDispDocument.h"
#include <afxole.h>
#include "GUIDS.h"
IMPLEMENT_DYNCREATE(CDispDocument, CDocument)
BEGIN_DISPATCH_MAP(CDispDocument, CDocument)
DISP_PROPERTY(CDispDocument, "FigureOne", m_FigureOne,
VT_R8) // = double Wert
DISP_PROPERTY(CDispDocument, "FigureTwo", m_FigureTwo,
VT_R8) // = double Wert
DISP_FUNCTION(CDispDocument, "Product", Product, VT_R8,
VTS_NONE)
END_DISPATCH_MAP()
Seite 270
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
BEGIN_INTERFACE_MAP(CDispDocument, CDocument)
// 3. Parameter von INTERFACE_PART „Dispatch“ ist Member von CCmdTarget
INTERFACE_PART(CDispDocument, IID_IMyDispInterface,
Dispatch)
END_INTERFACE_MAP()
CDispDocument::CDispDocument():
m_FigureOne(0.),
m_FigureTwo(0.)
{
// Existiert eine Dispatch-Map, so muss ein Aufruf von
// CCmdTarget::EnableAutomation() innerhalb des
// Konstruktors erfolgen
EnableAutomation();
AfxOleLockApp();
}
CDispDocument::~CDispDocument()
{
AfxOleUnlockApp();
}
BOOL CDispDocument::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
return TRUE;
}
double CDispDocument::Product()
{
return m_FigureOne * m_FigureTwo;
}
In der Quelltext-Datei wird eingeschlossen in BEGIN_DISPATCH_MAP und
END_DISPATCH_MAP festgelegt, auf welche Attribute und Methoden mittels OLE
Automation zugegriffen werden kann. Der erste Parameter des Makros
BEGIN_DISPATCH_MAP ist der Name der Objekt implementierenden Klasse, der
zweite Parameter ist der Name der Vaterklasse dieser Klasse. Welche Attribute nun
von außen zugänglich sind wird mittels des Makros DISP_PROPERTY festgelegt. Für
Methoden erfolgt dies durch das Makro DISP_FUNCTION. Der erste Parameter der
Makros DISP_PROPERTY sowie DISP_FUNCTION ist der Name der Objekt
implementierenden Klasse. Der zweite Parameter beschreibt den Namen unter
welchem das Attribut bzw. die Methode referenziert werden kann. Die dispID wird
dabei von der MFC automatisch festgelegt. Der erste Eintrag in der Dispatch-Map
erhält den Wert Eins, der zweite Eintrag den Wert Zwei, der dritte den Wert Drei; also
inkrementelles fortsetzen der dispIDs für weitere Einträge. Der dritte Parameter der
beiden Makros, referenziert schließlich auf das Attribut bzw. auf die Methode der
Objekt implementierenden Klasse, die letztendlich das OLE Automation Attribut oder
die OLE Automation Methode implementieren. Der vierte und letzte Parameter des
Makros DISP_PROPERTY beschreibt den Datentyp des Attributes. Wobei diese
Beschreibung aus der VARENUM Enumeration entnommen werden kann, siehe
Kaptitel 12.6.2. Der vierte Parameter von DISP_FUNCTION beschreibt den Typ des
Rückgabewertes, ebenfalls als Element von VARENUM. Der letzte Parameter von
DISP_FUNCTION ist eine durch Whitespace getrennte Liste von Typ-Bezeichnern
der Übergabeparameter der OLE Automation Funktion. Wobei die Bezeichner
Konstanten der VTS_constants sein müssen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 271
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Auszug der VTS_constants aus AFXDISP.h:
// parameter types: by value VTs
#define VTS_I2
"\x02"
#define VTS_I4
"\x03"
#define VTS_R4
"\x04"
#define VTS_R8
"\x05"
#define VTS_CY
"\x06"
#define VTS_DATE
"\x07"
#define VTS_BSTR
"\x08"
#define VTS_DISPATCH
"\x09"
#define VTS_SCODE
"\x0A"
#define VTS_BOOL
"\x0B"
#define VTS_VARIANT
"\x0C"
#define VTS_UNKNOWN
"\x0D"
// parameter types: by reference VTs
#define VTS_PI2
"\x42"
#define VTS_PI4
"\x43"
#define VTS_PR4
"\x44"
#define VTS_PR8
"\x45"
#define VTS_PCY
"\x46"
#define VTS_PDATE
"\x47"
#define VTS_PBSTR
"\x48"
#define VTS_PDISPATCH
"\x49"
#define VTS_PSCODE
"\x4A"
#define VTS_PBOOL
"\x4B"
#define VTS_PVARIANT
"\x4C"
#define VTS_PUNKNOWN
"\x4D"
//
//
//
//
//
//
//
//
//
//
//
a 'short'
a 'long'
a 'float'
a 'double'
a 'CY' or 'CY*'
a 'DATE'
an 'LPCOLESTR'
an 'IDispatch*'
an 'SCODE'
a 'BOOL'
a 'const VARIANT&'
// or 'VARIANT*'
// an 'IUnknown*'
//
//
//
//
//
//
//
//
//
//
//
//
a 'short*'
a 'long*'
a 'float*'
a 'double*'
a 'CY*'
a 'DATE*'
a 'BSTR*'
an 'IDispatch**'
an 'SCODE*'
a 'VARIANT_BOOL*'
a 'VARIANT*'
an 'IUnknown**'
Durch die Makros DISP_PROPERTY und DISP_FUNCTION wird von der MFC eine
Unmenge an Arbeit abgenommen, um auf Attribute und Methoden über OLE
Automation von außen zugreifen zu können. Das verpacken der Übergabeparameter
bzw. der Rückgabewerte in eine VARIANT Struktur ist nur ein kleiner Teil davon.
Neben DISP_PROPERTY und DISP_FUNCTION existieren noch weitere Makros für
selbigen Zweck, die noch kurz erläutert werden:
DISP_PROPERTY_EX( theClass, pszName, memberGet, memberSet,
vtPropType )
Wird verwendet, um zum Lesen und Setzen eines Attributes individuelle Set-/GetMethoden zu bestimmen.
DISP_PROPERTY_NOTIFY( theClass, szExternalName, memberName,
pfnAfterSet, vtPropType )
In pfnAfterSet wird der Name einer Funktion angegeben, die aufgerufen werden
soll, wenn das Attribut verändert wird.
DISP_PROPERTY_PARAM( theClass, pszExternalName, pfnGet,
pfnSet, vtPropType, vtsParams )
Im Unterschied zu DISP_PROPERTY_EX wird dieses Makro verwendet, um ein
Attribute bzw. Eigenschaft beispielsweise aus einem ein- oder mehr dimensionalen
Array zu beziehen bzw. innerhalb des Array zu setzen.
Seite 272
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Beispiel:
Set-/Get-Methoden:
afx_msg short GetArray(short row, short column);
afx_msg short SetArray(short row, short column, short
nNewValue);
Dispatch-Map Eintrag:
DISP_PROPERTY_PARAM(CMyCtrl, "Array", GetArray, SetArray,
VT_I2, VTS_I2 VTS_I2)
Überdies existiert für jedes DISP_XXX Makro noch ein DISP_XXX_ID Makro, um für
ein Attribut oder Methode eine individuelle dispID bestimmen zu können:
DISP_FUNCTION_ID(theClass, pszName, dispid, pfnMember,
vtRetVal, vtsParams)
DISP_PROPERTY_ID(theClass, pszName, dispid, memberName,
vtPropType)
DISP_PROPERTY_NOTIFY_ID(theClass, pszName, dispid, ...
12.6.4 Klassenfabrik bei Verwendung der Document/View Architektur
Wird ein OLE Automation unterstützendes Objekt als Dokument realisiert, so kann für
die Klassenfabrik die MFC Klasse COleTemplateServer verwendet werden.
COleTemplateServer ist direkt von COleObjectFactory abgeleitet und besitzt
Unterstützung für das Registrieren eines OLE Automation Objektes mit der WindowsRegistry.
Die Verwendung von COleTemplateServer soll nun am Beispiel einer SDIAnwendung demonstriert werden, Abbildung 93.
Abbildung 93: SDI-Anwendung, die DispObject beinhaltet
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 273
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Besitzt man nur einen Dokumententyp, also wie bei einer SDI Anwendung, das
zudem ein OLE Automation Objekt verkörpert, wird nur eine Instanz der Klasse
COleTemplateServer benötigt. Diese Instanz kann als Attribut der
Anwendungsklasse realisiert werden.
Header-Datei der Anwendungsklasse:
#ifndef _CMainApp_H
#define _CMainApp_H
#include <afxwin.h>
#include <afxole.h>
class CMainApp : public CWinApp
{
public:
CMainApp();
virtual BOOL InitInstance();
private:
COleTemplateServer m_server;
};
#endif
Die Verbindung zwischen Klassenfabrik und Objekt wird innerhalb der Methode
InitInstance der Anwendungsklasse hergestellt, und zwar nachdem das
Dokument mit der View und dem MainFrame verknüpft wurde. Das Herstellen der
Verbindung
erfolgt
durch
einen
Aufruf
der
Methode
COleTemplateServer::ConnectTemplate. Wobei als erster Parameter der
Methode die CLSID des OLE Automation unterstützenden Objektes übergeben wird.
Beim zweiten Parameter handelt es sich um einen Zeiger auf das Dokumenten
Template, welches dieses Objekt, realisiert als Dokument, beinhaltet. Zuletzt kann
noch durch den dritten Parameter von ConnectTemplate, wie beim Konstruktor von
COleObjectFactory entschieden werden, ob für jede Instanz des Objektes ein
eigenes Modul der Komponenten beinhaltenden Anwendung zur Verfügung stehen
soll. Bei SDI-Komponenten sollte dieser Parameter stets TRUE sein, dies bedeuted,
dass für jedes Automatisierungsobjekt eine separate Instanz der Komponente
gestartet wird.
Quelltext-Datei der Anwendungsklasse:
#include
#include
#include
#include
#include
#include
"CMainApp.h"
"CMainFrame.h"
"CDispDocument.h"
"CDispFrameView.h"
"resource.h"
"GUIDS.h"
CMainApp MainApp;
CMainApp::CMainApp()
{
}
Seite 274
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
BOOL CMainApp::InitInstance()
{
if (!AfxOleInit())
{
return FALSE;
}
Enable3dControls();
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CDispDocument),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CDispFrameView));
AddDocTemplate(pDocTemplate);
m_server.ConnectTemplate(CLSID_DispObject, pDocTemplate, FALSE);
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
{
COleTemplateServer::RegisterAll();
// return if we are running as automation server
return TRUE;
}
if (!ProcessShellCommand(cmdInfo))
return FALSE;
m_server.UpdateRegistry(OAT_DISPATCH_OBJECT);
COleObjectFactory::UpdateRegistryAll();
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
Der Aufruf von AfxOleInit() zu Anfang der InitInstance() Methode dient zur
Initialisierung der OLE Libraries. Jede MFC Anwendung die OLE bzw. COM
verwenden will, muss zu Beginn diese MFC Funktion aufrufen.
Da es sich hier um eine Softwarekomponente handelt, die auch stand-alone
betrieben werden kann, wird durch die Anweisung
if (cmd.Info.m_bRunAutomated) || cmdInfo.m_bRunEmbedded)
entschieden, ob es sich um einen stand-alone Betrieb handelt. Falls ja, wird die
Klassenfabrik – Objekt Verbindung COM nicht mitgeteilt, da der Aufruf von
COleTemplateServer::RegisterAll() ausgelassen wird. Dies hat zur Folge,
dass diese Instanz der Anwendung nicht als Softwarekomponente genutzt werden
kann, sondern falls ein Client den Dienst der Komponente in Anspruch nehmen
möchte, eine neue Instanz der Anwendung gestartet werden muss. Falls die
Komponente von einem Client genutzt wird, so wird nach dem Aufruf von
COleTemplateServer::RegisterAll()
die
Methode
InitInstance
verlassen, was zur Folge hat, dass die Benutzeroberfläche der Anwendung
verborgen bleibt.
Bei einem stand-alone Betrieb werden zudem die Eintragungen der Objekte in die
Windows-Registry durchgeführt. COleTemplateServer bietet dazu die spezielle
Methode UpdateRegistry. Mit OAT_DISPATCH_OBJECT als Übergabeparameter
von UpdateRegistry, werden spezielle Eintragungen für ein OLE Automation
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 275
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Objekt vorgenommen, was bei COleObjectFactory::UpdateRegistryAll()
nicht vorgenommen werden kann.
Zu bemerken ist, dass der mit der CLSID_DispObject assoziierte String aus einer
String-Ressource mit der Ressourcen-ID IDR_MAINFRAME herausgenommen wird.
Die String-Ressource besitzt folgendes Aussehen:
DispObject\n\nDispObject\n\n\nDispObject.Document\nDispObject
Document
Der String „DispObject.Document“ innerhalb obiger String-Ressource ist
letztendlich der mit CLSID_DispObject assoziierte String.
Wird die Komponente auf einem System neu installiert, so sollte sie zuerst einmal im
stand-alone Betrieb gestartet werden, um die Eintragungen in der Windows-Registry
vorzunehmen.
12.6.5 Verwenden des Objektes durch einen Visual Basic Client
Die Verwendung von DispObject
folgendermaßen realisiert werden:
durch
einen
Visual
Basic
Client
kann
Dim dispObject As Object
` instanzieren des Objektes
Set dispObject = CreateObject(„DispObject.Document“)
` setzen der Attribute von DispObject
dispObject.FigureOne = 1.23
dispObject.FigureTwo = 2.13
` aufruf der Methode Product und anzeigen des Ergebnisses
Label1.Caption = dispObject.Product
12.6.6 Verwenden des Objektes durch einen C++ Client
Nicht ganz so einfach wie in Visual Basic erfolgt die Verwendung eines OLE
Automation Objektes in C++. Die MFC besitzt jedoch dafür eine spezielle Klasse
namens COleDispatchDriver. COleDispatchDriver hat zudem keine
Vaterklasse.
Das Instanzieren eines Objekt mit COleDispatchDriver erfolgt über die Methode
CreateDispatch. CreateDispatch ermöglicht dabei die Identifizierung des
Objektes entweder mit dessen CLSID oder über den String, der mit der CLSID
assoziiert wurde. Die Instanzierung wird default-mäßig durch einen Aufruf von
Release, innerhalb des Destruktors von COleDispatchDriver, wieder gelöscht.
Unter Verwendung eines entsprechenden Konstruktors von COleDispatchDriver
kann dies Verhalten jedoch explizit angegeben werden.
Das Setzen oder Lesen von Attributen eines Objektes erfolgt über die Methoden
COleDispatchDriver::SetProperty bzw.
Seite 276
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
COleDispatchDriver::GetProperty.
void SetProperty(
void GetProperty(
DISPID dwDispID,
VARTYPE vtProp, ... );
DISPID dwDispID,
VARTYPE vtProp,
void* pvProp ) const;
Der erste Parameter dieser zwei Methoden ist die dispID des Attributes, das
entweder gesetzt oder gelesen werden soll. Beim zweiten Parameter der Methoden
handelt es sich um eine Beschreibung des Attributtyps in Form eines VARENUM
Elements, siehe Kapitel 12.6.2. Der letzte Parameter von GetProperty ist die
Adresse der Variable, die den Attributwert speichern soll. SetProperty’s letzter
Parameter ist die Variable deren Wert das Attribut des OLE Automation Objektes
erhalten soll.
Der Aufruf einer Methode erfolgt mittels
COleDispatchDriver::
InvokeHelper( DISPID dwDispID,
WORD wFlags,
VARTYPE vtRet,
void* pvRet,
const BYTE FAR* pbParamInfo,
...)
Bei dwDispID handelt es sich wiederum um die dispID der Methode. Mit wFlags
wird angegeben, ob es sich um eine Methode oder um ein Attribut handelt, das
gelesen oder gesetzt wird. Im Falle einer Methode muss dafür der Wert
DISPATCH_METHOD übergeben werden. vtRet ist der Typ des Rückgabewertes der
Funktion, pvRet die Adresse der Variable, in welcher der Wert des Rückgabewertes
gespeichert werden soll. pbParamInfo beschreibt die Datentypen der Parameter
der Methode. Wobei es sich bei pbParamInfo um ein Array von VTS_constants
handelt. Abschließend, als letzte Parameter von InvokeHelper, folgen noch die
Parameter, welche an die Methode übergeben werden sollen.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 277
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Nun zur Implementierung eines C++ Clients:
Die Oberfläche des C++ Clients AutoObject.exe:
Abbildung 94: Oberfläche des C++ Clients
Relevante Header-Datei des C++ Clients:
#ifndef _CAutoObjectDlg_H
#define _CAutoObjectDlg_H
#include <afxwin.h>
#include <afxole.h>
class CAutoObjectDlg : public CDialog
{
public:
CAutoObjectDlg(CWnd* pParent = NULL);
virtual ~CAutoObjectDlg();
protected:
afx_msg void OnClose();
afx_msg void OnProduct();
virtual
virtual
virtual
virtual
void
BOOL
void
void
DoDataExchange(CDataExchange* pDX);
OnInitDialog();
OnOK();
OnCancel();
DECLARE_MESSAGE_MAP()
private:
COleDispatchDriver m_DispDriver;
double m_FigureOne, m_FigureTwo, m_Result;
DISPID m_DispID_FigureOne, m_DispID_FigureTwo,
m_DispID_Product;
};
#endif
Für die einzelnen Attribute und Methoden des OLE Automation Objektes werden
Variablen vom Typ DISPID definiert, zur Speicherung derer dispIDs. Zudem wird ein
Attribut vom Typ COleDispatchDriver angelegt, um das OLE Automation Objekt
nur einmal zu instanzieren und nicht bei jedem Gebrauch, wie im Falle einer lokalen
Variablen von COleDispatchDriver.
Seite 278
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Relevante Quelltext-Datei des C++ Clients:
#include "CAutoObjectDlg.h"
#include "resource.h"
#include "GUIDS.h"
BEGIN_MESSAGE_MAP(CAutoObjectDlg, CDialog)
ON_WM_CLOSE()
ON_COMMAND(IDC_BTN_PRODUCT, OnProduct)
END_MESSAGE_MAP()
CAutoObjectDlg::CAutoObjectDlg(CWnd* pParent)
: CDialog(IDD_AUTOOBJECTDLG, pParent),
m_FigureOne(0.),
m_FigureTwo(0.),
m_Result(0.)
{
if (!m_DispDriver.CreateDispatch("DispObject.Document"))
AfxThrowOleDispatchException(0, "Can't instanciate a
DispObject");
OLECHAR *ppNames[]
= {
DISPID *ppDispIDs[] = {
L"FigureOne" , L"FigureTwo",
L"Product"};
&m_DispID_FigureOne,
&m_DispID_FigureTwo,
&m_DispID_Product };
HRESULT hr = E_FAIL;
for (int i = 0; i < sizeof(ppNames)/sizeof(ppNames[0]); i++)
{
hr = m_DispDriver.m_lpDispatch->GetIDsOfNames(IID_NULL,
&ppNames[i], 1, 0x0000, ppDispIDs[i]);
if (FAILED(hr))
{
TRACE0("Can't solve a DispID, trying to use a default
id\n");
*ppDispIDs[i] = (DISPID)(i + 1);
}
}
}
CAutoObjectDlg::~CAutoObjectDlg()
{
}
void CAutoObjectDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_ET_F1, m_FigureOne);
DDX_Text(pDX, IDC_ET_F2, m_FigureTwo);
DDX_Text(pDX, IDC_ET_RESULT, m_Result);
}
BOOL CAutoObjectDlg::OnInitDialog()
{
CDialog::OnInitDialog();
return TRUE;
}
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 279
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
void CAutoObjectDlg::OnProduct()
{
if (!UpdateData(TRUE)) return;
m_DispDriver.SetProperty(m_DispID_FigureOne, VT_R8,
m_FigureOne);
m_DispDriver.SetProperty(m_DispID_FigureTwo, VT_R8,
m_FigureTwo);
static BYTE params[] = {VTS_NONE};
m_DispDriver.InvokeHelper(m_DispID_Product, DISPATCH_METHOD,
VT_R8, &m_Result, params);
UpdateData(FALSE);
}
void CAutoObjectDlg::OnOK()
{
}
void CAutoObjectDlg::OnCancel()
{
}
void CAutoObjectDlg::OnClose()
{
EndDialog(0);
}
Im Konstruktor von CAutoObjectDlg wird das DispObject instanziert, anhand
des mit dessen CLSID verknüpften Strings „DispObject.Document“. Zudem
werden nun über die IDispatch-Methode GetIDsOfNames die zum Attribut- bzw.
Funktionsnamen zugehörigen dispIDs vom Objekt bezogen und in den dafür
vorgesehenen Member-Variablen gespeichert. Den Zeiger auf IDispatch des
Objektes
erhält
man
durch
das
Member
m_lpDispatch
von
COleDispatchDriver. Mit IDispatch::GetIDsOfNames erhält man die dispIDs
von Attributen und Methoden, sowie die dispIDs von sämtlichen Übergabeparameter
einer Methode. Wobei der erste Name des als zweiter Parameter übergebenen
Arrays stets der Name des Attributes bzw. der Methode sein muss. Im obigen
Beispiel werden sämtliche Namen von Attributen und Methoden in einem Array
gespeichert, um möglichst wenig Implementierungsaufwand beim Aufruf von
GetIDsOfNames zu erhalten.
Zugriff auf das Objekt erfolgt in der Methode OnProduct. OnProduct wird
aufgerufen, wenn die Schaltfläche Product betätigt wird. In dieser Methode von
CAutoObjectDlg werden die Attribute FigureOne und FigureTwo gesetzt, um
anschließend die mathematische Operation Product durchführen zu können.
12.7 Zusammenfassung
Die Technologie der Komponentensoftware ermöglicht das „Zusammenstöpseln“
einer
Anwendung
aus
einzelnen
Softwarekomponenten.
Wobei
eine
Softwarekomponente eine binäre Programmeinheit darstellt, die in einer beliebigen
Programmiersprache erstellt werden kann. Das einzige Kriterium bei der Wahl der
Programmiersprache ist die Fähigkeit, die definierte Schnittstelle erzeugen zu
können. Gerade bei größeren Projekten bietet es sich an die einzelnen
Softwarebausteine der unterschiedlichen Teams als Softwarekomponenten in
Seite 280
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Entwicklung von Komponentensoftware mit MFC
Auftrag zu geben. Die einzelnen Teams können dann selbst entscheiden, in welcher
Sprache sie die Komponente erstellen.
QueryInterface die Methode von IUnknown, die vor allem Versionskonflikte
vermeiden soll, ermöglicht einer Anwendung auch mit älteren Komponenten ohne
abzustürzen zu kooperieren. Besitzt eine Anwendung beispielsweise einen
Menüeintrag für eine spezielle mathematische Kalkulation, welche eine auf dem
System oder im Netz befindliche Version der Komponente implementiert und diese
Komponente jedoch noch nicht über das entsprechende Interface verfügt, das für die
mathematische Kalkulation verantwortlich ist, so scheitert einfach der Aufruf von
QueryInterface für besagtes Interface. Die Anwendung kann dem Benutzer dann
in Form eines Dialoges mitteilen, das diese Funktion momentan noch nicht zur
Verfügung steht.
OLE Automation ermöglicht eine Softwarekomponente mit Hilfe von Interpreter zu
steuern. Es kann auch als Kommunikationsmittel zwischen Anwendung und
Komponente oder zwischen Komponente und Komponente verwendet werden.
Neben der MFC kann auch die ActiveX Template Library, kurz ATL, verwendet
werden, um Softwarekomponenten zu erstellen. Die ATL wurde speziell für diesen
Einsatz konzipiert und zeichnet sich durch sehr schnellen Code aus, der über eine
geringe Größe verfügt. ATL ist Bestandteil von Visual C++ der neueren Versionen.
Die Grundlagenarbeit dieses Kapitels erleichtert den Start bei der Entwicklung von
Softwarekomponenten und bietet eine fundamentale Basis, um sich in die ATL
schnell einarbeiten zu können.
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 281
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Anhang
13 Anhang
13.1 Bücherliste
Diese Liste stellt eine subjektive Auswahl an Büchern dar. Wer auf der Suche nach
einem guten Nachschlagwerk oder einer MFC-Refernz ist, der sei auf die MSDNLibraray verwiesen, die auch im Internet zu finden ist (http://msdn.microsoft.com)
Autor: Prosise, Jeff
ISBN: 1-57231-695-0
Titel: Windows Programmierung mit MFC
Untertitel: Objektorientierte Programmierung für alle 32-bit Plattformen
Umfang: 1400 Seiten, CD-ROM
Erscheinungstermin: 2. Aufl. 07/1999
Preis: 129,00 DM
Windows-Programmierung mit MFC, 2. Auflage ist die stark erweiterte und erstmals
ins Deutsche übertragene Ausgabe des Standard-Werks zur Programmierung mit
den MFC. Dieses Buch ist die ultimative Resource für ale Programmierer, die die
Möglichkeiten von C++ in Verbindung mit den Microsoft Foundation Classes
verstehen und ausnützen wollen. Es beinhaltet eine Vielzahl von
Programmierkonzepten und -Techniken zusammen mit vielen Tips und Tricks zum
Programmieraltag mit den MFC für alle 32-bit Windows-Plattformen. Dieses Buch ist
unverzichtbar für alle Programmierer, die die MFC benützen und ein grundlegendes
Werk für alle, die lernen wollen, die Möglichkeiten der MFC in Ihre Arbeit zu
integrieren.
Autoren: Microsoft Corporation ISBN: 3-86063-461-5
Titel: Microsoft Visual C++ 6.0 Programmierhandbuch
Untertitel: Der offizielle Leitfaden zur Programmierung mit Visual C++ 6.0
Umfang: 1056 Seiten
Erscheinungstermin: 12.08.98
Preis: 89,00 DM
Visual C++ 6.0 ist die neueste Version der klassischen Entwicklungsumgebung für
die Windowsprogrammierung. Dieses Buch zeigt Ihnen, worauf es bei der
Programmierung mit Visual C++ 6.0 wirklich ankommt. Das MicrosoftEntwicklungsteam von Visual C++ hat hier, entsprechend der inhaltlich identischen
Online-Dokumentation, die wichtigsten Informationen und Konzepte
zusammengefaßt, die jeder Visual C++-Programmierer benötigt, um effizient und
schnell zu Ergebnissen zu kommen. Lernen Sie die neuen Features von Visual C++
6.0 kennen, arbeiten Sie mit Objekten und Klassen, optimieren Sie die
Geschwindigkeit Ihrer Programme und nutzen Sie die Microsoft Foundation Classes
(MFC). Dieses Buch ist der ideale Begleiter zum Einstieg in die Visual C++Programmierung und eine umfassende Referenz für den fortgeschrittenen
Programmierer.
Seite 282
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Autor: David Kruglinski, George Shepherd, Scot Wingo
ISBN: 3-86063-461-5
Titel: Inside Visual C Plusplus 6.0, Das Microsoft Standardwerk zur Programmierung
mit Visual C Plusplus. MFC, ATL, Internet und vieles mehr
Umfang: 1104 Seiten
Erscheinungstermin: 1998
Preis: 98,00 DM
Inside Visual C++ 6.0 ist die neuste Ausgabe des Buchs, das sich mittlerweile einen
herausragenden Platz in der Reihe der Standardwerke zur Visual C++Programmierung gesichert hat. In der 5. Auflage präsentiert es sich komplett
überarbeitet und an die neuen Möglichkeiten von Microsoft Visual C++ 6.0 angepaßt.
Neben den Fundamenten der Visual C++Programmierung, umfangreicher
Abdeckung der Programmierung mit den MFC und fortgeschrittenen Themen finden
Sie Informationen zu den wichtigen neuen Aspekten von Visual C++ 6. Neuen
Möglichkeiten der Datenbankprogrammierung (OLE DB), der Programmierung mit
der ATL, dem Umgang mit DHTML, den Steuerelementen des Internet Explorer 4
und der Windows CE-Programmierung sind eigene Kapitel gewidmet. Damit bietet
Inside Visual C++ in seiner 5. Auflage noch mehr an detaillierten und wertvollen
Informationen, die dieses Buch zur umfassenden Informationsquelle und zum idealen
Lehrbuch über eines der mächtigsten und komplexesten Entwicklungswerkzeuge
machen.
Autor: Charles Petzold
ISBN: 1-57231-995-X
Titel: Programming Windows
Untertitel: The definitive guide to the Win32 API
Umfang: 1479 Seiten
Erscheinungstermin: 1998, 5. Ausgabe
Preis: 59,99 $
"Schau mal im Petzold nach" ist nach wie vor die ultimative Antwort in Sachen
Windows-Programmierung. Mit der stark überarbeiteten und wesentlich erweiterten
5. Auflage liegt dieses Standardwerk für die Programmierung der Win32-API nun in
einer Form vor, die alle neueren Versionen von Windows abdeckt. Programmierer
unter Windows 95, 98 und NT 4 finden hier das aktuellste Wissen um den Umgang
mit den von Windows zur Verfügung gestellten Programmierschnittstellen. Dabei
bleibt das Buch das, was es schon immer gewesen ist: das umfassende Lehr- und
Nachschlagewerk für die Kernstücke der Windows-Programmierung. Kein WindowsProgrammierer sollte auf dieses Buch verzichten
Autor: Jeffrey Richter
ISBN: 3-86063-615-4
Titel: Microsoft Windows Programmierung für Experten
Umfang: 1000 Seiten, m. CD
Erscheinungstermin: 1/2000, 4. Auf.
Preis: 129,00 DM
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 283
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
Anhang
Herausragende Informationen und Insiderwissen über die Programmierung unter
Windows 98 und Windows 2000 mit zusätzlichen Informationen über ein zukünftiges
64-Bit Windows sichern diesem Titel einen Spitzenplatz unter den Büchern, die sich
mit Programmierung auf Kernel- und API-Ebene beschäftigen. Jeffrey Richter
vermittelt Ihnen sowohl die Grundlagen als auch die Feinheiten, um mächtige,
schnelle und robuste Programme für Windows 2000 und Windows 98 zu schreiben
und dabei die Möglichkeiten der Windows-API bis ins letzte auszunutzen.
Fortgeschrittene Techniken im Umgang mit DLLs, Threads, API-Hooking, Details
zum Speicherhandling unter Windows und vieles mehr machen dieses Buch zu
Standardwerk für die Programmierung der neusten Generation von Windows.
Autor: George Sheperd; Scot Wingo
ISBN: 0201407213
Verlag: Addison-Wesley Longman, Amsterdam
Titel: MFC Internals, Inside the Microsoft Foundation Class Architecture
Umfang: 709 Sieten
Erscheinungstermin: 1996
Preis: 92.00 DM
MFC ist die Klassenbibliothek für die Anwendungsentwicklung unter Windows.
Dieser Leitfaden wurde geschrieben für Programmierer die wissen wollen, wie und
warum die MFC intern aussieht. Dies ist sicherlich keine leichte Kost, dafür aber sehr
lehrreich
Autor: Kain, Eugene
ISBN: 0-201-18537-7
Verlag: Addison-Wesley Longman
Titel: The MFC Answer Book, Solutions for Effective Visual C Plusplus Applications
Umfang: 674 Seiten
Erscheinungstermin: 1999
Preis: 94.00 DM
Getting the most out of your MFC applications is the goal of Eugène Kain's The MFC
Answer Book. Though it does not cover newer Internet Explorer-style enhancements,
this title offers some indispensable tips for writing more attractive Microsoft
Foundation Classes (MFC) applications in Visual C++.
The book begins with an excellent tour of the MFC document/view architecture. As
the author notes, Visual C++ wizards let you generate simple, functional multipledocument interface (MDI) applications, but after that you're on your own. To remedy
this situation, the author shares his expertise for building better MDI applications,
including saving and reloading files effectively and how to manage more than one
view.
The same question-and-answer approach is used for such topics as views, dialog
boxes, and property sheets. Standout tips here include how to size and control views,
as well as how to change the color and font used for dialog controls. (The author also
shows you how to create applications that run in full-screen mode, just like in
Microsoft Word 97.) Toolbars, menus, and printing functions round out the tour.
Throughout this text, there are plenty of short, clear programming examples that
show exactly how to solve some of the most perplexing and common problems faced
Seite 284
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Geiger, Keim, Kleinknecht, Schuler, Weiß
Einführung in die Windows-Programmierung mit MFC
by the working MFC programmer. There's little doubt that The MFC Answer Book can
save you hours of experimenting on your own, but it can also help you create
significantly more responsive and appealing MFC programs. --Richard Dragan
STEINBEIS-TRANSFERZENTRUM
SOFTWARETECHNIK
Revision 1.4
Seite 285
Geiger, Keim, Kleinknecht, Schuler, Weiß