Unicode w Visual C++

Wstęp

Artykuł wprowadza do tematu używania Unicode w programach pisanych w C++ dla systemu Windows, z użyciem środowiska Visual C++. Stanowi zwięzły przegląd funkcji i innych elementów, których należy używać, żeby program działał z Unicode. Od czytelnika wymaga jedynie znajomości podstaw programowania w języku C++.

Wprowadzenie

Ponieważ komputer operuje na liczbach, znaki tekstowe trzeba w jakiś sposób kodować, czyli umówić się, że na przykład litera "A" ma numer 65. Powszechnie obowiązującym na pecetach standardem kodowania znaków jest ASCII [1]. Każdemu znakowi odpowiada w nim jeden bajt, co jak nie trudno policzyć pozwala na zakodowanie 256 różnych znaków. Pierwsze 128 z nich jest ustandaryzowane i obejmuje znaki specjalne (te o kodach poniżej 32, w tym m.in. znak końca wiersza), małe i duże litery alfabetu łacińskiego, cyfry i pozostałe znaki alfanumeryczne, które znajdują się na klawiaturze.

Standard obejmuje alfabet łaciński, bo zarówno komputer, jak i Internet powstały w USA. My jesteśmy w o tyle dobrej sytuacji, że z ich pomocą możemy się również porozumiewać po polsku, chociaż przydałaby się możliwość zapisywania także naszych polskich liter, jak "ą", "ę", "ć". Swoje własne znaczki mają też inne europejskie nacje, np. Czesi, Francuzi. W gorszej sytuacji są m.in. Rosjanie, którzy używają zupełnie innego alfabetu. Te wszystkie możliwe znaki narodowe, jak nietrudno się domyślić, nie zmieściły się w 256-elementowej tablicy znaków ASCII.

Dlatego powstały tzw. strony kodowe (ang. codepage). Definiują one zestaw pozostałych 128 znaków powyżej numeru 127. Na przykład Windows ustawiony na język polski używa strony kodowej Windows-1250. Inna strona kodowa z polskimi znakami, używana często w Linuksie i na stronach WWW, to ISO-8859-2.

Strony kodowe są fajne, o ile można gdzieś określić, której z nich używamy. Dzięki temu możemy bez większych problemów czytać strony WWW i pisać e-maile - w nagłówkach tych dokumentów zapisana jest informacja o użytym sposobie kodowania. Gorzej jest z plikami tekstowymi czy rozmowami przez IRC - tam nie ma gdzie zapisać nagłówka ze stroną kodową, dlatego znaki narodowe zapisane przez jeden program mogą zostać błędnie odczytane przez inny. Jeszcze większy problem powstaje, gdybyśmy chcieli użyć kilku różnych języków w jednym dokumencie (np. tekst po polsku z cytatami po rosyjsku). Tego w ogóle nie da się zrobić stosując jedną wybraną stronę kodową.

Z powodu tych problemów stworzony został standard Unicode [2]. Można chyba powiedzieć, że u jego postaw leży obserwacja, że w dobie półterabajtowych dysków i megabitowych łączy do Internetu, kiedy swobodnie pobieramy i kopiujemy muzykę i filmy, dane tekstowe zajmują tak mało miejsca, że znak nie musi używać już tylko jednego bajtu. Twórcy Unicode zdefiniowali więc jedną wielką tablicę znaków, która obejmuje chyba wszystkie symbole znane ludzkości, w tym litery łacińskie, cyrylicę, znaczki polskie, francuskie, a także chińskie i inne.

Żeby jednak nie było za prosto, tekst w Unicode można zapisywać na różne sposoby. Standard jest tak naprawdę bardzo skomplikowany i nie będę tu opisywał go dokładnie, bo chcę skupić się tylko na jego praktycznym wykorzystaniu. Z tego też powodu proszę wybaczyć mi pominięte informacje (jak brak wzmianki o Multibyte Charecter Set) czy uproszczenia (jak używanie skrótów ASCII i ANSI zamiennie). Poprzestaniemy na poznaniu dwóch standardów zapisu - UTF-16 i UTF-8. Są też inne, np. UTF-7, UTF-32.

