Dev Diary: Wybór państwa z listy czyli historia pewnego ComboBoxa

Gdy startował Tomiga Blog jeden z moich internetowych znajomych, a także fan Biznesu Filmowego, Thaven podsunął pomysł, żeby umieszczać na nim tzw. Dzienniki Deweloperskie czyli Developers Diary. Pomysł wydawał się ciekawy jednak nie do końca wiedziałem jakiego rodzaju tematy mógłbym w takich wpisach poruszać, aby z jednej strony nie były zbyt banalne, a z drugiej nie zdradzały moich największych tajemnic ;).

Komunikując się z graczami czy to przez forum czy też maile, często pojawiają się pytania dotyczące technicznych aspektów tworzenia mojej gry. Z drugiej strony wielu graczy przyznaje, że nie zna się kompletnie na programowaniu i nie zdaje sobie sprawy jak trudne, a w szczególności czasochłonne jest tworzenie nawet takiej (na pierwszy rzut oka) nieskomplikowanej produkcji jaką jest BF. Tekst ten może zobrazować Wam, że nie jest to takie proste zadanie i nawet zmiana jednego, z pozoru nie wiele znaczącego elementu, wymaga poświęcenie sporej ilości czasu i posiadania całkiem rozległej wiedzy.

Choć wpis ten kierowany jest głównie do osób, które nie są programistami, gdyż jego celem jest przedstawić proces związany z rozwiązaniem konkretnego problemu podczas pisania gry, to mimo wszystko zawiera on konkretne wskazówki czysto techniczne (fragmenty kodu, czy pojęcia związane stricto z programowaniem) więc także osoby zajmujące się tym na co dzień, mogą znaleźć w nim coś interesującego dla siebie.


Problem, a właściwie potrzeba

Co bardziej spostrzegawczy gracze zauważyli pewnie, że w wersji 0.7.2 BF2 w oknie edycji profilu gracza, pojawiła się lista wybieralna (combobox), z której można wybrać kraj pochodzenia. We wcześniejszych wersja trzeba było wpisać tę informację ręcznie. Przedstawię Wam zawiłą historia powstania tego comboboxa czyli dlaczego i w jaki sposób zostało to zaimplementowane oraz ile czasu zajęło pełne rozwiązanie problemu.

Skąd właściwie pojawił się pomysł zmiany interfejsu. Dotychczasowy sposób wprowadzania tej informacji był nie efektywny. Pozostawienie miejsca na wpisanie przez użytkownika dowolnego ciągu znaków generuje wiele problemów:

  • użytkownik może nic nie wpisać
  • użytkownik może wpisać coś bezsensownego (np. aa, qwert)
  • w rożny sposób można wpisać nazwę tego samego kraju np. USA, U.S.A, United States, United States of America

Co chcę osiągnąć
Wyżej wymienione kwestie stają się problemem w momencie, gdy chcemy w jakiś sposób przetwarzać, grupować dane lub tworzyć na ich podstawie zestawienia bądź statystyki. W związku z tym naturalnym rozwiązaniem było ograniczenie możliwości wybrania kraju przez użytkownika do listy z góry predefiniowanych państw. Do tego typu zadań, z punktu widzenia projektowania interfejsu użytkownika, najlepiej nadaje się lista rozwijalna tzw. combobox.

Ile jest państw na świecie?

Skoro wybór kontrolki mam już za sobą, czas zastanowić się czym ją wypełnić, czyli skąd wziąć listę krajów. Oczywiście rozwiązanie polegające nad siedzeniem nad atlasem geograficznym i wypisywanie wszystkich możliwych państw nie było brane pod uwagę. Pierwszym pomysłem, który przyszedł mi do głowy, to zajrzeć na Wikipedię, gdzie prawdopodobnie taka lista została już zrobiona. Jak szybko się ten pomysł pojawił, tak szybko został wyparty z podświadomości. Z jednej strony ilość czasu poświęcona na tego rodzaju akcję jest zbyt duży w stosunku do ważności całego zadania, z drugiej coś takiego jak wybieranie kraju jest bardzo częstym przypadkiem użycia (use casem), w związku z tym muszą być jakieś standardowe (gotowe) rozwiązania tego problemu.

