Einzelnen Beitrag anzeigen

Benutzerbild von himitsu
himitsu

Registriert seit: 11. Okt 2003
Ort: Elbflorenz
43.157 Beiträge
 
Delphi 12 Athens
 
#1

mehrsprachige Resourcen nutzen

  Alt 26. Dez 2007, 20:25
Im Zuge einer mehrsprachigen Unterstütung
und da MSDN-Library durchsuchenLoadString keine Sprachauswahl bietet,
hab ich mir, nach mehreren Anläufen, folgenden Code einfallen lassen.

Die verschiedenen Sprachen liegen dabei alle im selben Ressourcenbereich.

Man braucht dafür nur meine Funktion und die nötigen Resourcen in der Anwendung (über .hModule kann auch eine externe Resource angegeben, z.B. eine DLL).
FindResourceLang sucht dann die vorhandenen Sprachen und bietet über Result den Index zur automatischen Vorauswahl.

Im zurückgegebenem Array stehen dann alle gefundenen Sprachresourcen, wo man sich nach Lust und Laune oder über den Index(Result) bediehnen kann.


Code 1:
Delphi-Quellcode:
Type TResTableString = packed Record
    Len: Word;
    Text: packed Array[0..0] of WideChar;
  End;
  PResTableString = ^TResTableString;
  TEnumResRec = packed Record
    hModule: THandle;
    ResType: PWideChar;
    ResName: PWideChar;
    LangIDs: packed Array of packed Record
      LangID: LANGID;
      Case Byte of
        0: (P: Pointer; Len: Integer);
        1: (Text: PResTableString); // RT_STRING
    End;
  End;
  PEnumResRec = ^TEnumResRec;

Const DefaultLang = LANG_ENGLISH or (SUBLANG_ENGLISH_US shl 10);

Var GUILang: LANGID = LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10);
  ResErrorStr: WideString;

Function FindResourceLang(Var LangResList: TEnumResRec): Integer;
  Function EnumResLangProcW(hModule: THandle; lpszType, lpszName: PWideChar; wIDLanguage: LANGID; lParam: PEnumResRec): LongBool; StdCall;
    Var hRes: HRSRC;
      hResLoad: THandle;
      ResP: PResTableString;
      ResL, i, i2: Integer;

    Begin
      Result := True;
      If lpszType = PWideChar(RT_STRING) Then Begin
        hRes := FindResourceExW(lParam^.hModule, lParam^.ResType,
                      MakeIntResourceW(Integer(lParam^.ResName) shr 4 + 1), wIDLanguage);
        hResLoad := LoadResource(lParam^.hModule, hRes);
        ResL := SizeOfResource(lParam^.hModule, hRes);
        ResP := LockResource(hResLoad);
        If ResP = nil Then Exit;
        i := ResL;
        For i2 := 1 to Integer(lParam^.ResName) and $0F do Begin
          Dec(i, (ResP^.Len + 1) * 2);
          Inc(PWideChar(ResP), ResP^.Len + 1);
          If i <= 0 Then Exit;
        End;
        If (ResP^.Len = 0) or ((ResP^.Len + 1) * 2 > i) Then Exit;
        ResL := ResP^.Len + 2;
      End Else Begin
        hRes := FindResourceExW(lParam^.hModule, lParam^.ResType, lParam^.ResName, wIDLanguage);
        hResLoad := LoadResource(lParam^.hModule, hRes);
        ResL := SizeOfResource(lParam^.hModule, hRes);
        ResP := LockResource(hResLoad);
      End;
      i := Length(lParam^.LangIDs);
      SetLength(lParam^.LangIDs, i + 1);
      lParam^.LangIDs[i].LangID := wIDLanguage;
      lParam^.LangIDs[i].P := ResP;
      lParam^.LangIDs[i].Len := ResL;
    End;

  Var ResName: PWideChar;
    LangX, MaskX: LANGID;
    i, i2: Integer;
    S, S2: WideString;

  Begin
    Result := -1;
    LangResList.LangIDs := nil;
    If (Cardinal(LangResList.ResName) <= $0000FFFF) and (LangResList.ResType = PWideChar(RT_STRING)) Then
      ResName := MakeIntResourceW(Integer(LangResList.ResName) shr 4 + 1)
    Else ResName := LangResList.ResName;
    EnumResourceLanguagesW(LangResList.hModule, LangResList.ResType, ResName, @EnumResLangProcW, Integer(@LangResList));
    If LangResList.LangIDs = nil Then Begin
      If Cardinal(LangResList.ResName) < $0000FFFF Then S := IntToStrT(Integer(LangResList.ResName))
      Else S := WideString('''') + LangResList.ResName + WideString('''');
      Case Integer(LangResList.ResType) of
        Integer(RT_STRING): Begin
          Result := 0;
          ResErrorStr := #0'[Resource ' + S + ' (' + 'RT_STRING' + ') not found]';
          ResErrorStr[1] := WideChar(Length(ResErrorStr) - 1);
          SetLength(LangResList.LangIDs, 1);
          LangResList.LangIDs[0].LangID := LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10);
          LangResList.LangIDs[0].Text := @ResErrorStr[1];
          LangResList.LangIDs[0].Len := Length(ResErrorStr) - 1;
          Exit;
        End;
        Integer(nil): S2 := WideString('''') + WideString('''');
        Integer(RT_CURSOR): S2 := 'RT_CURSOR';
        Integer(RT_BITMAP): S2 := 'RT_BITMAP';
        Integer(RT_ICON): S2 := 'RT_ICON';
        Integer(RT_MENU): S2 := 'RT_MENU';
        Integer(RT_DIALOG): S2 := 'RT_DIALOG';
        Integer(RT_FONTDIR): S2 := 'RT_FONTDIR';
        Integer(RT_FONT): S2 := 'RT_FONT';
        Integer(RT_ACCELERATOR): S2 := 'RT_ACCELERATOR';
        Integer(RT_RCDATA): S2 := 'RT_RCDATA';
        Integer(RT_MESSAGETABLE): S2 := 'RT_MESSAGETABLE';
        12..$FFFF: S2 := IntToStrT(Integer(LangResList.ResType));
        Else S2 := WideString('''') + LangResList.ResType + WideString('''');
      End;
      Exception(888, ['Resource ' + S + ' (type ' + S2 + ') not found.']);
    End;

    MaskX := $FFFF;
    For i2 := 0 to 9 do Begin
      Case i2 of
        0, 4: If GUILang = LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10) Then Continue Else LangX := GUILang;
        1, 5: LangX := Word(GetThreadLocale);
        2, 6: LangX := GetUserDefaultLangID;
        3, 7: LangX := GetSystemDefaultLangID;
        8: LangX := DefaultLang;
        Else LangX := LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10);
      End;
      If (i2 = 4) or (LangX and not $03FF = 0) Then MaskX := $03FF;
      For i := High(LangResList.LangIDs) downto 0 do
        If LangResList.LangIDs[i].LangID and MaskX = LangX and MaskX Then
          Result := i;
      If Result >= 0 Then Break;
    End;
    If Result < 0 Then Result := 0;
  End;