W UTF-16 każdy znak zajmuje dwa bajty (16 bitów). Istnieją jego dwie odmiany, różniące się kolejnością bajtów - Big Endian i Little Endian, nazywane też UTF-16 BE i UTF-16 LE. Windows używa wersji Little Endian. Oznacza to, że najpierw znajduje się młodszy, a potem starszy bajt. Na pecetach w architekturze x86 liczby też są zapisywane w tym porządku.

UTF-16 LE to standard, którego używamy w programach Windows do przechowywania łańcuchów w pamięci. MSDN nazywa go po prostu Unicode. Jest prosty w przetwarzaniu, bo każdy znak to dwa bajty (są od tego jakieś wyjątki, ale nie będziemy się nimi zajmować). Możemy też zapisywać pliki tekstowe w tym standardzie, ale to będzie znaczne marnowanie miejsca, bo dla polskiego tekstu prawie co drugi bajt będzie równy zero.

Drugi standard to UTF-8. Znak może w nim zajmować od 1 do 6 bajtów. Pierwsze 128 znaków jest takie samo jak w ASCII, więc plik tekstowy ASCII używający tylko liter alfabetu łacińskiego jest jednocześnie poprawnym plikiem UTF-8. Do zapisania znaków narodowych potrzeba więcej bajtów. Na przykład litera "a" to pojedynczy bajt o kodzie 0x61, a litera "ą" to dwa bajty o kodach kolejno 0xC4, 0x85. Ten standard jest dobry do zapisywania plików tekstowych.

Plik tekstowy Unicode może mieć nagłówek, który mówi o użytym standardzie kodowania. Jest to tzw. BOM - Byte-Order Mark. Może, ale nie musi - dodawanie tego nagłówka i jego prawidłowe odczytywanie zależy od konkretnego edytora tekstu. Systemowy Notatnik go używa. Spotkałem się też z opinią, że do plików zapisywanych jako UTF-16 należy go stosować, a do plików zapisywanych jako UTF-8 nie. Nagłówek BOM to tak naprawdę zapisany w danym kodowaniu, na samym początku pliku, znak Unicode o numerze 0xFEFF (noszący nazwę Zero Width No-Break Space). W poszczególnych standardach jego kolejne bajty wyglądają tak:

Pierwsze kroki w Unicode

Aby określić kodowanie znaków obowiązujące w Twoim projekcie Visual C++, otwórz opcje projektu i ustaw odpowiednio wartość Configuration Properties > General > Character Set. "Not Set" oznacza kodowanie jednobajtowe (ANSI), "Use Unicode Character Set" oznacza Unicode. Jak to działa? Wybranie tej drugiej opcji powoduje po prostu zdefiniowanie makr preprocesora UNICODE i _UNICODE. Co one powodują, o tym będzie mowa niżej.

Zapisywanie łańcuchów, jak i pojedynczych znaków jako stałe w kodzie C++ wymaga dodatkowego zachodu - dopisywania przed nimi literki "L". Ponieważ char pozostaje typem o długości jednego bajtu, do dyspozycji mamy też nowy typ znakowy: wchar_t. To również jest słowo kluczowe C++ i oznacza ono znak 2-bajtowy, a w praktyce jest liczbą 16-bitową bez znaku, jak unsigned short (w odróżnieniu od char, który w Visual C++ domyślnie jest liczbą ze znakiem).

// Tak było w ANSI
char ZnakA = 'A';
const char *LancuchA = "Ala ma kota.";

// Tak jest w Unicode
wchar_t ZnakW = L'A';
const wchar_t *LancuchW = L"Ala ma nowego kota.";

// Tak zapisujemy znaki specjalne
char ZnakSpecialnyA = '\xA7';
wchar_t ZnakSpecjalnyW = L'\u8BCD';

Informacji o używaniu Unicode w kodzie C++ szukaj przede wszystkim w MSDN Library [3], np. w tematach takich jak: "Generic-Text Mappings in Tchar.h", "String Manipulation (CRT)", "Language Strings", "Unicode and Character Sets", "Code Page Identifiers". Świetnym artykułem na ten temat jest też [4].

Unicode a biblioteka standardowa C

