CString Management - VARIANT, STRINGTABLE und temporäre Objekte |
Geschrieben von: StarShaper | ||||||
Dienstag, den 20. September 2005 um 18:30 Uhr | ||||||
Seite 3 von 4
VARIANT in CStringTatsächlich habe ich dies bisher noch nicht gemacht. Ich arbeite nicht in COM/OLE/ActiveX wo das ein Thema ist. Aber ich habe ein Posting von Robert Quirk in der microsoft.public.vc.mfc Newsgroup gelesen wie es funktioniert. Zudem wäre es blöd dies nicht in das Tutorial zu schreiben. Deshalb hier nun die Anleitung mit ausführlicher Erklärung. Jegliche Fehler im Vergleich zum Original nehme ich auf meine Kappe. Ein VARIANT ist ein generischer Parameter/Rückgabewert in der COM Programmierung. Sie können Methoden schreiben welche einen VARIANT Typ zurückgeben. Welchen Typ die Funktion zurückgibt kann (und das tut sie oft) von den Input Parametern der Methode abhängen (z.B. in der Automatisation, anhängig davon welche Methode aufgerufen wird, gibt IDispatch::Invoke (via einer ihrer Parameters) einen VARIANT welcher ein BYTE enthält, ein WORD, ein float, ein double, ein Datum, ein BSTR und ungefähr drei Dutzend weitere Typen zurück. Schauen sie sich dazu die Spezifikationen eines VARIANT Typs in der MSDN an. In dem unteren Beispiel wird angenommen das der Typ als VARIANT vom Typ BSTR bekannt ist, was bedeutet das der Wert im string welcher durch bstrVal referenziert wird gefunden werden kann. So wird der Vorteil ausgenutzt das die Tatsache das es einen Konstruktor gibt welcher in einer ANSI Anwendung einen Wert, mit einer Referenz eines LPCWCHAR auf einen CString, konvertiert. Im Unicode Modus ist das der nomale CString Konstruktor. Schauen sie sich die Vorbahalte zu der Standard ::WideCharToMultibyte Konvertierung an und machen sie es für sich aus ob dies für sie akzeptabel ist oder nicht. Meistens, ist es ok. VARIANT vaData; vaData = m_com.YourMethodHere(); ASSERT(vaData.vt == VT_BSTR); CString strData(vaData.bstrVal); Beachten sei das sie auch eine generischere (vielfältigarer wiederverwendbar) Umwandlungs-Routine, welche das vt Feld fallspezifisch mit einbezieht, schreiben können. In diesem Fall ist der folgende Code vielleicht etwas für sie: CString VariantToString(VARIANT *va) { CString s; switch(va->vt) { /* vt */ case VT_BSTR: return CString(vaData->bstrVal); case VT_BSTR | VT_BYREF: return CString(*vaData->pbstrVal); case VT_I4: s.Format(_T("%d"), va->lVal); return s; case VT_I4 | VT_BYREF: s.Format(_T("%d"), *va->plVal); case VT_R8: s.Format(_T("%f"), va->dblVal); return s; ... die übrigen cases wurden als kleine Übung für sie weggelassen default: ASSERT(FALSE); // unbekannter VARIANT Typ (dieses ASSERT ist optional) return CString(""); } /* vt */ } Laden von STRINGTABLE WertenWenn sie ein Programm schreiben möchten welches leicht in andere Sprachen zu konvertieren sein soll, dürfen sie nicht native Sprach-Strings in ihrem Quellcode benutzen. Zum Beispiel wäre das in meinem Fall Englisch, weil das meine Muttersprache ist - aber ich kann auch ein wenig Deutsch sprechen. Wie auch immer. Es ist nicht gut diese strings an den Quellcode zu binden, so wie in dem folgenden Beispiel. CString s = "There is an error"; Stattdessen sollten sie alle sprachspezifischen strings in eine seperate Resourcen Datei packen (außer vielleicht die strings zum Debuggen. Aber diese sollten in einem fertigen Programm sowieso nicht mehr enthalten sein). Deshalb solle man dies so ähnlich in ein Programm schreiben: s.Format(_T("%d - %s"), code, text); Der nachfolgende literale string ist nicht sprachsensitiv. Wie auch immer. Sie sollten sehr vorsichtig sein und strings wie diesen nicht benutzen. // fmt ist "Error in %s file %s" // readorwrite ist "reading" oder "writing" s.Format(fmt, readorwrite, filename); Ich spreche hier aus eigener Erfahrung. Ich beging diesen Fehler in meiner ersten internationalisierten Anwendung. Trotz der Tatsache das ich Deutsch kann und weiß das ein Verb im Deutschen im Gegensatz zum Englischen an das Ende eines Satzes gestellt wird. Unser deutscher Verleger beschwerte sich lautstark darüber das er verwirrende deutsche Fehlernachrichten schreiben müsse damit der formatierte Code das machte was er sollte. Es ist viel besser (und das mache ich heute so) zwei strings zu haben. Einen für das Lesen und einen für das Schreiben und anschließend den entsprechenden zu laden. Dies erreicht man indem man sie string-parameter-insensitiv gestaltet, was bedeutet das ganze Format anstatt die einzelnen strings "reading" oder "writing" zu laden. // fmt is "Error in reading file %s" // "Error in writing file %s" s.Format(fmt, filename); Beachten sie das wenn sie mehr als eine Ersetzung haben, sie darauf achten müssen das die Reihenfolge der Wörter, also Nomen - Verb oder Verb - Nomen usw. keine Rolle spielt. Ich werde vorerst nicht über FormatMessage sprechen, welches derzeit besser ist als sprintf/Format, aber schlecht in die CString Klasse implementiert wurde. FormatMessage löst das obige Problem indem es die Parameter anhand der Position in der Parameterliste erkennt und es erlaubt diese im Output string neu anzuordnen. Aber wie wollen wir das nun erreichen? Indem wir wie oben schon einmal kurz erwähnt die strings in der Resourcen Datei, auch bekannt als STRINGTABLE in den Resourcen Segmenten, speichern. Um das zu tun, müssen sie zuerst einmal einen string mit dem Visual Studio Resourcen Editor erzeugen. Dem string wird eine string ID zugeordnet, normalerweise beginnend mit IDS_. Somit haben sie eine Nachricht, einen string wessen ID IDS_READING_FILE ist und einen anderen string mit der ID IDS_WRITING_FILE. Diese erscheinen in ihrer .rc Datei als: STRINGTABLE IDS_READING_FILE "Reading file %s" IDS_WRITING_FILE "Writing file %s" END Achtung: Diese Resourcen werden immer als Unicode strings gespeichert, unabhängig davon wie ihr Programm kompiliert wird. Sie sind sogar auf Win9x Plattformen Unicode strings, obwohl dort normalerweise Unicode strings nicht unterstützt werden. Das gilt aber eben nicht für die Resourcen! Gehen sie nun zu der Stelle wo die strings von ihnen gespeichert wurden // vorheriger code CString fmt; if(...) fmt = "Reading file %s"; else fmt = "Writing file %s"; ... // viel später CString s; s.Format(fmt, filename); und schreiben stattdessen das // überarbeiteter code CString fmt; if(...) fmt.LoadString(IDS_READING_FILE); else fmt.LoadString(IDS_WRITING_FILE); ... // viel später CString s; s.Format(fmt, filename); Nun können sie ihren Code in jede Sprache konvertieren. Die LoadString Methode nimmt eine string ID auf und erhält den für diese ID stellvertretenden STRINGTABLE Wert. Anschließend wird dieser dem CString zugeordnet. Es gibt ein kleveres Feature des CString Konstruktors welcher die Benutzung von STRINGTABLE Einträgen vereinfacht. Es ist nicht explizit in den CString::CString Spezifikationen dokumentiert, aber das Beispiel zur Benutzung des Konstruktors deutet es vage an. Warum es nicht Teil der offizellen Dokumentation ist weiß ich nicht. Das Feature ist, dass wenn sie eine STRINGTABLE ID in ein LPCTSTR casten implizit ein LoadString ausgeführt wird. Daher erreicht man mit den folgenden zwei Beispielen einen string zu generieren denselben Effekt und das ASSERT wird bei der Kompilierung im Debug Modus nicht auslösen. CString s; s.LoadString(IDS_WHATEVER); CString t( (LPCTSTR)IDS_WHATEVER); ASSERT(s == t); Sie fragen sich nun vielleicht wie das überhaupt funktionieren kann?! Wie kann es einen gültigen Pointer von einer STRINGTABLE ID bekommen? Ganz einfach. Alle string ID's liegen in einem Bereich zwischen 1 und 65536. Das bedeutet das die höherwertigen Bits des Pointers 0 sind. Zumindest auf gängigen 32-Bit Systemen auf denen ein Pointer 4 Byte groß ist. Klingt gut, aber was wenn ich Daten in einer sehr kleinen Adresse im RAM habe? Nun, die Antwort lautet schlicht und ergreifend. Das gibt es nicht! Derart niedrige verwendete Adressen in diesem Bereich existieren einfach nicht. Jeder Versuch auf Daten in diesem Adressbereich zwischen 0x00000001 bis 0x0000FFFF (1...65535) zuzugreifen wird immer in einem Zugriffsfehler enden. Diese Adressen sind in keinem Fall, zu keiner Zeit gültige Adressen. Deshalb muss ein Wert in diesem Bereich eine STRINGTABLE ID sein. Ich tendiere dazu das MAKEINTRESOURCE Makro für Castings zu benutzen. Ich denke das lässt den Code klarer erscheinen. Es handelt sich um ein Standard-Makro welches ansonsten in der MFC nicht viel Anwendung findet. Sie haben vielleicht bemerkt das viele Methoden entweder einen UINT oder einen LPCTSTR als Parameter annehmen, indem die C++ Überladungen genutzt werden. Das macht es unnötig, wie in C explizite Casts bei überladenen Methoden (in C sowieso nicht wirklich überladen) benutzen zu müssen. Das ist auch nützlich bei der Zuweisung von Resourcen Namen an verschiedene andere Strukturen. CString s; s.LoadString(IDS_WHATEVER); CString t( MAKEINTRESOURCE(IDS_WHATEVER)); ASSERT(s == t); Ich sage es ihnen nebenbei hier noch einmal. Ich praktiziere das was ich ihnen hier erzähle in meinen eigenen Projekten. Sie werden selten, wenn überhaupt, einen literalen string in meinen Programmen finden mit Ausnahme von gelegentlichen Debug Meldungen und natürlich jeglichen sprachunabhängigen strings. CStrings und temporäre ObjekteHier ein kleines Problem welches vor einer Weile in der microsoft.public.vc.mfc Newsgroup auftauchte. Ich werde es ein wenig vereinfachen. Das grundlegende Problem bestand darin das der Programmierer einen string in die Registry schreiben wollte. Nachfolgend möchte ich ihnen zwei Beiträge aus diesem Thread zeigen. Der erste stammt vom Fragesteller selbst und der zweite Beitrag von einem User welcher eine Lösung für das Problem aufzeigt:
|
||||||
Zuletzt aktualisiert am Mittwoch, den 22. August 2007 um 17:22 Uhr |
AUSWAHLMENÜ | ||||||||
|