wird z.B. so verwendet;
Delphi-Quellcode:
Var LangResList: TEnumResRec;
  Language: Integer;
  StrBuffer: Array[0..1023] of WideChar;
  i: Integer;

LangResList.hModule := HInstance;
LangResList.ResType := PWideChar(RT_STRING);
LangResList.ResName := {MakeIntResourceW(MsgID)};
Language := FindResourceLang(LangResList);

i := Min(Integer(LangResList.LangIDs[Language].Text^.Len * 2), SizeOf(StrBuffer) - 2);
CopyMemory(@StrBuffer, @LangResList.LangIDs[Language].Text^.Text, i);
StrBuffer[i div 2] := #0;
Delphi-Quellcode:
Var LangResList: TEnumResRec;
  Language: Integer;

LangResList.hModule := HInstance;
LangResList.ResType := PWideChar(RT_DIALOG);
LangResList.ResName := {MakeIntResourceW(DlgID)};
Language := FindResourceLang(LangResList);
Result := DialogBoxIndirectParamW(HInstance, PDlgTemplate(LangResList.LangIDs[Language].P)^, ...);
Code 2 (ohne Exception):
Delphi-Quellcode:
Type TResTableString = packed Record
    Len: Word;
    Text: packed Array[0..0] of WideChar;
  End;
  PResTableString = ^TResTableString;
  TEnumResRec = packed Record
    hModule: THandle;
    ResType: PWideChar;
    ResName: PWideChar;
    LangIDs: packed Array of packed Record
      LangID: LANGID;
      Case Byte of
        0: (P: Pointer; Len: Integer);
        1: (Text: PResTableString); // RT_STRING
    End;
  End;
  PEnumResRec = ^TEnumResRec;

Const DefaultLang = LANG_ENGLISH or (SUBLANG_ENGLISH_US shl 10);

Var GUILang: LANGID = LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10);

