Napsat správně mezinárodní aplikaci v Qt nemusí být snadné

Jan Pečiva 27. 12. 2010

Dobře napsaná aplikace – to je kus umění. Přesvědčit vás o tom může i okamžik, kdy se vaše aplikace dostane mezinárodnímu publiku. Rázem je nám osmibitový char těsný a uživatelé si stěžují, že soubor používající německou diakritiku nebo norské znaky nejde otevřít a že je potřeba s tím něco dělat.

Unicode má, zjednodušeně řečeno, množství různých kódování. Nejznámější jsou tato:

kódování nativní na platformě datový typ zapouzdřující třídy
UTF-8 Linux char std::string
UTF-16 Windows Windows: wchar_t std::wstring (Windows), QString
UTF-32 Linux: wchar_t std::wstring (Linux)

UTF-8

Nejrozšířenější kódování na Linuxu je UTF-8. Používá standardní typ char, přičemž znaky nad 127 jsou reprezentovány vícebajtovou hodnotou. Jeden znak může tímto způsobem obsadit 1 až 6 bajtů. Jednou z podstatných výhod tohoto kódování je, že Linux nepotřebuje nové API, aby podporoval unicode. Standardní funkci fopen pouze předáte UTF-8 zakódovaný název souboru a daný soubor bude pro vás otevřen. Název souboru přitom může být třeba v japonštině.

Text v UTF-8 můžeme snadno obsluhovat jako standardní nulou ukončený řetězec nebo jej uložit do std::string. Je zde však pár věcí, na které je nutno pamatovat. std::string sice krásně reprezentuje náš UTF-8 text, ale při zavolání funkce length() nedostaneme skutečný počet znaků, ale pouze množství bajtů, které daný text v paměti zabírá. Totéž dělá funkce strlen – vrací počet bajtů, které daný text v paměti zabírá. To reflektuje časté použití funkce strlen ke zjištění, kolik paměti je třeba pro daný text alokovat. Pro zjištění skutečného počtu znaků můžeme použít například funkci mbstowcs().

UTF-16

UTF-16 je nativně používané uvnitř Windows. I když používáte staré 8-bitové ASCII/ANSI kódování s různými znakovými sadami (code pages), kdykoliv tento text předáte Windows, on si jej zkonvertuje na UTF-16 před jakýmkoliv dalším zpracováním. Uvnitř tedy Windows obsluhují a uchovávají text jako UTF-16.

Každý znak v tomto kódování je v optimálním případě uložen na dvou bajtech. Avšak dva bajty by neumožnily uložit všechny znaky unicode. Proto se pro některé znaky používá dvojice dvou bajtových hodnot.

Pokud píšete své aplikace pro KDE nebo prostě jen používáte knihovnu Qt, budou všechny vaše texty předané knihovně Qt uloženy jako UTF-16, neboť vývojáři Qt zvolili UTF-16 jako svůj standardní formát ukládání textových řetězců.

UTF-32

UTF-32 se používá na Linuxu pro std::wstring. Typ wchar_t je zde 32-bitový, na rozdíl od Windows (Microsoft Visual C++), kde wchar_t má 16 bitů a std::wstring tedy bude také pouze UTF-16.

Bohužel, ani u UTF-32 se nemůžeme spolehnout, že každých 32-bitů bude reprezentovat jeden znak na obrazovce. Unicode totiž umožňuje například umístit diakritiku nad libovolný znak. Jedním z využití je lepší vyjádření výslovnosti daného jazyka pro cizince. Jako drobný příklad nám může sloužit anglická výslovnost, která používá kombinace znaků, které nejsou standardní součástí ASCII znakové sady a vypomáhá si kdejakým znaménkem, aby lépe vyjádřila přízvuk.

Paměťové nároky

