Blog Mateusza Chodyły czyli interdyscyplinarnego .NETowca 8-)

Część 7.1 (Globalization, drawing, text manipulation; globalizacja)

20. marca 2010 16:36

Minimalne wymagania:

  • Klasy: CultureInfo, RegionInfo, DateTimeFormatInfo, NumberFormatInfo, CompareInfo, CultureAndRegionInfoBuilder,
  • Powiązane wyliczenia: CultureTypes, NumberStyles, CompareOptions, CultureAndRegionModifier.

Globalizacja - proces tworzenia aplikacji, która obsługuje zlokalizowane interfejsy użytkownika i dane regionalne dla użytkowników w wielu kulturach. Lokalizacja - proces tłumaczenia zasobów aplikacji na lokalizowane wersje dla każdej kultury, które aplikacja będzie obsługiwać. Egzamin skupia się na formatowaniu i porównywaniu danych w różnych kulturach.

Co to jest kultura? Definiuje ona kalendarze, konwencje formatowania danych, czasu, liczb, walut a także zasady sortowania. Administratorzy ustawiają bieżącą kulturę, jednak można ją unieważnić w obrębie bieżącej aplikacji - 2 własności klasy Thread (pobierz przez Thread.CurrentThread):

  • public CultureInfo CurrentCulture { get; set; },
  • public CultureInfo CurrentUICulture { get; set; }.

Odnośnie ostatniej własności należy zaznaczyć, że obiekt CultureInfo przez nią zwracany może być neutralną kulturą. Neutralne kultury nie powinny być używane z metodami formatującymi takimi jak String.Format(IFormatProvider, String, Object[]), DateTime.ToString(String, IFormatProvider) i Convert.ToString(Char, IFormatProvider). Przykład:

public static void Main()
{
  Thread.CurrentThread.CurrentCulture = new CultureInfo("pl-PL"); // kultura dla bieżącego wątku
  Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES"); // kultura używana przez Resource Managera do określania zasobów dla określonej kultury podczas runtime'u
  double d = 1234567.89;
  MessageBox.Show(d.ToString("C")); // w zł, a nie w euro
  MessageBox.Show(d.ToString("C", new CultureInfo("en-GB"))); // string format, IFormatProvider provider; w funtach, inne separatory (dziesiętny i zmiennoprzecinkowy)

  wypiszKultury(CultureTypes.AllCultures); // wszystkie: neutralne, specyficzne, zainstalowane w Windows i utworzone przez użytkownika
  wypiszKultury(CultureTypes.NeutralCultures); // neutralne - powiązane tylko z językiem, a nie z określonym krajem/regionem (tylko język)
  wypiszKultury(CultureTypes.SpecificCultures); // specyficzne/konkretne - dla kraju/regionu (język + regionalne definicje formatowania)
  
  long acctNumber = 104254567890;
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0:H}", acctNumber)); // IFormatProvider provider, string format, Object[] args
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0}", acctNumber));
  acctNumber = 14567890;
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0:H}", acctNumber));
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0}", acctNumber));
  acctNumber = 18779887654111;
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0:H}", acctNumber));
  Console.WriteLine(String.Format(new AcctNumberFormat(), "{0}", acctNumber));
}

static void wypiszKultury(CultureTypes t)
{
  CultureInfo[] cis = CultureInfo.GetCultures(t);
  Console.WriteLine("---" + t.ToString() + "---");
  for (int i = 0; i < cis.Length; i++)
  {
    if (i == 25)
      break;
    else
      Console.WriteLine(cis[i].DisplayName + " [" + cis[i].EnglishName + "]");
  }
}

public class AcctNumberFormat : IFormatProvider, ICustomFormatter
{
  private const int ACCT_LENGTH = 12;

  public object GetFormat(Type formatType) // IFormatProvider
  {
    if (formatType == typeof(ICustomFormatter))
      return this;
    else
      return null;
  }

  public string Format(string fmt, object arg, IFormatProvider formatProvider) // ICustomFormatter
  {
    string result = arg.ToString();

    if (result.Length < ACCT_LENGTH)
      result = result.PadLeft(ACCT_LENGTH, '0'); // wypełnij zerami

    if (result.Length > ACCT_LENGTH)
      result = result.Substring(0, ACCT_LENGTH); // obetnij

    if (!String.IsNullOrEmpty(fmt) && fmt.ToUpper() == "H")
      return result.Substring(0, 5) + "-" + result.Substring(5, 3) + "-" + result.Substring(8);
    else
      return result;
  }
}