Funkcje biblioteki standardowej C do operowania na łańcuchach mają wersje dla znaków Unicode, odpowiadające tym używającym starego dobrego typu char. Ich nazwy różnią się w większości zamianą przedrostka str- na wcs-. Na przykład: strcat -> wcscat, strlen -> wcslen, strcmp -> wcscmp, strcpy -> wcscpy, sprintf -> swprintf.

Inne funkcje też mają swoje odpowiedniki Unicode. Na przykład funkcja do otwierania pliku - fopen - ma swoją wersję Unicode o nazwie _wfopen_s. Szczegóły na temat tych funkcji znajdziesz w MSDN Library.

Biblioteka standardowa C dostarcza też funkcji do konwersji znaków i łańcuchów między standardem UTF-16 a innymi. Więcej o konwersjach (wraz z przykładowym kodem) znajdziesz niżej, w rozdziale na temat WinAPI. Te funkcje to:

Nazbierało się nam już kilka skrótów używanych w nazwach funkcji, więc warto je czym prędzej wyjaśnić, żeby nie wyglądały aż tak strasznie. Przyrostek "A" pochodzi od "ANSI" i oznacza kodowanie jednobajtowe (np. polskie Windows-1250). Przyrostek "W" pochodzi od "Wide" (ang. "szeroki") i oznacza kodowanie UTF-16. "wcs" to zapewne skrót od "Wide Character String". "MB" pochodzi od "Multibyte" (ang. "wielobajtowy") i oznacza kodowania inne niż UTF-16, np. Windows-1250 czy UTF-8. Stąd, "mbs" to zapewne "Multibyte String".

Czas teraz na kolejną ważną informację. Otóż możemy pisać kod C++ tak, że będzie się kompilował i działał czy to ze znakami ANSI, czy Unicode, bez najmniejszych zmian w kodzie, zależnie tylko od opcji projektu. Wymaga to jednak trochę zachodu. Po pierwsze, trzeba włączyć nagłówek <tchar.h>. Po drugie, jako typu znakowego używać trzeba TCHAR. Typ ten, zależnie od opcji projektu (czyli w praktyce od braku lub obecności makr UNICODE, _UNICODE) jest aliasem do char lub wchar_t. Wszystkie łańcuchy i stałe znakowe w kodzie trzeba natomiast otaczać makrem _T(), które zależnie od opcji projektu doda lub nie doda przed stałą literkę "L". Oto przykład:

const TCHAR *LancuchT = _T("Ala ma kota.");

Istnieją też wersje funkcji operujących na łańcuchach, które zależnie od opcji projektu używają znaków ANSI lub Unicode. Nazwy tych funkcji zaczynają się od _tcs-, np. _tcslen, _tcscpy, _tcscmp, _tcscat.

Chciałbym jeszcze wspomnieć na temat lokalizmów (ang. locale). Nie jest to wprawdzie bezpośrednio związane z Unicode, ale skoro już zająłeś się porządnie tematyką znaków i łańcuchów, może Cię to zainteresować. Lokalizm (nazywany w .NET kulturą, ang. culture) to ustawienie w programie konkretnego języka, kultury, narodowości czy kraju.

Powoduje ono pewne zmiany w działaniu funkcji operujących na łańcuchach, na przykład może zmienić format zapisywania daty i czasu, symbol waluty czy zasady sortowania alfabetycznego łańcuchów (po polsku jest A-Ą-B-C-Ć- itd.). Aby ustawić lokalizm dla biblioteki standardowej C, użyj funkcji setlocale lub jej wersji Unicode - _wsetlocale. To ustawienie obowiązuje tylko bibliotekę standardową C, nie ma wpływu na bibliotekę standardową C++ (np. strumienie) czy Windows API.

Jednak uważaj podczas zabawy z tymi rzeczami! Najbardziej widoczną zmianą, jaką powoduje ustawienie lokalizmu na polski zamiast domyślnego angielskiego, jest używanie jako symbolu dziesiętnego podczas konwersji łańcuchów z/na liczby zmiennoprzecinkowe przecinka zamiast kropki przez funkcje takie jak sprintf, atof, _wtof. Tak więc jeśli masz tekstowe pliki konfiguracyjne, w których zapisane są liczby w rodzaju "10.5", to po zmianie lokalizmu na polski przestaną się prawidłowo wczytywać! Rozwiązaniem jest stworzenie osobnego obiektu lokalizmu dla języka neutralnego (angielskiego) i podawanie go podczas konwersji do specjalnej wersji funkcji konwertującej, tak jak w przykładzie:

_locale_t LokalizmNeutralny = _create_locale(LC_ALL, "C");

double Liczba = _wtof_l(L"10.5", LokalizmNeutralny);

Unicode a biblioteka standardowa C++

Biblioteka standardowa C++ również wspiera Unicode. Używając typu std::string mogłeś nawet nie wiedzieć, że to jest tak naprawdę konkretyzacja szablonu klasy std::basic_string. Parametrem tego szablonu jest typ znaków, z których składa się łańcuch. Dla typu std::string jest to oczywiście typ char. Analogicznie, istnieje wersja Unicode łańcuchów C++ sparametryzowana typem wchar_t i nosi ona nazwę std::wstring.

Są też wersje strumieni używające Unicode. Kryją się pod nazwami std::wcout, std::wcin. Tu jednak czai się pewna pułapka. Otóż niespodziewanie strumienie plikowe std::wifstream, std::wofstream, std::wfstream nie zapisują ani nie odczytują plików Unicode (UTF-16)! Zamiast tego, po cichu konwertują tekst na kodowanie jednobajtowe ANSI i w takiej postaci składują łańcuchy na dysku. Aby naprawić ten problem i naprawdę używać plików UTF-16, podczas otwierania pliku wybieraj tryb binarny.

Na wypadek, gdyby zaciekawił Cię jednak temat lokalizmów, napomnę jak on wygląda w przypadku biblioteki C++. Otóż ma on postać nagłówka <locale>. Lokalizmy C++ są jednak tak "zakręcone", że mało które źródło o nich wspomina. Temat ten pomijają nawet niektóre książki na temat biblioteki standardowej C++. Co więcej, analizując dokumentację składników tego nagłówka można dojść do wnisku, że są one napisane ekstremalnie niewydajnie. Nawet funkcja sprawdzająca, czy pojedynczy znak jest białym znakiem, jest metodą wirtualną!

Unicode a Windows API

Jeśli programujesz w Visual C++, to używasz zapewne nie tylko biblioteki standardowej języka, ale też funkcji systemowych. Dobra wiadomość jest taka, że WinAPI wspiera Unicode i to bez żadnego problemu. Na dobry początek definiuje szereg typów:

Wszystkie funkcje systemowe (a przynajmniej wszystkie używające znaków lub łańcuchów) mają dwie wersje, zakończone przyrostkiem -A (do znaków ANSI) i -W (do znaków Unicode). Na przykład istnieją funkcje MessageBoxA i MessageBoxW. Funkcje których używałeś wcześniej - te bez żadnego przyrostka (np. MessageBox) to tak naprawdę makra, które rozwijają się do jednej z tych dwóch wersji zależnie od kodowania ustawionego w opcjach projektu. Przestawienie odpowiedniej opcji na Unicode oznacza więc, że funkcje systemowe automatycznie zaczynają działać z "szerokimi" znakami.

Unicode działa w całym WinAPI. Tak więc jeśli Twój projekt używa Unicode, kody znaków otrzymywane jako wParam w komunikacie WM_CHAR też będą znakami Unicode.

WinAPI dostarcza też swoich funkcji do konwersji między standardami kodowania znaków. Te funkcje to MultiByteToWideChar (zamienia łańcuch w innym kodowaniu na UTF-16) i WideCharToMultiByte (zamienia łańcuch w kodowaniu UTF-16 na inne). Wspierane jest bardzo wiele stron kodowych, z czego te ważniejsze wpisałem poniżej. Tych wartości i stałych używaj jako parametru CodePage w pokazanych dalej funkcjach.

Taka konwersja między różnymi standardami kodowania musi się odbywać w dwóch etapach. W pierwszym funkcja WinAPI wylicza długość bufora docelowego, a w drugim dopiero konwertuje łańcuch. Żeby to jakoś zamknąć w łatwy do użycia kod, napisałem funkcje konwertujące, które używają łańcuchów C++. Ponadto konwersja może się nie udać - wtedy moja funkcja zwraca false.

