C++ in der Praxis

Veröffentlicht: 01. Okt 2006 | Aktualisiert: 10. Okt 2006

Von Paul DiLascia

Laden Sie den Code zu diesem Artikel herunter: C++atWork2006_10.exe (174KB)

F:

Wenn ich eine DLL mit Automatisierungsunterstützung mit VisualC++®6.0 erstelle, werden einige Funktionen für die Registrierung, aber nicht für die Abmeldung meiner DLL generiert. Ich habe einen DllUnregisterServer ähnlich dem Folgenden geschrieben:

STDAPI DllUnregisterServer(void)
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());
  if (!COleObjectFactory::UnregisterAll())
  return ResultFromScode(SELFREG_E_CLASS);
  return NOERROR;
}

COleObjectFactory::UnregisterAll gibt den Wert "TRUE" zurück, aber meine DLL bleibt angemeldet. Wie sollte der Code geschrieben werden?

Ivan Pavlik

A:

Sie sind hier leider auf ein kleines Problem in der Welt von MFC gestoßen. UnregisterAll ist gemeinhin die Funktion, die zur Abmeldung der DLL verwendet wird. Die Datei olefact.cpp, in der UnregisterAll implementiert wird, sieht etwa wie folgt aus:

for (/* pFactory = all factories */) 
{
  pFactory->Unregister();
}

Das bedeutet, dass diese Datei eine Schleife durch alle Factorys des Moduls durchläuft und für jede Factory Unregister aufruft. So weit, so gut. Was aber macht COleObjectFactory::Unregister genau? Die Antwort finden Sie im Code. FakePre-0cefd7f32e234b1689ec44465b65ebef-d2c0e166100e4022a3b109ddd16924ee

Wie Sie sehen können, macht COleObjectFactory::Unregister überhaupt nichts, Es gibt nur "TRUE" zurück. Dies ist recht seltsam, da COleObjectFactory::Register tatsächlich die COM-Klasse durch Aufrufen von ::CoRegisterClassObject registriert. Andererseits gibt es eine andere Funktion, COleObjectFactory::Revoke, die ::CoRevokeClassObject für die Abmeldung der COM-Klasse aufruft. MFC ruft "Revoke" automatisch vom COleObjectFactory-Destruktor auf. Was genau geschieht hier also?

Das Problem liegt in einer Verwirrung bei der Terminologie begründet, die aus den unterschiedlichen Weisen stammt, auf die COM-Klassen für DLL- und EXE-Dateien registriert werden. Für DLLs werden Klassen durch Hinzufügen von Schlüsseln zur Windows-Registrierung angemeldet (CLSID, ProgID und so weiter). Für EXE-Dateien müssen Sie CoRegisterClassObject aufrufen, um die Klasse zur Laufzeit im COM-System zu registrieren. Die Tatsache, dass bei EXE-Dateien das Gegenteil von "Register" nicht "Unregister" sondern "Revoke" ist (CoRevokeClassObject), macht die Angelegenheit noch verwirrender.

Mit der Aufnahme von COM-Unterstützung in MFC sollten die Dinge zwar vereinfacht werden, aber dies war nicht immer ganz erfolgreich. COleObjectFactory::Register sieht wie folgt aus:

// in olefact.cpp
BOOL COleObjectFactory::Register()
{
  if (!afxContextIsDLL) {
    ::CoRegisterClassObject(...,
    CLSCTX_LOCAL_SERVER, ..);
     }
}

Es ist sofort ersichtlich, dass DLL-Dateien überhaupt nicht betroffen sind. Der Code registriert nur EXE-Dateien unter Verwendung von CLSCTX_LOCAL_SERVER (Kontext = lokaler Server, EXE-Datei wird auf lokalem Computer ausgeführt). In Anlehnung an die zugrunde liegende C-API hat Microsoft denselben Namen, "Revoke", für die Funktion verwendet, mit der die EXE-Datei abgemeldet wird: COleObjectFactory::Revoke (wird automatisch vom Klassenfactory-Destruktor aufgerufen).