Interfejs IFormatProvider dostarcza mechanizmu do pobierania obiektu do kontroli formatowania. Implementowany m.in. przez NumberFormatInfo, DateTimeFormatInfo i CultureInfo.

Klasa CultureInfo ma m.in. następujące własności:

  • public virtual CompareInfo CompareInfo { get; } - definiuje jak porównywać stringi dla kultury (tylko get!),
  • public virtual DateTimeFormatInfo DateTimeFormat { get; set; } - odpowiedni format dla wyświetlania dat i czasów,
  • public static CultureInfo InvariantCulture { get; } - powiązana z językiem angielskim, ale nie powiązana z żadnym krajem/regionem,
  • public virtual NumberFormatInfo NumberFormat { get; set; } - odpowiedni format dla wyświetlania liczb, waluty i procentów,
  • public virtual TextInfo TextInfo { get; } - definiuje system pisania (writing system) powiązany z kulturą,
  • public bool UseUserOverride { get; } - czy bieżąca CultureInfo używa ustawień kultury wybranej przez użytkownika (użytkownik może unieważnić niektóre wartości powiązane z bieżącą kulturą Windows w Panelu Sterowania).

DateTimeFormatInfo i NumberFormatInfo może być utworzona tylko dla niezmiennej (invariant) lub dla specyficznej kultury (culture) /nie dla neutralnych/.

Klasy NumberFormatInfo i DateTimeFormatInfo definiują jak odpowiednio wartości liczbowe i wartości DateTime są formatowane i wyświetlane w zależności od kultury.