Function FindResourceLang(Var LangResList: TEnumResRec): Integer;
  Function EnumResLangProcW(hModule: THandle; lpszType, lpszName: PWideChar; wIDLanguage: LANGID; lParam: PEnumResRec): LongBool; StdCall;
    Var hRes: HRSRC;
      hResLoad: THandle;
      ResP: PResTableString;
      ResL, i, i2: Integer;

    Begin
      Result := True;
      If lpszType = PWideChar(RT_STRING) Then Begin
        hRes := FindResourceExW(lParam^.hModule, lParam^.ResType,
                      MakeIntResourceW(Integer(lParam^.ResName) shr 4 + 1), wIDLanguage);
        hResLoad := LoadResource(lParam^.hModule, hRes);
        ResL := SizeOfResource(lParam^.hModule, hRes);
        ResP := LockResource(hResLoad);
        If ResP = nil Then Exit;
        i := ResL;
        For i2 := 1 to Integer(lParam^.ResName) and $0F do Begin
          Dec(i, (ResP^.Len + 1) * 2);
          Inc(PWideChar(ResP), ResP^.Len + 1);
          If i <= 0 Then Exit;
        End;
        If (ResP^.Len = 0) or ((ResP^.Len + 1) * 2 > i) Then Exit;
        ResL := ResP^.Len + 2;
      End Else Begin
        hRes := FindResourceExW(lParam^.hModule, lParam^.ResType, lParam^.ResName, wIDLanguage);
        hResLoad := LoadResource(lParam^.hModule, hRes);
        ResL := SizeOfResource(lParam^.hModule, hRes);
        ResP := LockResource(hResLoad);
      End;
      i := Length(lParam^.LangIDs);
      SetLength(lParam^.LangIDs, i + 1);
      lParam^.LangIDs[i].LangID := wIDLanguage;
      lParam^.LangIDs[i].P := ResP;
      lParam^.LangIDs[i].Len := ResL;
    End;

  Var ResName: PWideChar;
    LangX, MaskX: LANGID;
    i, i2: Integer;

  Begin
    Result := -1;
    LangResList.LangIDs := nil;
    If (Cardinal(LangResList.ResName) <= $0000FFFF) and (LangResList.ResType = PWideChar(RT_STRING)) Then
      ResName := MakeIntResourceW(Integer(LangResList.ResName) shr 4 + 1)
    Else ResName := LangResList.ResName;
    EnumResourceLanguagesW(LangResList.hModule, LangResList.ResType, ResName, @EnumResLangProcW, Integer(@LangResList));
    If LangResList.LangIDs = nil Then Exit;

    MaskX := $FFFF;
    For i2 := 0 to 9 do Begin
      Case i2 of
        0, 4: If GUILang = LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10) Then Continue Else LangX := GUILang;
        1, 5: LangX := Word(GetThreadLocale);
        2, 6: LangX := GetUserDefaultLangID;
        3, 7: LangX := GetSystemDefaultLangID;
        8: LangX := DefaultLang;
        Else LangX := LANG_NEUTRAL or (SUBLANG_NEUTRAL shl 10);
      End;
      If (i2 = 4) or (LangX and not $03FF = 0) Then MaskX := $03FF;
      For i := High(LangResList.LangIDs) downto 0 do
        If LangResList.LangIDs[i].LangID and MaskX = LangX and MaskX Then
          Result := i;
      If Result >= 0 Then Break;
    End;
    If Result < 0 Then Result := 0;
  End;
Delphi-Quellcode:
Var LangResList: TEnumResRec;
  Language: Integer;