Fakt, że Biznes Filmowy powstaje na powszechnie używaną platformę (Windows) i korzysta z frameworku .NET podsunął myśl, że musi istnieć rozwiązanie systemowe ułatwiające obsługę wielu krajów, a w szczególności lista państw. I rzeczywiści, szybkie sprawdzenie dokumentacji potwierdziło moje przypuszczenia. Jest specjalna przestrzeń nazw Globalization, która ułatwia manipulowanie międzynarodowymi ustawieniami.

W bibliotece tej znajduje się m.in klasa CultureInfo. Znajdziemy w niej wiele bardziej (i mniej) przydatnych informacji. Oprócz samej listy państw otrzymałem dostęp do dodatkowych danych, takich jak nazwa wyświetlana w zależności od języka systemowego użytkownika – DisplayName, nazwę anglojęzyczną – EnglishName, oraz nazwę lokalną używaną w danym państwie LocalName ( np. dla państw arabskich zapisana przy pomocy ichniejszych „krzaczków”.) Tak więc dzięki użyciu tej klasy „za darmo” mam automatyczne tłumaczenie nazw państw na język użytkownika. W praktyce oznacza to, że jeżeli będziecie korzystać z polskiej wersji systemu Windows, nazwa naszego kraju pojawi się jako Polska, jeżeli angielskiej to jako Poland.

Kolejną decyzję, którą należy podjąć w trakcie projektowania, jest w jaki sposób przechowywać te dane w profilu użytkownika. Ponieważ, jak już wiemy nazwy krajów brzmią różnie, w zależności od języka, dlatego też nie możemy trzymać tam tekstu, który bezpośrednio wybierzemy z listy wybieralnej. Potrzebny jest więc klucz, jednoznacznie identyfikujący dany kraj.

W zastosowaniach informatycznych klucze najczęściej mają postać cyfr. Jednak w tym przypadku nie wydaje się to być najlepszym rozwiązaniem, gdyż później i tak ten klucz cyfrowy musimy jakoś rozkodować. Np. jeżeli założymy, że Polska to będzie 148 to następnie będziemy musieli stworzyć jakąś metodę, która zwrócić nam nazwę kraju dla tej liczby. Inną wadą tego rodzaju klucza jest fakt, że jest on zupełnie abstrakcyjny (w żaden sposób liczba 148 nie kojarzy się z Polską).

To skłoniło mnie do zastosowania innego rozwiązania. Pewnie zauważyliście, że np. na imprezach sportowych nazwy krajów są u standaryzowane. Jak się okazało standaryzacja ta jest opisana w standardach ISO. Powstały trzy takie standardy opisane w normie ISO 3166-1
Są to: dwu i trzy znakowy kod alfanumeryczny oraz trzy znakowy numeryczny. Dwa znaki powinny wystarczyć do identyfikacji kraju w związku z tym zamiast klucza 148 użyjmy ‚pl’, który już na pierwszy rzut oka sugeruje nam z jakiego kraju pochodzi gracz. Kolejny plus standaryzacji to ten, że listę tych skrótów mamy już zawartą w klasie CultureInfo, więc ponownie zaoszczędzam sobie sporo pracy.

Czas na pierwszy kawałek kodu. Zobaczmy więc w jaki sposób możemy wyciągnąć listę wszystkich krajów z systemu Windows.

        public static Dictionary<string, string> GetCountryList()
        {
            Dictionary<string, string> cultureList = new Dictionary<string, string>();
            CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures);

            foreach (CultureInfo culture in cultures)
            {
                if (culture.LCID != 127)
                {
                    RegionInfo region = new RegionInfo(culture.LCID);

                    if (!(cultureList.ContainsKey((string)region.TwoLetterISORegionName)))
                    {
                        cultureList.Add(region.TwoLetterISORegionName, region.DisplayName);
                    }
                }
            }
            return cultureList;
        }

Powyższa metoda zwraca nam słownik (klasa Dictionary) zawierającą państwa wraz odpowiadającymi im kluczami w formacie ISO2. Czyli: <klucz, wartość> np: <pl, Polska>