bool ConvertUnicodeToChars(std::string *Out, const std::wstring &S,
  unsigned CodePage)
{
  if (S.empty())
  {
    Out->clear(); return true;
  }

  int Size = WideCharToMultiByte(
    CodePage, 0, S.data(), (int)S.length(), NULL, 0, NULL, NULL);
  if (Size == 0)
  {
    Out->clear(); return false;
  }

  char *Buf = new char[Size];
  int R = WideCharToMultiByte(
    CodePage, 0, S.data(), (int)S.length(), Buf, (int)Size, NULL, NULL);
  if (R == 0)
  {
    Out->clear(); return false;
  }

  Out->assign(Buf, Size);
  delete [] Buf;
  return true;
}

bool ConvertUnicodeToChars(string *Out, const wchar_t *S, unsigned NumChars,
  unsigned CodePage)
{
  if (NumChars == 0)
  {
    Out->clear(); return true;
  }

  int Size = WideCharToMultiByte(
    CodePage, 0, S, (int)NumChars, NULL, 0, NULL, NULL);
  if (Size == 0)
  {
    Out->clear(); return false;
  }

  char *Buf = new char[Size];
  int R = WideCharToMultiByte(
    CodePage, 0, S, (int)NumChars, Buf, Size, NULL, NULL);
  if (R == 0)
  {
    Out->clear(); return false;
  }

  Out->assign(Buf, Size);
  delete [] Buf;
  return true;
}

bool ConvertCharsToUnicode(std::wstring *Out, const std::string &S,
  unsigned CodePage)
{
  if (S.empty())
  {
    Out->clear(); return true;
  }

  int Size = MultiByteToWideChar(
    CodePage, 0, S.data(), (int)S.length(), NULL, 0);
  if (Size == 0)
  {
    Out->clear(); return false;
  }

  wchar_t *Buf = new wchar_t[Size];
  int R = MultiByteToWideChar(
    CodePage, 0, S.data(), (int)S.length(), Buf, Size);
  if (R == 0)
  {
    Out->clear(); return false;
  }

  Out->assign(Buf, Size);
  delete [] Buf;
  return true;
}

bool ConvertCharsToUnicode(std::wstring *Out, const char *S, unsigned NumChars,
  unsigned CodePage)
{
  if (NumChars == 0)
  {
    Out->clear(); return true;
  }

  int Size = MultiByteToWideChar(
    CodePage, 0, S, (int)NumChars, NULL, 0);
  if (Size == 0)
  {
    Out->clear(); return false;
  }

  wchar_t *Buf = new wchar_t[Size];
  int R = MultiByteToWideChar(
    CodePage, 0, S, (int)NumChars, Buf, Size);
  if (R == 0)
  {
    Out->clear(); return false;
  }

  Out->assign(Buf, Size);
  delete [] Buf;
  return true;
}

Skoro już jesteśmy przy innych kodowaniach, wczytując plik tekstowy możnaby spróbować rozpoznać kodowanie, w jakim jest zapisany. WinAPI dostarcza bardzo fajnej funkcji do tego celu: IsTextUnicode, ale trzeba umieć jej używać. Dla przekazanego bufora ze znakami wykonuje szereg testów i zwraca ich wyniki. Odpowiednie zinterpretowanie tych wyników pozwoli ustalić, czy test jest zakodowany jako UTF-16 LE albo UTF-16 BE. Po szczegóły odsyłam do dokumentacji tej funkcji w MSDN Library. Można do tego dodać własne testy (przede wszystkim sprawdzanie nagłówka BOM) i napisać prostą w użyciu funkcję rozpoznającą kodowanie, taką jak ta:

enum ENC {
  ENC_NONE = 0,
  ENC_UTF32_LE,
  ENC_UTF32_BE,
  ENC_UTF16_LE,
  ENC_UTF16_BE,
  ENC_UTF8,
  ENC_ANSI,
};

// Funkcja wewnętrzna do testowania czy dane zaczynają się od danego nagłówka
bool HasBOM(const char *Data, size_t NumBytes,
  const char *BOM, size_t BOMNumBytes)
{
  if (NumBytes < BOMNumBytes)
    return false;

  for (size_t i = 0; i < BOMNumBytes; i++)
    if (Data[i] != BOM[i])
      return false;

  return true;
}