LangResList.hModule := ...;
LangResList.ResType := ...;
LangResList.ResName := ...;
Language := FindResourceLang(LangResList);
If Language < 0 Then {Error}
...
DefaultLang die Sprache in der eure Anwendung hauptsächlich perstellt wurde
also die wo möglichst alle Resourcen vorhanden sind
Tipp: Englisch macht sich gut, damit erhöht sich die Wahrscheinlichkeit daß jemand, dessen Sprache nicht unterstützt wird, dennoch was entziffern kann (Englisch ist ja angeblich 'ne Weltsprache)
GUILang kann vom Programmierer zur Auswahl der Sprache passend definiert werden.
ResErrorStr wird intern zum Speichern des Fehlertextes benötigt.

Nach einer passenden Sprache wird in dieser Reinfolge gesucht:
Code:
GUILang
ThreadLangID
UserDefaultLangID
SystemDefaultLangID

GUILang (ignore SUBLANG_*)
ThreadLangID (ignore SUBLANG_*)
UserDefaultLangID (ignore SUBLANG_*)
SystemDefaultLangID (ignore SUBLANG_*)
DefaultLang (ignore SUBLANG_*)
LANG_NEUTRAL

erste gefungene Sprache
wurde keine Resource gefungen, so wird eine Exception ausgelößt und für RT_STRING ein Fehlertext zurückgegeben
oder es kommt der Rückgabewert -1 (beim zweiten Code).


in der Ressourcendatei (.RC) sähe es dann z.B. so aus:
Code:
STRINGTABLE
  LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
  BEGIN
    123 "an example text"
  END

STRINGTABLE
  LANGUAGE LANG_GERMAN, SUBLANG_GERMAN
  BEGIN
    123 "ein Beispieltext"
  END

100 DIALOGEX 0, 0, 193, 122
  STYLE   ...
  FONT    ...
  LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
  CAPTION "..."
  BEGIN
    ...
  END


Zum auslesen vorhandener Sprachen kann die selbe Funktion mißbraucht werden:
Delphi-Quellcode:
Var LangResList: TEnumResRec;

LangResList.hModule := HInstance;
LangResList.ResType := ...; // vorhandene Resource,
LangResList.ResName := ...; // von welcher die Sprachen ausgelesenwerden
FindResourceLang(LangResList);
For i := 0 to High(LangResList.LangIDs) do
  {LangResList.LangIDs[i].LangID}
diese ließe sich ganz gut über die Sprachnamen (welche man natürlich mit speichern müßte) machen:
Delphi-Quellcode:
Var LangResList: TEnumResRec;
  S: WideString;

LangResList.hModule := HInstance;
LangResList.ResType := PWideChar(RT_STRING);
LangResList.ResName := MakeIntResourceW(100);
FindResourceLang(LangResList);
For i := 0 to High(LangResList.LangIDs) do Begin
  SetLength(S, LangResList.LangIDs[i].Text^.Len);
  CopyMemory(@S[1], @LangResList.LangIDs[i].Text^.Text,
    LangResList.LangIDs[i].Text^.Len * 2);
  List.Add(S, LangResList.LangIDs[i].LangID);
End;

// STRINGTABLE
// LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// BEGIN
// 100 "english"
// ...
// END
//
// STRINGTABLE
// LANGUAGE LANG_GERMAN, SUBLANG_GERMAN
// BEGIN
// 100 "Deutsch"
// ...
// END
//
// STRINGTABLE
// LANGUAGE LANG_GERMAN, SUBLANG_GERMAN_SWISS
// BEGIN
// 100 "schweizer Deutsch"
// ...
// END
Man könnte zwar die LangID entziffern und darüber den Sprachnamen erhalten, aber dafür bräuchte man dann in der Anwendung eine Liste aller möglichen LANG_* und SUBLANG_* Kombinationen.
Außerdem finde ich es besser den Sprachnamen in der entsprechenden Sprache zu speichern.
Windows hat da zwar auch irgendwo 'ne Liste, aber diese ist nur in der Systemsprache (von Windows) vorhanden.
(oder wer von euch kann alle Sprachen, um dann rauszufinden was dann in 'ner Liste "deutsch" heißt?)
PS: das Menü zur Sprachänderung sollte entweder Bildlich gekennzeichnet oder in englich (können ja wohl die Meißten) benannt sein.


Im Anhang hab ich das Ganze mal in ein Demoprojekt ausgelagert und unter Anderem auch mal eine LoadStringLangW erstellt, sowie eine Unterstütung für mehrere Resourcendateien entwickelt.


Das ganze funktioniert zwar (hab's "erfolgreich" in 'nem Projekt drin), aber für Verbesserungen bin ich dennoch dankbar.

vorallem die Reinfolge der Sprachen der Autoauswahl beschäftigt mich immernoch sehr.
So wäre es z.. auch möglich,
Code:
GUILang
GUILang (ignore SUBLANG_*)
ThreadLangID
ThreadLangID (ignore SUBLANG_*)
UserDefaultLangID
UserDefaultLangID (ignore SUBLANG_*)
SystemDefaultLangID
SystemDefaultLangID (ignore SUBLANG_*)
DefaultLang (ignore SUBLANG_*)
LANG_NEUTRAL
erste gefungene Sprache
aber da gibt's Probleme mit den Untersprachen (SUBLANG_*)

wenn z.B. Chinesisch-Simple ausgewählt wird und nur Chinesich-Traditional gefunden wird, aber der Benutzer das Trationale nicht lesen kann, dann wär's ja blöd ihm dieses zu präsentieren

[edit=sakura] Anhang auf Wunsch entfernt, neuer weiter unten. Mfg, sakura[/edit]
Garbage Collector ... Delphianer erzeugen keinen Müll, also brauchen sie auch keinen Müllsucher.
my Delphi wish list : BugReports/FeatureRequests
  Mit Zitat antworten Zitat