Mamy wiec już kolekcję państw czas wypełnić nimi naszą kontrolkę. Zanim jednak to nastąpi nasza lista powinna być posortowana. Tzn chcemy aby państwa były wyświetlane w kolejności alfabetycznej dla danego języka. .NET Framework posiada bardzo pomocną klasę SortDictionary, która automatycznie sortuje nam kolekcję. Niestety nie możemy jej użyć w naszym przypadku, ponieważ sortowanie (jak to w słownikach) odbywa się po kluczu, a my chcemy posortować po nazwie kraju (czyli wartości).

Przykładowo można to zrobić tak:

        myList.Sort(
				delegate(KeyValuePair<string, string> firstPair,
					KeyValuePair<string, string> nextPair)
				{
					return firstPair.Value.CompareTo(nextPair.Value);
				}
		);

Programowanie to jednak bardzo ciekawa zabawa intelektualna ponieważ często ten sam problem można rozwiązać na kilka różnych sposób. Często niektóre rozwiązania są bardziej optymalnie w jednym aspekcie a mniej w drugim np:. prędkości działania vs długości kodu. W naszym przypadku prędkość nie jest krytyczna więc możemy pokusić się o optymalizację długości kodu. W .NET Frameworku 3.5 pojawił się bowiem nowy funkcjonalność tzw. operator lambda. Pozwala on na uproszczenie powyższej metody do takiej to postaci.

     dict = dict.OrderBy(x => x.Value).ToDictionary(x => x.Key, x => x.Value);

Ten przykład pokazuje dlaczego od czasu do czasu Biznes Filmowy wymaga instalowania nowych bibliotek. Choć może to być nieco uciążliwe dla końcowego użytkownika znacznie ułatwia to programowanie. Programiści to leniwi ludzie i co chwile wprowadzane są ulepszenia ułatwiające im życie (a konkretnie pisanie kodu). Dlaczego warto od czasu do czasu sięgnąć po najnowsze narzędzia i wersje bibliotek (oczywiście nic nie jest za darmo nowe możliwości wymagają nowych umiejętności no ale w sumie po to bawię się w pisanie darmowych programów aby się czegoś nowego nauczyć).

Teraz skoro mamy już nasz słownik państw posortowany, możemy wypełnić nim naszą kontrolkę.

Wygląda to tak:

        public static void FillComboWithCountries(ref ComboBox cb)
        {
            Dictionary<string, string> countries = new Dictionary<string, string>();

            countries = BFTools.GetCountryList();

            for (int i = 0; i < (int)countries.Count; i++)
            {
                cb.Items.Add(new ComboBoxItem2(
                    countries.ElementAt(i).Key, countries.ElementAt(i).Value
                    ));

            }
            cb.SelectedIndex = 0;
        }

*W powyższym przykładzie zastosowano klasę pomocniczą ComboBoxItem2, która przechowuje po prostu dwa stringi odpowiadające za klucz i wartość.

„Użytkownik nigdy się nie myli” czyli „Idiotoodporność”

To, że stworzyliśmy świetny kawałek kodu wcale nie oznacza, że doceni to końcowy użytkownik. Wręcz przeciwnie, bardzo często chce on udowodnić, że program jest głupi (a jego autor tym bardziej ;). Ponieważ z definicji „użytkownik zawsze ma rację”, nasz program należy  „uidiotoodpornić” – czyli zabezpieczyć przed możliwie wszystkimi absurdami, które spróbuje „wyklikać” nasz potencjalny gracz.

Przekora użytkowników można przedstawić na prostym przykładzie. Jeżeli domyślnie wybierzemy na liście pierwszą lepszą wartość (w naszym przypadku – polska wersja systemu Windows – po posortowaniu będzie to pierwszy kraj na literę A czyli Afganistan) to:

  1.   30 procent zostawi tą wartość, bo nie będzie się im chciało wypełniać pól, które nie są obowiązkowe do wypełnienia
  2.   30 wybierze jakąś losową wartość (…bo tak 😉

Dopiero reszta zada sobie odrobinę wysiłku wybranie rzeczywistego kraju swojego pochodzenia.

Oczywiście nie chcemy doprowadzić do sytuacji 1) i 2). Co w związku z tym? Warto domyślnie ustawić użytkownikowi kraj z którego pochodzi. Spytacie pewnie jak to zrobić, przecież tego nigdy nie będziemy pewni? Owszem nigdy nie będziemy mieli 100% pewności jednak możemy spróbować ustalić to z dużą dozą prawdopodobieństwa. Wystarczy sprawdzić z jakich ustawień systemowych (konkretnie języka) korzysta jego wersja sytemu Windows. Jeżeli mamy ustawiony język polski to z dużym prawdopodobieństwem naszym krajem pochodzenia jest polska. Jak pisałem już wcześniej nie jest to metoda w 100% skuteczna ale zdecydowanie lepsza niż zostawienie Afganistanu ;).