ENC DetectEncoding(
  const char *Data, // Dane do analizy
  size_t NumBytes, // Rozmiar bufora Data w bajtach
  ENC SuggestedEncoding, // Sugerowane kodowanie - ANSI, UTF-8 lub NONE
  size_t *OutBomSize) // Liczba bajtów nagłówka BOM
{
  ENC Enc = (ENC)0;
  *OutBomSize = 0;

  // Nagłówek UTF-32 LE
  if (HasBOM(Data, NumBytes, "\xFF\xFE\x00\x00", 4))
    Enc = ENC_UTF32_LE, *OutBomSize = 4;
  // Nagłówek UTF-32 BE
  else if (HasBOM(Data, NumBytes, "\x00\x00\xFE\xFF", 4))
    Enc = ENC_UTF32_BE, *OutBomSize = 4;
  // Nagłówek UTF-8
  else if (HasBOM(Data, NumBytes, "\xEF\xBB\xBF", 3))
    Enc = ENC_UTF8, *OutBomSize = 3;
  // Trzeba odpalić analizę
  else
  {
    int AnalysisFlags =
      IS_TEXT_UNICODE_SIGNATURE | IS_TEXT_UNICODE_REVERSE_SIGNATURE |
      IS_TEXT_UNICODE_ASCII16 | IS_TEXT_UNICODE_REVERSE_ASCII16 |
      IS_TEXT_UNICODE_ILLEGAL_CHARS | IS_TEXT_UNICODE_ODD_LENGTH |
      IS_TEXT_UNICODE_NULL_BYTES |
      IS_TEXT_UNICODE_STATISTICS | IS_TEXT_UNICODE_REVERSE_STATISTICS |
      IS_TEXT_UNICODE_CONTROLS | IS_TEXT_UNICODE_REVERSE_CONTROLS;
    IsTextUnicode(Data, (int)NumBytes, &AnalysisFlags);

    // Nagłówek UTF-16 LE
    if ((AnalysisFlags & IS_TEXT_UNICODE_SIGNATURE) != 0)
      Enc = ENC_UTF16_LE, *OutBomSize = 2;
    // Nagłówek UTF-16 BE
    else if ((AnalysisFlags & IS_TEXT_UNICODE_REVERSE_SIGNATURE) != 0)
      Enc = ENC_UTF16_BE, *OutBomSize = 2;
    // To jest na pewno UTF-16 LE
    else if ((AnalysisFlags & IS_TEXT_UNICODE_ASCII16) != 0)
      Enc = ENC_UTF16_LE;
    // To jest na pewno UTF-16 BE
    else if ((AnalysisFlags & IS_TEXT_UNICODE_REVERSE_ASCII16) != 0)
      Enc = ENC_UTF16_BE;
    // Nadal nie wiadomo...
    else
    {
      // Liczymy argumenty za UTF-16 LE i BE
      int UTF16LE_Args = 0, UTF16BE_Args = 0;

      if ((AnalysisFlags & IS_TEXT_UNICODE_NULL_BYTES) != 0)
        UTF16LE_Args++, UTF16BE_Args++;

      if ((AnalysisFlags & IS_TEXT_UNICODE_STATISTICS) != 0)
        UTF16LE_Args++;
      if ((AnalysisFlags & IS_TEXT_UNICODE_REVERSE_STATISTICS) != 0)
        UTF16BE_Args++;

      if ((AnalysisFlags & IS_TEXT_UNICODE_CONTROLS) != 0)
        UTF16LE_Args++;
      if ((AnalysisFlags & IS_TEXT_UNICODE_REVERSE_CONTROLS) != 0)
        UTF16BE_Args++;

      // To jednak na pewno nie jest UTF-16
      if ((AnalysisFlags & IS_TEXT_UNICODE_ILLEGAL_CHARS) != 0 ||
        (AnalysisFlags & IS_TEXT_UNICODE_ODD_LENGTH) != 0)
        UTF16LE_Args = 0, UTF16BE_Args = 0;

      if (UTF16LE_Args > 0 && UTF16LE_Args >= UTF16BE_Args)
        Enc = ENC_UTF16_LE;
      else if (UTF16BE_Args > 0)
        Enc = ENC_UTF16_BE;
      // To nie jest UTF-16 - jest UTF-8 lub ANSI
      else
      {
        if (SuggestedEncoding == ENC_UTF8)
          Enc = ENC_UTF8;
        else if (SuggestedEncoding == ENC_ANSI)
          Enc = ENC_ANSI;
        // Preferowane kodowanie nie podane - wybierz domyślne
        else
          // Jako domyślne zwracam UTF-8, ale równie dobrze można ANSI
          Enc = ENC_UTF8;
      }
    }
  }

  return Enc;
}