Was ist also der Verwendungszweck von COleObjectFactory::Unregister, der Funktion, die nichts macht? Vielleicht war es die Absicht von Microsoft, "Register" und "Unregister" eines Tages auch für DLL-Dateien anwendbar zu machen. Derzeit ist dies aber nicht der Fall. Sie benötigen also eine völlig andere Funktion für die Registrierung der DLL-Datei: COleObjectFactory::UpdateRegistry. Diese Funktion verwendet ein boolesches Argument, das MFC mitteilt, ob die COM-Klasse an- oder abgemeldet werden soll. Es gibt auch die Funktion UpdateRegistryAll, die eine Schleife durch alle Klassenfactorys durchläuft und für jede Factory UpdateRegistry aufruft. So implementieren Sie also DllUnregisterServer richtig:

STDAPI DllUnregisterServer(void)
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());
  return COleObjectFactory::UpdateRegistryAll(FALSE)
  ? S_OK : SELFREG_E_CLASS;
}

DllRegisterServer sollte identisch aussehen, aber "TRUE" anstelle von "FALSE" an UpdateRegistryAll übergeben.

Die Standardimplementierung für UpdateRegistry versucht, die entsprechenden Registrierungsschlüssel? ProgID, Insertable, ThreadingModel und so weiter? zu HKCR/CLSID?InprocServer32 hinzuzufügen oder daraus zu löschen, aber in manchen Fällen werden zusätzliche, für die COM-Klasse spezifische Schlüssel benötigt. So müssen Sie z.B. möglicherweise Kategorien registrieren, bei denen es sich um eine Art Erweiterung des alten "Insertable"-Schlüssels handelt (was bedeutet, dass die COM-Klasse im Entwurfsmodus in ein Formular gezogen werden kann). In diesem Fall müssen Sie UpdateRegistry überschreiben, um Ihre eigenen Schlüssel hinzuzufügen oder zu entfernen.

In den Ausgaben von Microsoft Systems Journal (jetzt MSDN®-Magazin genannt) vom November und Dezember1999 wurde das Erstellen eines Bereichsobjekts für Internet Explorer unter Verwendung der Active Template Library (ATL) IRegistrar-Schnittstelle erläutert. (Bereichsobjekte müssen eine besondere Kategorie mit dem Namen "CATID_DeskBand" registrieren.) IRegistrar ist ein äußerst nützliches Tool, mit dem Sie ein Registrierungsskript (.RGS-Datei) zum Hinzufügen Ihrer Registrierungseinträge schreiben können, anstatt Registrierungsfunktionen wie RegOpenKey, RegSetValue und so weiter aufzurufen. In Abbildung1 sehen Sie ein typisches Skript.

Sie sehen hier, dass Sie mit IRegistrar Variablen wie %ClassName% und %ThreadingModel% definieren und zur Laufzeit durch die eigentlichen Werte ersetzen können. Mit IRegistrar müssen Sie die Registrierungs-API nie wieder aufrufen. Sie können ein Skript schreiben, das den eigentlichen Registrierungseinträgen ähnlicher ist, und Sie können mit demselben Skript sogar Ihre DLL an- oder abmelden. Das heißt, dass IRegistrar intelligent genug für Abmeldungen ist. Ich konnte IRegistrar nirgendwo dokumentiert finden, aber der Code ist verfügbar. (Sie finden ihn in atliface.h .) Wenn Sie IRegistrar nicht bereits für das An- und Abmelden Ihrer COM-DLLs verwenden, sollten Sie es sich näher ansehen: Sie werden viel Zeit sparen. Weitere Einzelheiten finden Sie in meinem Artikel vom November1999.

F:

Hauptanwendung hat ein normales Menü mit einem Bearbeitungsuntermenü mit Befehlen ("Ausschneiden", "Kopieren", "Einfügen" und so weiter). Ich möchte, dass das Untermenü "Bearbeiten" als Kontextmenü angezeigt wird, wenn der Benutzer mit der rechten Maustaste in das Hauptfenster klickt. Das Problem ist nun, wie ich dieses Untermenü aus dem Hauptmenü nehmen kann. Es scheint keine mit einem Untermenü verbundene Befehls-ID zu geben. Ich kann den nullbasierten Index verwenden, aber aufgrund der Anpassung steht das Menü "Bearbeiten" möglicherweise nicht immer an zweiter Stelle in der Reihenfolge. Ich kann auch nicht nach dem Wort "Bearbeiten" suchen, weil wir mehrere Sprachen unterstützen und so das eigentliche Wort anders lauten kann. Wie kann ich in all diesen Fällen das Untermenü "Bearbeiten" finden?

Brian Manlin

A:

Sofern Sie Untermenüs haben, sollte das Menü "Bearbeiten" das zweite Untermenü sein. Gemäß den offiziellen Windows®-GUI-Richtlinien sind die ersten drei Menüelemente "Datei", "Bearbeiten" und "Ansicht" (in dieser Reihenfolge) ? wenn Sie diese Elemente unterstützen. Informationen hierzu finden Sie im Artikel mit den Richtlinien der Windows-Schnittstelle für Software Design (Microsoft Press®, 1995, möglicherweise in englischer Sprache). Ich kann Ihnen aber helfen, das Untermenü "Bearbeiten" ? oder ein beliebiges anderes Untermenü ? zu finden.

Sie haben Recht: Es gibt keine Befehls-ID für ein Untermenü. Das liegt daran, dass Windows intern das Befehls-ID-Feld für das HMENU des Untermenüs verwendet, wenn es sich bei dem Menüelement um ein Untermenü handelt. Wenn das Untermenü immer einen spezifischen Befehl enthält (zum Beispiel "Bearbeiten > Ausschneiden") und der Befehl immer dieselbe ID aufweist (zum Beispiel ID_EDIT_CUT), können Sie einfach eine Funktion schreiben, die das Untermenü sucht, das einen bestimmten Befehl enthält. In Abbildung2 sehen Sie den Code. CSubmenuFinder fungiert als ein Namespace für die statische Funktion "FindCommandID", die sich rekursiv selbst aufruft, um das Menü und alle Untermenüs eingehend nach einem Menüelement zu durchsuchen, dessen Befehls-ID dem gesuchten Element entspricht.

Dieser Codeausschnitt durchsucht das Hauptmenü nach einem Untermenü, das ein Menüelement mit der Befehls-ID "ID_EDIT_CUT" enthält, und gibt ggf. das gefundene Untermenü zurück. Für den Test von CSubmenuFinder habe ich ein kleines Programm mit dem Namen "EdMenu" geschrieben. "EdMenu" verwendet den vorherigen Code, damit WM_CONTEXTMENU genau dasselbe Bearbeitungsuntermenü anzeigt, das im Hauptmenü enthalten ist, wenn der Benutzer mit der rechten Maustaste in das Hauptfenster klickt. CSubmenuFinder findet das Untermenü, wo auch immer es im Hauptmenü erscheint. Ich habe dies getestet, indem ich das Menü "Bearbeiten" in einem meiner Builds vorübergehend vor das Menü "Ansicht" verschoben habe. Laden Sie den Quellcode herunter, um dies selbst auszuprobieren.

F:

konvertiere ich MFCCString in eine Zeichenfolge in verwaltetemC++? Ich habe beispielsweise den folgenden Code inC++:

void GetString(CString& msg)
{
  msg = // build a string
}

Wie kann ich diese Funktion mit verwaltetemC++ umschreiben und den CString-Parameter durch eine verwaltete Zeichenfolge ersetzen? Das Problem liegt darin, dass GetString die CString des Aufrufenden ändert, und ich möchte dies unter Verwendung einer verwalteten Zeichenfolge tun.

Sumit Prakash

A: Die Antwort hängt davon ab, ob Sie die neue C++/CLI-Syntax oder die alten verwalteten Erweiterungen verwenden. Beide Syntaxarten können verwirrend sein, aber hier finden Sie Antworten.

