Portál AbcLinuxu, 30. dubna 2025 15:24
V dnešním díle se dozvíte, jak program lokalizovat a jak k němu korektně přibalit nějaká data - třeba obrázky, ikony nebo zvuky.
Hned na začátku seriálu jsem doporučoval označovat všechny řetězce viditelné uživateli pro překlad, což se dělá vložením řetězce do metody tr()
třídy QObject
. Když budete chtít překladatelům trochu usnadnit práci, můžete do kódu přidat ještě speciální komentáře, které budou obsahovat například informace o kontextu, což se hodí především u rozsáhlých projektů. Tyto komentáře se vytvářejí tak, že přímo nad řádek s řetězcem označeným pro překlad vytvoříte C++ komentář začínající dvojtečkou:
//: this is the hidden button hBtn = new QPushButton(tr("Explode!")); /*: example string */ QString str(tr("Go"));
Tohle ovšem nestačí k tomu, aby program použil dodané překlady. Je třeba načíst překlad ve funkci main()
pomocí vytvoření a registrace objektu QTranslator
, což je vysvětleno o kousek níže, u zdrojového kódu main.cpp
. Z programátorského hlediska nám tyto znalosti většinu času postačí. Jak ale vytvořit překlad?
Je nutné přidat do projektového souboru (s příponou .pro
) následující:
# "cs" je ISO 639 kód pro češtinu, "CZ" je ISO 3166 kód pro ČR TRANSLATIONS = calendar_cs_CZ.ts
A v případě, že nechceme překlady ve výchozím kódování (Latin1), ale v UTF-8, tak ještě
CODECFORTR = UTF-8
Nyní, když spustíte program lupdate
s připraveným .pro souborem jako argument, vygenerují (a později už jen aktualizují) se vám soubory, které jste zadali v .pro
souboru do proměnné TRANSLATIONS
.
$ lupdate calendar.pro Updating 'calendar_cs_CZ.ts'... Found 1 source text(s) (1 new and 0 already existing)
Tímto nám vznikl soubor calendar_cs_CZ.ts
, což je jakési XML, které lze editovat pomocí grafického nástroje Qt Linguist. Spustíme si jej tedy a otevřeme v něm tento soubor (z příkazové řádky pomocí linguist calendar_cs_CZ.ts
). Zvolíme jazyk, do kterého překládáme, a můžeme se do toho pustit. V případě tohoto ukázkového programu není příliš co překládat, takže přeložíme "Add" jako "Přidat", uložíme změny a tím naše překladatelská práce hasne, od toho jsou tu přece jiní :-)
Posledním krokem je spuštění lrelease
(opět s .pro
souborem jako argument), což vygeneruje z .ts
souborů binární soubory s příponou .qm
, které pak program načítá.
$ lrelease calendar.pro Updating '/home/dave/abcl/qt4-7/calendar/calendar_cs_CZ.qm'... Generated 1 translation(s) (1 finished and 0 unfinished)
Teď, když spustíte program z adresáře, kde je soubor calendar_cs_CZ.qm
, tak za předpokladu, že máte systémový jazyk nastavený na češtinu, se vám program spustí v češtině.
Ukázkový program pro tento díl obsahuje kalendář (QCalendarWidget
) a umožňuje ke každému dni napsat libovolný počet poznámek. Poznámky se ukládají do konfiguračního souboru přes objekt QSettings
, jehož použití jsme si ukazovali již ve druhém díle.
main.cpp
: Zde je novinkou zavedení lokalizace. Vytvoříme si instanci objektu QTranslator
, jehož metodou load()
načteme soubor s překladem. Metodě předáváme název .qm
souboru bez přípony. Jako druhý argument můžeme předat cestu k adresáři, kde se má soubor hledat. Načtenou lokalizaci poté zavedeme do programu metodou QCoreApplication::installTranslator()
(nejjednoduššeji přes instanci QApplication
).
#include <QApplication> #include <QLocale> #include <QTextCodec> #include <QTranslator> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); // nastavíme název programu a doménu pro QSettings app.setApplicationName("calendar"); app.setOrganizationDomain("watzke.cz"); // kódování pro lokalizaci bude UTF-8 QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8")); QTranslator t; /* načteme překlad podle aktuální locale ze souboru ve tvaru: * název programu + podtržítko + jazyk_země * kde jazyk je dvoumístný kód jazyka (ISO 639) malými písmeny a * země je dvoumístný kód země (ISO 3166) velkými písmeny, např. cs_CZ */ t.load( app.applicationName() + "_" + QLocale::system().name() ); // a přiřadíme jej k této instanci programu app.installTranslator(&t); MainWindow win; win.show(); return app.exec(); }
mainwidget.h
: API.
#ifndef MAINWIDGET_H #define MAINWIDGET_H #include <QWidget> #include <QSettings> class QCalendarWidget; class QLineEdit; class QListWidget; class MainWidget : public QWidget { Q_OBJECT public: MainWidget(); private: QSettings settings; QCalendarWidget* calendar; QLineEdit* lineEdit; QListWidget* list; QString getDate(); private slots: void addItem(); void removeItem(); void listItems(); }; #endif // MAINWIDGET_H
mainwidget.cpp
: Zde je novinkou použití tříd QDate
a QDateTime
. Slouží pro manipulaci s datem, respektive datem a časem. Mají například statické metody pro získání aktuálního data a umí jej různě formátovat.
#include "mainwidget.h" #include <QAction> #include <QCalendarWidget> #include <QDateTime> #include <QHBoxLayout> #include <QLineEdit> #include <QListWidget> #include <QPushButton> #include <QVBoxLayout> MainWidget::MainWidget() { // vytvoříme kalendář calendar = new QCalendarWidget; // nastavíme první den v týdnu na pondělí calendar->setFirstDayOfWeek(Qt::Monday); // skryjeme vertikální hlavičku s čísly týdnů calendar->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader); // vytvoříme jednořádkový editor textu lineEdit = new QLineEdit; // a jednoduchý seznam, list = new QListWidget; // ve kterém lze vybrat v jednu chvíli jen jednu položku list->setSelectionMode(QAbstractItemView::SingleSelection); // a který má akci, jež se spustí při stisknutí klávesy Delete QAction* removeShortcut = new QAction(list); removeShortcut->setShortcut(QKeySequence("Delete")); list->addAction(removeShortcut); // tlačítko pro přidání položky do seznamu QPushButton* addBtn = new QPushButton(tr("Add")); QHBoxLayout* lineLayout = new QHBoxLayout; lineLayout->addWidget(lineEdit); lineLayout->addWidget(addBtn); // vytvoříme vertikální rozložení QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(calendar); layout->addLayout(lineLayout); layout->addWidget(list); // propojíme ovládací prvky connect(calendar, SIGNAL(selectionChanged()), this, SLOT(listItems())); connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(addItem())); connect(addBtn, SIGNAL(clicked()), this, SLOT(addItem())); connect(removeShortcut, SIGNAL(triggered()), this, SLOT(removeItem())); // načteme položky pro výchozí (dnešní) datum po spuštění programu listItems(); } // přidá položku se zadaným textem k vybranému datu void MainWidget::addItem() { // načteme text z textového pole QString text = lineEdit->text(); // a pokud je prázdný, tak končíme if(text.isEmpty()) return; // získáme aktuální datum QString date = getDate(); if(date.isEmpty()) return; // získáme aktuální datum s časem QDateTime datetime(QDateTime::currentDateTime()); // a přidáme jej na začátek řetězce text.prepend(datetime.toString("dd.MM.yy@hh.mm: ")); // index přidávané položky (v nastavení) QString index = "0"; // přepneme se v nastavení do sekce s dnešním datem settings.beginGroup(date); // pokud jsou zde již nějaké položky, nastavíme správný index (poslední + 1) if(settings.childKeys().size()) index = QString::number(settings.childKeys().last().toInt() + 1, 10); // uložíme položku do nastavení settings.setValue(index, text); // a nezapomeneme (!) vyskočit ze sekce s dnešním datem settings.endGroup(); // nakonec přidáme položku i do seznamu QListWidgetItem* item = new QListWidgetItem(text); list->addItem(item); list->setCurrentItem(item); // a vyprázdníme textové pole lineEdit->clear(); } // odstraní vybranou položku void MainWidget::removeItem() { //if(!list->count()) // return; // index vybrané položky v seznamu int row = list->currentRow(); // pokud nebyla vybrána žádná položka, tak končíme if(row < 0) return; QString date = getDate(); if(date.isEmpty()) return; settings.beginGroup(date); // index vybrané položky v nastavení QString key = settings.childKeys().at(row); // pokud odstraňujeme poslední položku, odstraníme i sekci daného data if(settings.childKeys().size() == 1) settings.remove(""); else settings.remove(key); settings.endGroup(); // nakonec odstraníme položku i ze seznamu delete list->takeItem(row); } // načte položky k danému dni z nastavení do seznamu void MainWidget::listItems() { // vyprázdníme seznam list->clear(); QString date = getDate(); if(date.isEmpty()) return; settings.beginGroup(date); // postupně načteme do seznamu všechny položky v sekci daného data foreach(QString key, settings.childKeys()) list->addItem(settings.value(key).toString()); settings.endGroup(); // a označíme list->setCurrentRow(0); } // vrátí aktuální datum jako řetězec v ISO formátu (yyyy-MM-dd) QString MainWidget::getDate() { QDate qdate = calendar->selectedDate(); if(qdate.isNull()) return QString(); return qdate.toString(Qt::ISODate); }
Zdrojáky si můžete stáhnout v archívu calendar.tar.bz2.
Často se stane, že je potřeba program dodávat s různými vlastními daty. Může jít například o ikony, obrázky nebo zvuky. Qt pro tuhle situaci má řešení, a to Resource System. Standardní přístup je ten, že se binární data, která program potřebuje, zakompilují do binárky programu. A jak to udělat?
Musíme si (v adresáři s programem) vytvořit .qrc
soubor, což je vlastně XML, ve kterém uvedeme všechny potřebné soubory. Máme dvě možnosti: Buď můžeme použít Qt Creator, v něm si do projektu přidat "resource file" a všechno si naklikat, nebo si tento soubor vytvořit ručně. Popíšu zde pouze ruční metodu, abych vysvětlil, co se vlastně ve skutečnosti děje. Náš ukázkový soubor resources.qrc
bude vypadat následovně:
<!DOCTYPE RCC><RCC version="1.0"> <qresource> <file>data/image.jpg</file> <file alias="abcicon.png">data/icon.png</file> </qresource> </RCC>
Do párového tagu file
po jednom vložíme cesty ke všem souborům. Cesty jsou relativní k umístění .qrc
souboru. Atribut alias umožňuje zadat alias, pod kterým bude daný soubor dostupný (viz níže).
Nyní o tomto souboru dáme vědět qmake
tím, že přidáme do .pro souboru tento řádek:
RESOURCES += resources.qrc
To je vše. K zakompilovaným souborům lze přistupovat zadáním relativní cesty za prefix :/
. Třeba takto:
QPixmap icon(":/abcicon.png");
main.cpp
: Ukázka načtení ikonky okna a obrázku. Program je tak jednoduchý, že můžeme udělat vše ve funkci main()
.
#include <QApplication> #include <QIcon> #include <QLabel> int main(int argc, char *argv[]) { QApplication a(argc, argv); QLabel w; // nastavíme ikonu okna (favicon abclinuxu) w.setWindowIcon(QIcon(":/abcicon.png")); // nastavíme bílé pozadí w.setPalette(QPalette(Qt::white)); w.setAutoFillBackground(true); // načteme průhledný gif (logo abclinuxu) w.setPixmap(QPixmap(":/data/image.gif")); w.show(); return a.exec(); }
Zdrojáky si můžete stáhnout v archívu resources.tar.bz2.
V příštím díle si mimo jiné povíme něco o stylování GUI pomocí CSS.
Na to kolik ti je..vsechna cest..prvni slusnej ceskej serial o qt..
Jak je udělána lokalizace samotného Qt? Jde mi o to, jestli se aplikace musí starat o načtení například /usr/share/qt4/translations/qt_cs.qm sama, nebo to za ni udělá knihovna sama. Viděl jsem totiž programy (pravda, bylo to Qt3), které si s sebou táhly qt_*.qm, což mi moc systémové nepřišlo.
Nieco ako puzzle? Odporucam pozriet programik qtdemo. Je tam vela zaujimavych ukazok.
Na niečo také by sa dal použiť Qt Animation Framework, ale na takú pomerne jednoduchú vec mi to pripadá trochu overkill. Potom jak tu už niekto spomínal v qtdemo je dosť všeliakých ukážok...
Není mi jasná jedna věc: jestliže tvůj překlad obsahuje jen jeden záznam, jak je možné, že je lokalizovaný i samotný kalendář?
Nechce se mi teď hledat, jak nejrychleji a dočasně přepnout systém do češtiny, takže jen hádám, že komponenta kalendáře je ve skutečnosti přímo součást KDE a tím pádem je lokalizovaná podle systému a ne jak si člověk nastaví v aplikaci...
Nebo sis snad sám pro sebe lokalizoval Qt do češtiny?
Do článku bych ještě přidal zmínku, že Qt jako takové obsahuje taky spoustu textu připraveného k lokalizaci (tlačítka, chybové hlášky, atd.), proto je vhodné v nových aplikacích nahrát qm soubor i pro samotné Qt knihovny. Česká lokalizace zatím není dostupná, ale slovenská ano. Jednoduše tak učiníme přidáním kódu:
QTranslator qtTranslator; if (qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath))) app.installTranslator(&qtTranslator);
K tématu lokalizace bych už dodal jen, že je možné qm souborů loadnout libovolný počet, přičemž se vůbec nerozlišuje, který soubor přísluší k jaké části zdrojáku. Tzn. že uvnitř funkce tr (a jí podobných) se prochází seznam překladů od shora dolů dokud se nenarazí na první záznam, který je stejný jako řetězec předaný do tr. To představuje menší komplikaci v případě, kdy stejný text v jazyce programu (zpravidla anglický) chceme na různých místech přeložit jinak, např. Tlačítko "Set" přeložíme asi jako "Nastavit", kdežto třeba label "Set" před comboboxem třeba jako "Množina". Řešením je nastavit hodnotu i druhému argumentu funkce tr, samozřejmě pro jiný překlad jiný comment...
Jinak všechna čest, jen tak dál! (... na to kolik ti je )
Toto je serial o Qt, nie KDE. V takom pripade su samozrejme nutne vlastne preklady (vacsinou nestaci prelozit len Yes, No ...). Ale zmienka o preklade samotneho Qt tu mohla byt. Inak suhlasim s tym, ze je to super serial.
Myslel jsem to tak, že Qt samotné asi neimplementuje celý widget kalendáře, ale má jen nějaký wrapper okolo nativní komponenty systému. Kalendář tedy vypadá na každé platformě jinak, ale nevím, hned večer to vyzkouším na widlích...
/usr/share/qt/translations
QTranslator translator; translator.load(":/translations/qt_sk"); app.installTranslator(&translator);
Tím asi nedosáhneš kýženého výsledku. Spíš bych to viděl tak, že klient kromě vstupních dat pošle i používaný jazyk, server si hned po navázání spojení natáhne lokalizaci (installTranslator
) ve správném jazyce a po ukončení spojení lokalizaci zase odstraní (removeTranslator
).
Jen si nejsem jistý jak se to bude chovat, když se připojí více klientů současně, protože lokalizace je společná pro celou aplikaci...
Nebo použij něco normálního.
Ja bych pro kazdeho klienta fork()oval, a v kazdem procesu pak nastavil jinou textdomain().
QTranslator::translate
, napriklad:
Server:
class Translator: public QObject { public: Translator(QObject *parent = NULL): QObject(parent) { QTranslator *skTranslator = new QTranslator(this); QTranslator *csTranslator = new QTranslator(this); skTranslator->load(":/translations/qt_sk"); csTranslator->load(":/translations/qt_cs"); m_tableOfTranslators["sk"] = skTranslator; m_tableOfTranslators["cs"] = csTranslator; } // vrati lokalizovanu spravu na zaklade jazyka lang QString translateForClient(const QString &message, const QString &lang) { if (m_tableOfTranslators.contains(lang) == false) { return message; } return m_tableOfTranslators[lang]->translate(message); } private: QHash<QString, QTranslator *> tableOfTranslators; }; //...niekde v kode QString message = translator->translateForClient("%n file found", client->lang());
Nějak tak by to mohlo fungovat. Nedošlo mi, že QCoreApplication::translate
volá QTranslator::translate
pro každý nainstalovaný slovník...
Je to trochu mimo téma článku, ale zajímalo by mě jestli je v Qt možné přiřadit jednomu tlačítku více klávesových zkratek. Pokud jsem to pochopil správně, tak je možné přiřadit pouze sekvenci kláves .
virtual void keyPressEvent(QKeyEvent * event)
metody dialogu/okna by mohol pomoct, napriklad:
void MainWindow::keyPressEvent(QKeyEvent * event) { switch (event->key()) { case Qt::Key_C: case Qt::Key_D: case Qt::Key_E: nejakaMetoda(); break; default: break; } }
Dekuji. Neni to sice uplne ono, ale je to moznost. V idealnim pripade bych potreboval, aby se pri stisku jakekoli z pridelenych klaves zamacklo tlacitko jako u klavesy, ktera je k tlacitku asociovana.
Vyzkousel jsem si prepinani jazyka zmenou systemove promenne LANG, diky ktere muzu menit nacteny jazyk a obrazky z resources (pokud u prefixu nastavim jazyk). Existuje nejaka moznost jak prepinat jazyky za behu programu?
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.