A oto fragment kodu który to zrobi:

        public static string SelectUserCountry(ref ComboBox cb, string isoCode2)
        {
            ComboBoxItem2 item = null;
            string currentRegion = RegionInfo.CurrentRegion.DisplayName;
            int currentRegionIndex = 0;

            for (int i = 0; i < cb.Items.Count; i++)
            {
                item = (ComboBoxItem2)cb.Items[i];
                if (item != null)
                {
                    // store current region index just for case
                    if (currentRegion == item.Text) currentRegionIndex = i;

                    if (isoCode2 == item.Id)
                    {
                        cb.SelectedItem = item;
                        return isoCode2;
                    }
                }
            }

            // if everthings go OK we should't be here in other case no country matched set default depends on system setting
            cb.SelectedIndex = currentRegionIndex;
            return ((ComboBoxItem2)cb.SelectedItem).Id;

        }

Konkretnie ten fragment kodu ma za zadanie ustawić kontrolce państwo wg. podanego w jej parametrze kodu iso2. W przypadku jednak, gdy taki kod nie istnieje (lub jest pusty) domyślnie zostanie wybrany kraj ustawiony w systemie użytkownika.

Flagi
Skoro mamy już państwa to czasami dobrze by było prezentować je nie w formie nazwy ale w jakiejś formie graficznej np. jako flagi. Pytanie skąd wziąć flagi. Nie, nie będziemy ich rysować. Tym razem skorzystamy z potęgi internetu oraz wyszukiwarki Google. Po kilkudziesięciu sekundach mamy już rozwiązanie. Jest nim darmowy zestaw plików graficznych udostępniona przez Marka Jamesa aka FamFamFam. I z niej to skorzystamy.

Dodatkowa ciekawostka jest fakt, że po rozpakowaniu archiwum okazuje się, że nazwy plików są w postaci pl.gif dla polskiej flagi… a co to oznacza…, że nasz u standaryzowany klucz w postaci kodów ISO2 i tym razem przyniósł nieoczekiwane korzyści. Nie musimy tworzyć dodatkowego mapowania lub zmieniać nazw wszystkich plików graficznych.

I to by było na tyle. Całość rozwiązania problemu „kraju pochodzenia użytkownika” zajęło kilka godzin, które z racji pracy nad BFem „po godzinach”, rozciągnęły się na kilka dni. Obejmowało to zdefiniowania potrzeby, zapoznanie się z dziedziną problemu (reprezentacja krajów), znalezienie sposobu jej rozwiązania przy wykorzystaniu odpowiednich narzędzi i standardowych rozwiązań. Jak to zazwyczaj bywa w trakcie, pojawiło się kilka interesujących problemów jak kodowanie krajów czy  sortowanie słownika po wartościach. W końcu musiałem znaleźć odpowiednie pliki graficzne z flagami.

Podsumowanie
I to wszystko musiało zostać zrobione, aby zmienić sposób wprowadzania kraju pochodzenia użytkownika z pola tekstowego na listę do wyboru…. Mam nadzieje, że ten banalny przykład zobrazował Wam jak skomplikowanym procesem jest programowanie w ogóle, że nie jest to tylko kwestia znajomości technologii (języka programowania) ale ociera się o wiele innych nauk nawet z pogranicza psychologii (zachowania użytkownika) czy geografii ;). Wiecie więc już jak wiele czynników należy brać pod uwag przy programowaniu i jak odpowiednie dobranie środków oraz stosownie standardów może ułatwić i przyśpieszyć ten proces.