Da dies das Jahr2006 ist, fange ich mit der neuen Syntax an. Um die Syntax richtig zu schreiben, achten Sie auf zwei Dinge: Erstens weisen verwaltete Objekte in C++/CLI einen Akzent auf:"^". "CString" wird also zu "String^". Zweitens müssen Sie daran denken, dass der Verweis (&) in der verwalteten Syntax der Nachverfolgungsverweis ist; in C++/CLI ist dies"%". Es ist möglicherweise nicht so einfach,"^" nicht zu vergessen, aber mit der Zeit werden Sie auch"%" ganz normal finden. Die neuen Funktionen sehen also folgendermaßen aus:

// using C++/CLI
void GetString(String^% msg)
{
  msg = // build a string
}

Ergibt dies einen Sinn? Anhand eines kleinen Programms mit dem Namen "strnet.cpp" können Sie sehen, dass dies wirklich funktioniert (siehe Abbildung3). Hier werden zwei Klassen, "CFoo1" und "CFoo2", je mit einem GetName-Member implementiert. In der ersten Klasse wird "CString&" und in der zweiten "String^%" verwendet. Wenn Sie dieses Programm kompilieren und ausführen (natürlich unter Verwendung von /clr), sehen Sie, dass bei beiden Klassen ? bei der Klasse, die "CString", und der Klasse, die "String" verwendet ? das übergebene (systemeigene oder verwaltete) Objekt von der Memberfunktion geändert wird.

Sie sollten übrigens für verwaltete Objekte immer einen Nachverfolgungsverweis (%) anstelle eines systemeigenen Verweises (&, der ebenfalls kompiliert) verwenden, weil der Nachverfolgungsverweis den Verweis auch dann nachverfolgt, wenn der Garbage Collector das Objekt innerhalb seines verwalteten Heaps verschiebt.

Wenn Sie die Syntax des alten Stils verwenden (/clr:oldSyntax), können Sie dennoch einen Verweis verwenden. Sie dürfen nur nicht vergessen, dass es sich bei verwalteten Objekten um __gc-Zeiger handelt, wenn Sie verwaltete Erweiterungen verwenden; aus "CString" wird also "String__gc *". Und da der Compiler bereits weiß, dass es sich bei "String" um einen verwalteten Typ handelt, benötigen Sie "__gc" nicht, und ein einfacher Zeiger (*) reicht aus. Der Verweis ist derselbe. Die Konvertierung der Syntax alten Stils sieht also folgendermaßen aus:

// __gc omitted
void GetString(String*& msg)
{
  msg = // build a string
}

Eigentlich ist es also gar nicht so kompliziert. Wenn Sie Probleme haben, die Syntax vonC++ zu verstehen, greifen Sie einfach auf die Grundlagen zurück.

F:

Ich habe kürzlich eine CLR-Konsolenanwendung mit Visual Studio®2005 generiert und dabei festgestellt, dass Visual Studio eine "main"-Funktion erstellt hat, die folgendermaßen aussieht:

int main(array<System::String ^> ^args)
{
  ...
  return 0;
}

Diese scheint sich von den alten Elementen "argc" und "argv" zu unterscheiden, mit denen ich vonC bzw.C++ her vertraut bin. Als ich versucht habe, auf "args[0]" zuzugreifen, da ich dachte, es handle sich um den Dateinamen (wie in C/C++), habe ich festgestellt, dass "args[0]" nicht der Dateiname, sondern der erste Befehlszeilenparameter ist. Was ist mit dem Dateinamen passiert? Und können Sie den Grund für diese Änderung erklären?

Jason Landrew

A:

Von Zeit zu Zeit werden in praktisch allen Programmen Änderungen vorgenommen. Wir kennen "argc" und "argv", seit C im Jahr1972 entwickelt wurde, und nun hat Microsoft es geändert. Vielleicht sollte damit sichergestellt werden, dass alle wissen, wie die neue Arraysyntax zu verwenden ist.