Punkt wejścia, czyli funkcja główna programu (main albo WinMain) przyjmuje łańcuch z parametrami wiersza poleceń, a więc przydałoby się, żeby też miała swoją wersję Unicode. Jak się okazuje, ma! Najprostszym rozwiązaniem na otrzymanie parametrów wiersza poleceń jako łańcuch Unicode (w takiej postaci jak do WinMain, czyli jako jeden łańcuch) jest użycie funkcji systemowej GetCommandLine (czy też jawnie jej wersji GetCommandLineW). Możesz jednak zdefiniować funkcję wejścia przyjmującą znaki Unicode:

// Kolosowa ANSI:
int main(int argc, char *argv[], char *envp[])
// Konsolowa Unicode
int wmain(int argc, wchar_t *argv[], wchar_t *envp[])
// Konsolowa zależna od opcji projektu
int _tmain(int argc, TCHAR *argv[], TCHAR *envp[])

// Okienkowa ANSI:
int WINAPI WinMain(HINSTANCE Instance, HINSTANCE, LPSTR CmdLine, int ShowCmd)
// Okienkowa Unicode:
int WINAPI wWinMain(HINSTANCE Instance, HINSTANCE, LPWSTR CmdLine, int ShowCmd)
// Okienkowa zależna od opcji projektu:
int WINAPI _tWinMain(HINSTANCE Instance, HINSTANCE, LPTSTR CmdLine, int ShowCmd)

Nawet jeśli z jakiś powodów piszesz własną funkcję wejścia niższego poziomu (np. jesteś demoscenowcem piszacym intra 4k), masz do dyspozycji jej wersję Unicode - wWinMainCRTStartup.

Wspomnę jeszcze, że jeśli chcsz zająć się tematem lokalizmów, to w kwestii Windows API zainteresuj się funkcją SetThreadLocale.

Unicode a inne biblioteki

W DirectX (zarówno 9 jak i 10) Unicode działa tak samo jak w WinAPI. Funkcje używające znaków mają dwie wersje, zakończone na -A i -W, a wersja bez przyrostka automatycznie wybiera odpowiednią zależnie od opcji projektu. Dotyczy to na przykład funkcji DXGetErrorDescription, ID3DXFont::DrawText.

Wiele innych bibliotek niestety nie wspiera Unicode. Do kontaktu z nimi trzeba wtedy konwertować łańcuchy na kodowanie ANSI. Jeszcze inne (np. wxWidgets) pozwalają na skompilowanie ze źródła w wersji zarówno ANSI, jak i Unicode.

Podsumowanie

Unicode można używać w kodzie pisanym w Visual C++ bez większych problemów, jednak wymaga to używania odpowiednich typów i funkcji. Z drugiej strony, nie przynosi w praktyce żadnych korzyści, o ile nie potrzebujesz pisać programu, który musi używać plików Unicode albo który będzie wydany w różnych wersjach językowych.

Jeśli potrzebujesz konwersji między różnymi kodowaniami znaków, bardziej zaawansowanych niż to oferują funkcje WinAPI, zainteresuj się biblioteką libiconv [5].

Bibliografia

  1. Ascii Table, http://www.asciitable.com/.
  2. Unicode Home Page, http://unicode.org/.
  3. MSDN Library, http://msdn.microsoft.com/en-us/library/default.aspx.
  4. Tex Texin, Cheat Sheet: Unicode-enabling Microsoft C/C++ Source Code, http://www.i18nguy.com/unicode/c-unicode.html.
  5. libiconv, http://www.gnu.org/software/libiconv/.
Adam Sawicki
asawicki.info
21.05.2008