Není pravdou, že UTF-8 je nejúspornější variantou pro ukládání textu. Nejúspornější může být pro anglický text, kde může využívat téměř výhradně jednobajtových znaků. Pravděpodobně bude nejúspornější i po dobu, kdy budete překládat vaši aplikaci do jazyků založených na latince, t.j. znacích abecedy většiny zemí Evropy, Ameriky a dalších kontinentů. Nicméně už při pokusu překladu do ruštiny (používá znaky cyrilice) mám velké pochybnosti o úspornosti UTF-8, nemluvě už vůbec o japonštině, čínštině a jazycích arabského světa. Všechny tyto jazyky budou pro téměř všechny znaky využívat dvou až šestibajtové reprezentace znaků. Pro tyto jazyky je paměťově výhodnější používat UTF-16.

Rychlost

Pokud neděláme aplikaci zpracovávající velké množství textu, není otázka rychlosti zpracování textů většinou problém. Většina funkcí pro práci s unicode je efektivně napsaná a není potřeba zvažovat zvláštní optimalizace. Obecné doporučení při designu aplikace je použít kódování, které je používáno na dané platformě. Píšete-li čistě pro Linux, není špatnou volbou UTF-8. Používáte-li knihovnu Qt nebo píšete-li čistě pro Windows, UTF-16 zabrání zbytečným konverzím mezi kódováním textu vaší aplikace a Qt nebo Windows. Chcete-li zůstat platformě neutrální a nepoužívat Qt, zůstane pravděpodobně vaše volba mezi std::string (UTF-8) a std::wstring (UTF-16 na Windows, UTF-32 na Linuxu).

Příklady

Používá-li váš zdroják unicode, je velmi snadné tisknout text do konzole. Tedy, alespoň na mém Linuxu Ubuntu 10.10. Ale čekám, že příklad bude fungovat na všech moderních linuxových distribucích. Windowsáci moment počkají na příklad funkční na jejich OS a linuxoví odborníci můžou žhavit závity, aby přidali kus moudrosti do diskuze pod článkem.

#include <stdio.h>
int main(int argc, char* argv[])
{
   printf("%s\n", "Pozdrav z Evropy: Schöne Grüße");
   printf("%s\n", "Něco z Arabštiny: يؤلمني.");
   printf("%s\n", "A ještě něco z Japonštiny: 私はガラスを食");
   char* twochars = "\xe6\x97\xa5\xd1\x88";
   printf("%s\n", twochars);
   return 0;
}

Chceme-li použít std::string situace je jednoduchá. Pro std::wstring však nefunguje L"Текст на кирилица". Pokud někdo zná důvod, ať jej uvede v diskuzi pod článkem.

#include <iostream>
std::wstring stringToWString(const std::string& s)
{
   std::wstring temp(s.length(),L' ');
   std::copy(s.begin(), s.end(), temp.begin());
   return temp;
}
int main(int argc, char* argv[])
{
   std::string s("Pozdrav z Evropy: Schöne Grüße");
   std::cout << s << std::endl;
   std::wstring ws(stringToWString("Текст на кирилица"));
   std::wcout << ws << std::endl;
   return 0;
}

A nyní již k netrpělivým Windowsákům. Naneštěstí, programování na Windows je v mnoha případech nestandardní a občas se musíte uchýlit k metodě pokus-omyl, abyste vyvinuli něco, co je ve skutečnosti tak jednoduché jako výpis textu do konzole.

#include <windows.h>
#include <iostream>
#include <assert.h>
// multi-byte string to wide-character string
static std::wstring mbToWString(const char *s)
{
   size_t l = strlen(s)+1; // include \n in the string length
   wchar_t *str = new wchar_t[l * sizeof(wchar_t)];
   int r = MultiByteToWideChar(CP_UTF8, 0, s, int(l), str, int(l));
   assert(r > 0 && "MultiByteToWideChar() failed.");
   return str;
}
int main(int argc, char* argv[])
{
   DWORD tmp;
   std::wstring ws1(mbToWString("Pozdrav z Evropy: Schöne Grüße\n"));
   WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ws1.c_str(), (DWORD)ws1.length(), &tmp, NULL);
   std::wstring ws2(mbToWString("Něco z Arabštiny: يؤلمني.\n"));
   WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ws2.c_str(), (DWORD)ws2.length(), &tmp, NULL);
   std::wstring ws3(mbToWString("A ještě něco z Japonštiny: 私はガラスを食\n"));
   WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ws3.c_str(), (DWORD)ws3.length(), &tmp, NULL);
   std::wstring ws4(mbToWString("Текст на кирилица\n"));
   WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ws4.c_str(), (DWORD)ws4.length(), &tmp, NULL);
   MessageBoxW(NULL, mbToWString("Pozdrav z Evropy: Schöne Grüße\n"
                                 "Něco z Arabštiny: يؤلمني.\n"
                                 "A ještě něco z Japonštiny: 私はガラスを食\n"
                                 "Текст на кирилица").c_str(), L"UTF-8", 0);
   return 0;
}