public static void Main()
{
  StringBuilder sb = new StringBuilder();
  foreach (CultureInfo ci in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
  {
    if (ci.TwoLetterISOLanguageName != "en")
      continue;

    NumberFormatInfo nfi = ci.NumberFormat;
    sb.AppendFormat("The currency symbol for '{0}' is '{1}'", ci.DisplayName, nfi.CurrencySymbol);
    sb.AppendLine();
  }
  MessageBox.Show(sb.ToString());

  double d = 3.145678;
  CultureInfo ci_pl_PL = new CultureInfo("pl-PL");
  ci_pl_PL.NumberFormat.CurrencySymbol = "$";
  Thread.CurrentThread.CurrentCulture = ci_pl_PL; // musisz stworzyć nową i przypisać
  Console.WriteLine("C", d); // niepoprawne - wypisze znak 'C'
  Console.WriteLine("{0:C}", d); // 3,15 $
  Console.WriteLine(d.ToString("C")); // 3,15 $
  ci_pl_PL.NumberFormat.CurrencyDecimalDigits = 3;
  Console.WriteLine(d.ToString("C")); // 3,146 $

  ci_pl_PL.NumberFormat.PercentPositivePattern = 0;
  Console.WriteLine(d.ToString("p4")); // bez klamer; 314,5678 %
  ci_pl_PL.NumberFormat.PercentPositivePattern = 1; // zwróć uwagę na brak spacji pomiędzy liczbą a znakiem %
  Console.WriteLine(d.ToString("p2")); // 314,57%

  NumberFormatInfo nfi_en_US = new CultureInfo("en-US", false).NumberFormat; // bool useUserOverride
  Int64 myInt64 = 123456789012345;
  Console.WriteLine(myInt64.ToString("C", nfi_en_US)); // $123,456,789,012,345.00
  int[] mySizes1 = { 2, 3, 4 };
  nfi_en_US.CurrencyGroupSizes = mySizes1;
  Console.WriteLine(myInt64.ToString("C", nfi_en_US)); // $12,3456,7890,123,45.00

  DateTimeFormatInfo dtfi = new CultureInfo("en-US", false).DateTimeFormat; // false - używa domyślnych ustawień kultury
  Console.WriteLine(dtfi.GetAbbreviatedDayName(DayOfWeek.Monday)); // Mon

  // poniższe to gettery/settery
  Console.WriteLine(dtfi.AMDesignator); // AM
  Console.WriteLine(dtfi.LongDatePattern); // D -> dddd, MMMM dd, yyyy
  Console.WriteLine(dtfi.LongTimePattern); // T -> h:mm:ss tt
  Console.WriteLine(dtfi.MonthDayPattern); // m/M -> MMMM dd
  Console.WriteLine(dtfi.ShortDatePattern); // d -> M/d/yyyy
  Console.WriteLine(dtfi.ShortTimePattern); // t -> h:mm tt
  Console.WriteLine(dtfi.SortableDateTimePattern); // s -> yyyy'-'MM'-'dd'T'HH':'mm':'ss
  Console.WriteLine(dtfi.UniversalSortableDateTimePattern); // u -> yyyy'-'MM'-'dd HH':'mm':'ss'Z'
  Console.WriteLine(dtfi.YearMonthPattern); // y/Y -> MMMM, yyyy

  double value = 12345.6789;
  // C lub c = Currency
  Console.WriteLine(value.ToString("C3", new CultureInfo("en-US"))); // $12,345.679
  Console.WriteLine(value.ToString("C3", CultureInfo.CreateSpecificCulture("en-US"))); // $12,345.679

  // D lub d = Decimal
  try
  {
    Console.WriteLine(value.ToString("D"));
  }
  catch (FormatException)
  {
    Console.WriteLine("FormatException");
  }
  int value_ing = 12345;
  Console.WriteLine(value_ing.ToString("D"));  // tylko całkowite; 12345
  Console.WriteLine(value_ing.ToString("D8")); // 00012345

  // F lub f = Fixed-point
  double integerNumber = -29541;
  Console.WriteLine(integerNumber.ToString("F3", CultureInfo.InvariantCulture)); // -29541.000

  // placeholdery:
  Double myDouble = 1234567890;
  Console.WriteLine(myDouble.ToString("(###) ### - ####")); // (123) 456 - 7890

  value = .086;
  Console.WriteLine(value.ToString("#0.##%", CultureInfo.InvariantCulture)); // 8.6%

  value = 1.2;
  Console.WriteLine(value.ToString("0.00", CultureInfo.InvariantCulture)); // 1.20
  Console.WriteLine(value.ToString("00.00", CultureInfo.InvariantCulture)); // 01.20
  Console.WriteLine(value.ToString("00.00", CultureInfo.CreateSpecificCulture("da-DK"))); // 01,20
}

Klasa CompareInfo implementuje zbiór metod dla porównywania ciągów z zachowaniem informacji o kulturze. Są 2 możliwości pobrania obiektu CompareInfo: użycie własności CultureInfo.CompareInfo lub metody CompareInfo.GetCompareInfo. Operacje porównywania (m.in. te wykonywane przez IndexOf lub LastIndexOf) mogą dać nieoczekiwane wyniki, jeśli szukana wartość jest ignorowana (nawiasem mówiąc jeśli szukasz pustego ciągu w IndexOf, to zwrócona wartość jest równa 0). Powinieneś/aś używać InvariantCulture do zapewnienia spójnego zachowania niezależnie od ustawień kultury OSa - 2 sposoby:

  • Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture,
  • s.IndexOf("AE", StringComparison.InvariantCulture));

Różne kultury wykonują sortowanie w różny sposób np. dla znaku AE:

  • szwedzki: po Z,
  • niemiecki: po A,
  • angielski (znak specjalny): przed A.

Co ciekawe niektóre kultury obsługują więcej niż jeden porządek sortowania np. zh-CN (chiński, Chiny). Przykład:

public static void Main()
{
  string[] words = new string[] { "Apple", "Æble" };
  Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
  SortWords(words);
  Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK");
  SortWords(words);

  words = new string[] { "AEble", "Æble" };
  Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
  FindAE(words);
  Thread.CurrentThread.CurrentCulture = new CultureInfo("da-DK");
  FindAE(words);

  Thread.CurrentThread.CurrentCulture = new CultureInfo(0x00000407); // de-DE, Dictionary sort
  Thread.CurrentThread.CurrentCulture = new CultureInfo(0x00010407); // de-DE, Phone Book Sort DIN sort
  // dla zh-CN: Pronunciation: 0x00000804, Stroke Count: 0x00020804

  String[] sign = new String[] { "<", "=", ">" };
  String s1 = "Coté", s2 = "coté", s3 = "côte";
  CompareInfo ci = new CultureInfo("fr-FR").CompareInfo;
  Console.WriteLine("The LCID for {0} is {1}.", ci.Name, ci.LCID);
  Console.WriteLine("fr-FR Compare: {0} {2} {1}", s1, s2, sign[ci.Compare(s1, s2, CompareOptions.IgnoreCase) + 1]);
  Console.WriteLine("fr-FR Compare: {0} {2} {1}", s2, s3, sign[ci.Compare(s2, s3, CompareOptions.None) + 1]);

  String myStr1 = "calle";
  String myStr2 = "llegar";
  String myXfix = "lle";
  CompareInfo myComp = CultureInfo.InvariantCulture.CompareInfo;
  Console.WriteLine("IsPrefix( {0}, {1} ) : {2}", myStr1, myXfix, myComp.IsPrefix(myStr1, myXfix));
  Console.WriteLine("IsPrefix( {0}, {1} ) : {2}", myStr2, myXfix, myComp.IsPrefix(myStr2, myXfix));
  Console.WriteLine("IsSuffix( {0}, {1} ) : {2}", myStr1, myXfix, myComp.IsSuffix(myStr1, myXfix));
  Console.WriteLine("IsSuffix( {0}, {1} ) : {2}", myStr2, myXfix, myComp.IsSuffix(myStr2, myXfix));

  Console.WriteLine(String.Compare("Apple", "Æble", false, CultureInfo.InvariantCulture));
  Console.WriteLine(String.Compare("Apple", "Æble", false, CultureInfo.GetCultureInfo("da-DK")));
  string str = "co", str2 = "co";
  Console.WriteLine(str.CompareTo(str2));

  String num = "A";
  int val = int.Parse(num, NumberStyles.HexNumber);
  Console.WriteLine("{0} in hex = {1} in decimal.", num, val);
  num = "    -45   ";
  val = int.Parse(num, NumberStyles.AllowLeadingSign | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite);
  Console.WriteLine("'{0}' parsed to an int is '{1}'.", num, val);
}

private static void SortWords(string[] words)
{
  Console.WriteLine("-> " + Thread.CurrentThread.CurrentCulture);
  Array.Sort(words);
  foreach (string s in words)
    Console.WriteLine(s);
}

private static void FindAE(string[] words)
{
  Console.WriteLine("-> " + Thread.CurrentThread.CurrentCulture);
  Array.Sort(words);
  foreach (string s in words)
  {
    Console.WriteLine(" AE in {0}: {1}", s, s.IndexOf("AE"));
    Console.WriteLine(" Æ in {0}: {1}", s, s.IndexOf("Æ"));
  }
}

Wynik działania:

-> en-US
Able
Apple
-> da-DK
Apple
Able
-> en-US
AE in Able: 0
A in Able: 0
AE in AEble: 0
A in AEble: 0
-> da-DK
AE in AEble: 0
A in AEble: -1
AE in Able: -1
A in Able: 0
The LCID for fr-FR is 1036.
fr-FR Compare: Coté = coté
fr-FR Compare: coté > côte
IsPrefix( calle, lle ) : False
IsPrefix( llegar, lle ) : True
IsSuffix( calle, lle ) : True
IsSuffix( llegar, lle ) : False
1
-1
0
A in hex = 10 in decimal.
'    -45   ' parsed to an int is '-45'.

Klasa RegionInfo zawiera informacje o kraju/regionie. W przeciwieństwie do CultureInfo nie reprezentuje preferencji użytkownika i nie polega na języku lub kulturze użytkownika. Podczas tworzenia obiektu RegionInfo powinieneś używać “pełnej” nazwy np. en-US zamiast samego kodu kraju np. US.

RegionInfo myRI1 = new RegionInfo("US"); // lub en-US
Console.WriteLine("Name:                         {0}", myRI1.Name);
Console.WriteLine("DisplayName:                  {0}", myRI1.DisplayName);
Console.WriteLine("EnglishName:                  {0}", myRI1.EnglishName);
Console.WriteLine("IsMetric:                     {0}", myRI1.IsMetric);
Console.WriteLine("ThreeLetterISORegionName:     {0}", myRI1.ThreeLetterISORegionName);
Console.WriteLine("ThreeLetterWindowsRegionName: {0}", myRI1.ThreeLetterWindowsRegionName);
Console.WriteLine("TwoLetterISORegionName:       {0}", myRI1.TwoLetterISORegionName);
Console.WriteLine("CurrencySymbol:               {0}", myRI1.CurrencySymbol);
Console.WriteLine("ISOCurrencySymbol:            {0}\n", myRI1.ISOCurrencySymbol);