Wnioski i lekcje dla początkujących adeptów programowania

  1. Nie wymyślaj koła od nowa, korzystaj z rozwiązań, standardów, które już istnieją, troszkę czasu trzeba poświęcić na ich odnalezienie ale w konsekwencji zaoszczędzisz dużo czasu/nerwów w przyszłości (przykład nazwy plików z flagami)
  2. Warto co jakiś czas przesiadać się na aktualne narzędzia (biblioteki) gdyż oferują one nowe funkcje upraszczające i przyśpieszające rozwiązywanie problemów
  3. Użytkownik nigdy się nie myli… zminimalizuje więc sytuacje kiedy może on dostarczyć nie pożądane przez nas informacje.

Jeszcze mała prośba na zakończenie. Jeżeli spodobał Wam się ten tekst i uważacie, że w przyszłości tego typu wpisy powinny być kontynuowane, to pod tym artykułem możecie wystawić mu ocenę (takie gwiazdki). Jeżeli trochę się ich nazbiera to postaram się od czasu do czasu przedstawiać problemy z jakimi się spotkałem podczas prac nad Biznesem Filmowym lub innymi projektami, z którym mam do czynienia „po godzinach”. Oczywiście wszelkie dodatkowe uwagi i komentarze są mile widziane.

P.S
Powyższe rozwiązania ogólnie ma jeden problem. Dla bardziej zaawansowanych programistów mam więc małą zagadkę. Na czym on polega? 😉
P.P.S
Ponownie uwaga dla programistów jeżeli uważacie, że coś w powyższym podejściu można zrobić lepiej, efektywniej bądź bardziej elegancko to piszcie, zawsze chętnie czegoś nowego się nauczę.

2 myśli w temacie “Dev Diary: Wybór państwa z listy czyli historia pewnego ComboBoxa

  1. Ah, jak miło zostać wymienionym w tekście :). Tak naprawdę cześć biblioteki .net odpowiedzialna za kulturę i języki to bardzo ciekawa sprawa, która ma wpływ na różne ciekawe rzeczy. Może pozostając więc w temacie podzielę się z problemem, na jaki kiedyś natrafiłem w pracy.
    Otóż, cała sprawa rozchodziła się o to, że w różnych krajach różnie zapisywana jest data. Np. 10 maja 2001 roku w Polsce to 10.05.2001, w Ameryce 05/10/2001, a w zjednoczonych królestwach 10/05/2001 (niekoniecznie dokładnie tak, pisze z głowy, ale chodzi o zobrazowanie problemu).
    Kontrolka kalendarza którą używamy w pracy, automatycznie wybiera formatowanie na podstawie kultury wątku na jakim działa. Gdzieś przy starcie programu ten pyta o język, wybór użytkownika determinuje późniejszy wybór kalendarza.
    Problem pojawił się wtedy, kiedy user wybrał angielski, a format daty ustawiał się amerykański.
    Czemu tak się działo? Jest wiele odmian kultury angielskiej – inna jest w GB, inna w USA, w Australii jeszcze inna, a masa jeszcze z ex kolonii brytyjskich. Jakiś programista opracowujący mechanizm uznał, że jak ktoś wybiera dowolny język, to zapewne chodzi mu o domyślną kulturę danego języka. Ponieważ .Net jest od Microsoftu, który jest z Ameryki, więc oczywiście domyślna kultura to amerykańska.
    tutaj mała ciekawostka: ISO2 przyjmuje dwie postacie: pierwsza to np. en – angielski. druga zawiera jeszcze informacje o odmianie danej kultury, np. en-EN, en-US, itd. Jednak typ wyliczeniowy zawierający ISO2 jest tak zaprojektowany, żeby mając niesprecyzowaną odmianę kultury (en), zrobić z niej domyślną (en-US) wystarczy dodać do wartości pola 0x0400.

    1. Rzeczywiście są takie niuanse, akurat mnie wystarczyła lista krajów więc póki co nie zagłębiałem się w szczegóły (aczkolwiek wiedziałem o różnicach kulturowych – to akurat przydaje się przy zasobach i aplikacjach wielojęzykowych)
      Mimo wszystko dzięki za obszerny komentarz jak w przyszłości będę miał jakieś problemy z globalization to już wiem do kogo uderzać 😉

Dodaj komentarz