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ß