V message boxu uvidíte krásný unicode string, zahrnující i Arabštinu. Avšak v konzoli jsou stále ještě trable. České a německé znaky se sice zobrazily správně, ale japonština či ruština ne. Domnívám se, že je to problém fontu v konzoli a neobjevil jsem způsob, jak jej změnit. Při pokusech s printf a wprintf byl výsledek ještě horší. Ve všech případech bylo nutné použít funkci mbToWString, která zkonvertuje UTF-8 string ve zdrojáku do UTF-16, který je nativní na Windows. Pak již windowsovské funkce s W na konci začínají konečně dělat to, co od nich programátor očekává. Má-li někdo více zkušeností, ať se podělí pod článkem.

Qt nám nabízí množství funkcí pro práci s texty a jejich kódováním. Omezím se jen na pokročilejší věci a budu předpokládat znalost základní práce s textem v Qt a třídou QString. Při importu textu třídou QString často dochází ke konverzi kódování textu z externího na vnitřní reprezentaci. Vnitřní reprezentace je Utf-16, přičemž externí může být různá. Máme různé importní funkce, např. QString::fromAs­cii(), nebo QString::from­Latin1(), či QString::from­Local8Bit(). Tyto funkce používají vnitřně různé Qt kodeky, které provádějí vlastní konverzi. Teoreticky můžeme tyto kodeky i nastavovat dle vlastní potřeby. A opravdu toho bude potřeba. Nicméně nepředbíhejme a podívejme se na různé možnosti konstrukce QString a jeho obsahu. Samozřejmě nevyčerpáme všechny možnosti, ale alespoň ty často používané:

// konstrukce využívající kodek pro CStrings
QString a("Pozdrav z Evropy: Schöne Grüße\n");
QString b = QString::fromStdString("Něco z Arabštiny: يؤلمني.\n");
// konstrukce využívající kodek pro locale – lokálně nastavenou znakovou sadu
QString c = QString::fromAscii("Pozdrav z Evropy: Schöne Grüße\n");
QString d = QString::fromLocal8Bit("Něco z Arabštiny: يؤلمني.\n");
// konstrukce provádějící konverzi z Latin1 znakové sady
QString e = QLatin1String("Pozdrav z Evropy: Schöne Grüße\n");
QString f = QString::fromLatin1("Něco z Arabštiny: يؤلمني.\n");
// konstrukce provádějící konverzi z Utf-8
QString g = QString::fromUtf8("Utf-8 string: Schöne Grüße\n");
// výpis do Linuxové konzole
QString s(a+b+c+d+e+f+g);
std::cout << s;
std::cout << std::endl;
// výpis do Windowsové konzole
#if defined(__WIN32__) || defined(_WIN32)
DWORD tmp;
WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), s.utf16(), (DWORD)s.length(), &tmp, NULL);
#endif
// text v okně
QMessageBox::information(NULL, QString("Info"), s);

Spustíte-li tento zdroják na Linuxu, měl by fungovat bez nejmenšího zaváhání. Jediný text, který bude vždy zobrazen špatně, je ten, který jsme vytvořili pomocí Latin1 konverze – předhodíme-li jí arabské znaky, nemůže konverze dopadnout dobře. Trochu překvapivě fungují QString::fromAscii a fromLocal8Bit správně, neboť použijou korektní konverzi z Utf-8 pravděpodobně díky tomu, že můj Ubuntu 10.10 používá Utf-8 locale. A konverzní rutiny z c-stringů jsou nastaveny také korektně na této platformě, aby používaly Utf-8.