RegionInfo myRI2 = new RegionInfo(new CultureInfo("en-US", false).LCID);
if (myRI1.Equals(myRI2))
  Console.WriteLine("The two RegionInfo instances are equal."); // będą równe
else
  Console.WriteLine("The two RegionInfo instances are NOT equal.");

Ostatnią klasą do omówienia jest CultureAndRegionInfoBuilder. Jak nazwa wskazuje umożliwia ona definiowanie własnych kultur bazujących na innych lub całkiem nowych kulturach i krajach/regionach. Później taka kultura może być zainstalowana (zarejestrowana - tylko z prawami admina) na komputerze i używana także przez inne aplikacje. Klasa znajduje się w sysglobl.dll /zakładka .NET/. Członki (wybrane):

  • public CultureAndRegionInfoBuilder(string cultureName, CultureAndRegionModifiers flags); flagi CultureAndRegionModifiers:
    • None - określona dodatkowa kultura,
    • Neutral - neutralna,
    • Replacement - zamienia istniejącą kulturę .NET Framework lub ustawienia lokalne (locale) Windows,
  • public void LoadDataFromCultureInfo(CultureInfo culture),
  • public void LoadDataFromRegionInfo(RegionInfo region),
  • public void Register() - zapisuje bieżący obiekt i udostępnia kulturę aplikacjom (uprawnienia admina - więc najlepiej podczas instalacji),
  • public static void Unregister(string cultureName) - usuwa powyższą (z %WINDIR%\Globalization),
  • public void Save(string filename) - zapisuje reprezentację XML do pliku,
  • public static CultureAndRegionInfoBuilder CreateFromLdml(string xmlFileName) - odczytuje powyższą,
  • public CompareInfo CompareInfo { get; set; },
  • public CultureInfo ConsoleFallbackUICulture { get; set; } - alternatywna kultura UI odpowiednia dla aplikacji konsolowych,
  • public CultureTypes CultureTypes { get; },
  • public string CurrencyNativeName { get; set; },
  • public bool IsMetric { get; set; },
  • public NumberFormatInfo NumberFormat { get; set; },
  • public int LCID { get; },
  • public string RegionEnglishName { get; set; },
  • public TextInfo TextInfo { get; set; },
  • public string TwoLetterISOLanguageName { get; set; }.

Przykład:

CultureAndRegionInfoBuilder cib = new CultureAndRegionInfoBuilder("pl-US", CultureAndRegionModifiers.None);
cib.LoadDataFromCultureInfo(new CultureInfo("pl-PL"));
cib.LoadDataFromRegionInfo(new RegionInfo("US"));
cib.CultureEnglishName = "Polish Greenpoint";
cib.CultureNativeName = "Polski Greenpoint";
cib.IsMetric = true;
cib.ISOCurrencySymbol = "PLN";
cib.RegionEnglishName = "Polish Greenpoint Region";
cib.RegionNativeName = "Region polski Greenpoint";
cib.Register();

CultureInfo ci = new CultureInfo("pl-US");
Console.WriteLine("Name: . . . . . . . . . . . . . {0}", ci.Name);
Console.WriteLine("EnglishName:. . . . . . . . . . {0}", ci.EnglishName);
Console.WriteLine("NativeName: . . . . . . . . . . {0}", ci.NativeName);
Console.WriteLine("TwoLetterISOLanguageName: . . . {0}", ci.TwoLetterISOLanguageName);
Console.WriteLine("ThreeLetterISOLanguageName: . . {0}", ci.ThreeLetterISOLanguageName);
Console.WriteLine("ThreeLetterWindowsLanguageName: {0}", ci.ThreeLetterWindowsLanguageName);
CultureAndRegionInfoBuilder.Unregister("pl-US");

Do końca zostały jeszcze 2 przyjemne tematy: rysowanie i obsługa tekstów.

Tagi:

70-536

Dodaj komentarz





  • Komentarz
  • Podgląd
Loading



Zmodyfikowany BlogEngine.NET (Bazowa szata graficzna: Mads Kristensen/Zdjęcia panoramiczne)
Hosting dzięki uprzejmości PCSS/Centrum Innowacji Microsoft
(c) Mateusz Chodyła (Panel logowania)