26/05/2011

Aplikacje wielojęzyczne - WinForms

Home

Przyszła pora wrócić do tematu aplikacji wielojęzycznych. Tym razem skupię się na WinForms. Zacznę od tego, że część rzeczy, o których pisałem we wcześniejszym poście na temat aplikacji ASP.NET można zastosować do innych technologii, w szczególności do WinForms. Dla przypomnienia:
  • Jeśli chcemy aby nasza aplikacja miała wiele wersji językowych to przygotowujmy się do tego od pierwszej linijki tej aplikacji.
  • Stałe znakowe w kodzie są złe, bardzo złe, niewyobrażalnie złe... Stałe zawierające komunikaty dla użytkownika itp. powinny znajdować się w zasobach aplikacji, a pozostałe, nazwijmy je techniczne powinny zostać zdefiniowany w jednym konkretnym miejscu np.: klasie o nazwie Constans.
Tyle tytułem wstępu. Kiedy przystępowałem do prac nad aplikacją WinForms byłem o tyle w gorszej sytuacji w porównaniu do wcześniejszych prac nad aplikacją ASP.NET, że nie wiedziałem o żadnych narzędziach wbudowanych w Visual Studio wspomagających lokalizację WinForms. Co do samego mechanizmu przełączania się pomiędzy różnymi wersjami językowymi zasobów to wygląda to tak samo jak w ASP.NET. Mamy więc odpowiednio ponazywane pliki np.: Resources.resx, Resources.en.resx itd. zawierające zasoby dla poszczególnych języków (kultur). Teraz w zależności od tego jaką kulturę ustawimy na właściwości Thread.CurrentThread.CurrentUICulture taka wersja zasobu zostanie wczytana. Na samym początku pomyślałem więc aby przejrzeć kod wygenerowany przez designer i w nim pozamieniać stałe znakowe na odwołania do zasobów. Podobnie postąpiłem przecież dla ASP.NET. Czyli poniższy kod:
...
// 
// cancelButton
// 
this.cancelButton.Location = new System.Drawing.Point(85, 103);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.Text = "Anuluj";
...
Zamienić na taki:
...
// 
// cancelButton
// 
this.cancelButton.Location = new System.Drawing.Point(85, 103);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.Text = Resources.Cancel;
...
To nie jest jednak dobry pomysł przynajmniej z kilku powodów. Kod generowany przez designer WinForms nie jest przeznaczony do samodzielnej modyfikacji. Podejście to zadziała ale wystarczy, że ktoś użyje designera aby zmienić położenie kontrolki lub zrobić coś równie prostego, a kod zostanie ponownie wygenerowany, a nasze zmiany usunięte. Po drugie przeglądanie kodu designera w poszukiwaniu stałych znakowych to żmudna, nudna i błędogenna robota. W następnej kolejności pomyślałem więc o innym rozwiązaniu:
public Form1()
{
  InitializeComponent();
  cancelButton.Text = Resources.Cancel;
  ...
}
Czyli zanim wyświetlimy formę, pobieramy zasoby i lokalizujemy GUI. Podejście bardzo proste, wręcz prymitywne. Można je trochę udoskonalić na przykład dodać do bazowej klasy metodę LocalizeGUI:
public class BaseForm : Form
{
  protected override void OnShown(EventArgs e)
  {
    LocalieGUI();
    base.OnShown(e);
  }

  protected virtual void LocalieGUI()
  {}
}
...
public partial class Form1 : BaseForm
{
  protected override void LocalieGUI()
  {
    cancelButton.Text = Resources.Cancel;
    ...
  }
}
Również bardzo proste rozwiązanie i można jeszcze dużo w nim zmienić. Zasadniczy problem polega jednak na tym, że takie rzeczy sprawdzają się kiedy używa się ich od początku. Ja dostałem gotową aplikację i jak wyobraziłem sobie przeglądanie całego kodu w poszukiwaniu etykiet, przycisków itd., a następnie przenoszenie ustawiania tekstu wyświetlanego użytkownikowi do metody LocalizeGUI to mi się odechciało.

Na szczęście istnieje dużo lepsze rozwiązanie problemu. Otóż Visual Studio posiada wsparcie dla wielojęzycznych WinForms i w przeciwieństwie do narzędzia Tools->Generate Local Resources dla ASP.NET działa całkiem dobrze. Używa się go bardzo prosto:
  • Otwieramy designer dla formy (kontrolki użytkownika).
  • Otwieramy okno właściwości (Properties) dla formy (kontrolki użytkownika).
  • Pole Localizable z kategorii Design ustawiamy na True i zapisujemy zmiany. W tym momencie designer wyniesie do pliku o nazwie NAZWA_FORMY.resx wszystkie stałe znakowe, które podlegają lokalizacji. Jeśli zajrzymy teraz do kodu generowanego przez designer to będzie on wyglądał trochę inaczej niż wcześniej. W szczególności nie znajdziemy tam kodu takie jak this.cancelButton.Text = "Anuluj";, a taki resources.ApplyResources(this.cancelButton, "cancelButton"); gdzie resources to obiekt klasy ComponentResourceManager. Co bardzo ważne dla kontrolek, które dodamy do formy później będzie to wyglądało tak samo.
  • Wracamy do okna właściwości (Properties).
  • Zmieniamy pole Language z Default na docelowy język np.: English.
  • Przechodzimy do okna designera i dokonujemy tłumaczenia. Czyli przycisk 'Anuluj' zamieniamy na 'Cancel', a etykietę 'Nazwa' na 'Name' itd.
  • Zapisujemy zmiany. W projekcie pojawi się nowy plik z zasobami o nazwie NAZWA_FORMY.JEZYK.resx.
  • Przywracamy poprzednią wartość pola Language czyli Default. Zawartość wszystkich zlokalizowanych kontrolek powinna wrócić do stanu wyjściowego.
  • Proces powtarzamy dla innych języków.
Z narzędziem tym pracuje się naprawdę przyjemnie. Trzeba tylko pamiętać, że w designerze na pierwszy rzut oka nie widać wszystkich rzeczy do przetłumaczenia na przykład pozycji menu. Praktyka pokazało również, że narzędzie to współpracuje z kontrolkami zewnętrznych dostawców. Mam tylko dwa zastrzeżenia. W jednym przypadku zmodyfikowany przez designer kod nie chciał się potem kompilować i musiałem go poprawić. Po drugie, o czym pisałem już w poście dotyczącym ASP.NET, jeśli nasza aplikacja składa się z wielu okien to otrzymamy wiele plików z zasobami. Moim zdaniem wprowadza to niestety bałagan do projektu i zmusza nas do wielokrotnego tłumaczenie tych samych tekstów.

2 comments:

Anonymous said...

"Na szczęście istnieje dużo leprze rozwiązanie problemu" -> lepsze :)

Michał Komorowski said...

Dziękuję za zwrócenie uwagi. Jak widać sprawdzanie pisowni w programie Word nie jest doskonałe.

Post a Comment