Jiná situace je na Windows – z pohledu našich příkladů poněkud komplikované platformě. Na první pokus se vypíše korektně pouze text zkonstruovaný přes QString::fromUt­f8(). Ostatní texty jsou v pořádku, dokud používají pouze standardní znaky bez diakritiky a arabštiny. Kodeky pro c-strings a locale jako by nebyly nastaveny správně. Zdá se, jako by Qt (testováno na verzi 4.7.1 v MSVC 2005) nevědělo, že náš zdroják je napsán v Utf-8 a že chceme, aby použil Utf-8 kodeky. Můžeme mu je tedy nastavit manuálně. Nad náš kód přidáme:

widgety

QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));

Po té již vidíme vidíme většinu textu korektně vypsaného do konzole i do message boxu. Špatně zůstává pouze text zkonvertovaný Latin1 kodekem – přesně jak čekáme. Navíc MSVC zřejmě neumí vypisovat unicode text přes std::cout << s. Naštěstí jde situace obejít přes funkci WriteConsoleW().

Závěrem hodně štěstí s Unicode ve vašich programech a neváhejte se podělit se svými zkušenostmi v diskuzi pod článkem, případně přidat další zajímavé informace užitečné nám všem.

Našli jste v článku chybu?
120na80.cz: Ochablé svaly mohou značit vážnou nemoc

Ochablé svaly mohou značit vážnou nemoc

DigiZone.cz: Banaxi: videa kdekoli na světě

Banaxi: videa kdekoli na světě

DigiZone.cz: Na jaká videa se vlastně díváme

Na jaká videa se vlastně díváme

Lupa.cz: Proč jsou firemní počítače pomalé?

Proč jsou firemní počítače pomalé?

Vitalia.cz: 5 nemocí, se kterými pomáhá urologie

5 nemocí, se kterými pomáhá urologie

Podnikatel.cz: Letáky? Lidi zuří, ale ony stále fungují

Letáky? Lidi zuří, ale ony stále fungují

Lupa.cz: Další Češi si nechali vložit do těla čip

Další Češi si nechali vložit do těla čip

120na80.cz: Galerie: Čínští policisté testují českou minerálku

Galerie: Čínští policisté testují českou minerálku

Podnikatel.cz: „Lex Babiš“ Babišovi paradoxně pomůže

„Lex Babiš“ Babišovi paradoxně pomůže

Lupa.cz: Jak se prodává firma za miliardu?

Jak se prodává firma za miliardu?

DigiZone.cz: DVB-T2 ověřeno: seznam TV zveřejněn

DVB-T2 ověřeno: seznam TV zveřejněn

Podnikatel.cz: ČSSZ posílá přehled o důchodovém kontě

ČSSZ posílá přehled o důchodovém kontě

Vitalia.cz: Antibakteriální mýdla nepomáhají, spíš škodí

Antibakteriální mýdla nepomáhají, spíš škodí

Vitalia.cz: Tradiční čínská medicína a rakovina

Tradiční čínská medicína a rakovina

Podnikatel.cz: Instalatér, malíř a elektrikář. "Vymřou"?

Instalatér, malíř a elektrikář. "Vymřou"?

Lupa.cz: Patička e-mailu závazná jako vlastnoruční podpis?

Patička e-mailu závazná jako vlastnoruční podpis?

Lupa.cz: Blíží se konec Wi-Fi sítí bez hesla?

Blíží se konec Wi-Fi sítí bez hesla?

Vitalia.cz: Voda z Vltavy před a po úpravě na pitnou

Voda z Vltavy před a po úpravě na pitnou

Root.cz: Hořící telefon Samsung Note 7 zapálil auto

Hořící telefon Samsung Note 7 zapálil auto

DigiZone.cz: Technisat připravuje trojici DAB

Technisat připravuje trojici DAB