Ein Grund für die neue Syntax bestand darin, ein Programm zu generieren, das mit /clr:safe kompiliert wird, was die beste Codesicherheit bietet. Es ist ein bisschen so, als ob Sie Ihr Programm in einen Reinraum mit höchster Schutzstufe einsperren. Bei Verwendung der Option /clr:safe erlegt der Compiler allerlei Einschränkungen auf. Sie dürfen keine systemeigenen Typen verwenden. Sie dürfen keine Globalen verwenden. Sie dürfen keine nicht verwalteten Funktionen aufrufen. Kurz gesagt: Sie können vieles nicht tun, was in der Vergangenheit möglich war. Und was ist der Grund für all diese Einschränkungen? Der Grund ist, dass Sie so wirklich sicher sein können, dass Ihr Code sicher ist. Technisch ausgedrückt generiert /clr:safe nachprüfbaren Code, d.h. die CLR (Common Language Runtime) kann überprüfen, ob der Code Sicherheitseinstellungen verletzt und z.B. versucht, auf eine Datei zuzugreifen, auf die der Zugriff gemäß den Sicherheitseinstellungen des Benutzers nicht gestattet ist. Eines der vielen Tabus auf der Liste von /clr:safe betrifft systemeigene Zeiger, d.h. Zeiger allgemein. ("Systemeigen" ist redundant, da Sie keinen Zeiger auf einen verwalteten Typ haben können.) Da Zeiger mit /clr:safe nicht gestattet sind, kann die alte argv-Deklaration nicht verwendet werden. Also hat Microsoft die Deklaration so geändert, dass stattdessen ein String^-Array verwendet wird. Und da Arrays ihre Länge kennen, ist "argc" nicht erforderlich. Die Laufzeitbibliothek enthält den geeigneten Startcode, mit dem das arg-Array vor Aufruf der main-Funktion erstellt und initialisiert wird. Wenn Sie nicht planen, /clr:safe zu verwenden, können Sie eine main-Funktion schreiben, die die alte argc/argv-Signatur verwendet.

Das erklärt den Grund für die Verwendung eines verwalteten Arrays, nicht aber, warum "args[0]" der erste Befehlsparameter und nicht der Dateiname ist. Ich kann natürlich nicht für Microsoft sprechen, aber vielleicht war der Gedankengang, dass das Auslassen des Dateinamens mehr Sinn ergibt, oder dass Sie diesen Dateinamen nicht brauchen. Was auch immer der Grund sein mag, ist unerheblich, da es einfach ist, den Dateinamen herauszufinden. Es gibt wahrscheinlich viele Möglichkeiten dafür, aber die offensichtlichste und CLR-zentrische (mit /clr:safe kompatible) Weise, die mir in den Sinn kommt, ist die Verwendung der Klasse "System::Environment". Diese Klasse macht die folgende Methode verfügbar:

static array<String^>^ GetCommandLineArgs ()

Im Gegensatz zu dem der main-Methode verfügbaren args-Parameter startet das von "Environment::GetCommandLineArgs" zurückgegebene Array mit dem Namen der ausführbaren Datei.

Und warum möchten Sie den Namen Ihrer eigenen EXE-Datei überhaupt herausfinden? Schließlich schreiben Sie das Programm und kennen daher vermutlich ihren Name. Ein denkbarer Grund ist es, eine Hilfemeldung erstellen zu können, die den Programmnamen enthält, ohne dass Sie diesen hartcodieren müssen. Diese Hilfemeldung könnte etwa wie folgt aussehen:

FooFile -- Turns every word in your file to "foo"
usage:
FooFile [/n:<number>] filespec
filespec = the files you want to change
<number> = change only the first <number> occurrences

Hier ist "FooFile" der Name des Programms. Ich könnte einfach "FooFile" in den Hilfetext schreiben, aber ich habe schon vor langer Zeit gelernt, dass ich Programme oft umbenenne oder Code von einem Programm in ein anderes kopiere. Wenn ich den Programmnamen vom Programm selbst erhalte (durch "argv" oder "Environment::GetCommandLineArgs"), wird immer der richtige Name in meinen Hilfemeldungen angezeigt, auch wenn ich die Datei umbenenne oder meinen Code in ein anderes Programm kopiere.

Viel Spaß beim Programmieren!

Senden Sie Fragen und Kommentare für Paul in englischer Sprache an cppqa@microsoft.com.