Сегодня мне хотелось бы рассказать о некоторых приёмах работы с системой перевода интерфейса в Qt. На первый взгляд может показаться, что всё предельно просто (тривиальные примеры в документации этому способствуют). Ну в самом деле, что сложного? Обёртываем строку в tr() или translate(), генернируем файл .ts, переводим, собираем в файл .qm, загружаем его — вот и всё. Но практика, как известно, зачастую несколько расходится с теорией.
Итак, переходим к примеру. Допустим, есть некое приложение, интерфейс которого должен быть переведён на пару-тройку языков. При этом к нему прилагается библиотека, в свою очередь также содержащая элементы, нуждающиеся в переводе. Итого: для каждого языка (кроме, возможно, исходного английского) имеется набор из трёх файлов переводов: appname_xx.qm, libname_xx.qm и (чтобы текст на кнопках подтверждения, отмены и т.д. также был на выбранном языке) qt_xx.qm (тут xx — имя локали для выбранного языка). Это ещё довольно простой случай, ведь иногда основной проект может зависеть от нескольких вспомогательных библиотек, и тогда файлов переводов может быть больше.
Уже посложнее, чем пример из документации, верно? А если мы, к примеру, хотим предоставить наиболее продвинутым пользователям возможность самим переводить интерфейс на их родной язык? В таком случае обычно предлагается помещать нужный файл переводов в соответствующую папку в домашнем каталоге. Итак, что мы имеем: несколько файлов переводов для разных частей приложения, расположенных в разных местах. Их обнаружение и загрузка — дело уже далеко не такое простое, как загрузка одного единственного файла перевода с известным путём.
Что же делать? Выход, конечно же, есть — создать класс, который брал бы на себя обязанности по обнаружению, загрузке и дальнейшему использованию переводов. Начнём, пожалуй:
Уфф, половину проблемы решили. Осталось разобраться с получением списка языков, для которых доступны переводы. Приступим:
Однако, пока мы решали проблему удобства, другая, не менее важная проблема — проблема производительности — осталась за кадром. А она имеет место быть. Как известно, перевод интерфейса "на лету" осуществляется путём переопределения метода QWidget::changeEvent, проверки типа события (он должен быть QEvent::LanguageChange) и, собственно, перевода видимых элементов с использованием функции tr. А теперь представьте себе программу, содержащую около тысячи видимых и подлежащих переводу элементов. Пользователь меняет язык интерфейса и все пять (к примеру) файлов перевода сначала убираются, а затем устанавливаются вновь (с другой локалью), вызывая аж десять событий смены языка. Итого — десять тысяч вызовов setText, setToolTip и т.д. Это уже будет заметно на глаз (и едва ли приятно, так как приложение на секунду "подвисает").
Надо что-то делать. Например, можно воспользоваться вот этим примером. Суть его такова: создаётся объект-посредник, к его слоту подсоединяется некий сигнал. При вызове слота запускается два таймера: "промежуточный" (в примере он назван "минимальным", что, на мой взгляд, не совсем корректно) и "максимальный". Если в течение некоторого промежутка времени слот вызывается снова, то "промежуточный" таймер перезапускается. Как только один из таймеров срабатывает, объект испускает соответствующий сигнал. Пример по ссылке выше приспособлен под определённую ситуацию, я же несколько "причесал" его под общий случай. Скачать можно тут.
А теперь то, ради чего было это лирическое отступление. Создадим ещё один простенький класс, который установим в качестве фильтра событий на экземпляр QApplication (или QCoreApplication). Вот он:
Итак, переходим к примеру. Допустим, есть некое приложение, интерфейс которого должен быть переведён на пару-тройку языков. При этом к нему прилагается библиотека, в свою очередь также содержащая элементы, нуждающиеся в переводе. Итого: для каждого языка (кроме, возможно, исходного английского) имеется набор из трёх файлов переводов: appname_xx.qm, libname_xx.qm и (чтобы текст на кнопках подтверждения, отмены и т.д. также был на выбранном языке) qt_xx.qm (тут xx — имя локали для выбранного языка). Это ещё довольно простой случай, ведь иногда основной проект может зависеть от нескольких вспомогательных библиотек, и тогда файлов переводов может быть больше.
Уже посложнее, чем пример из документации, верно? А если мы, к примеру, хотим предоставить наиболее продвинутым пользователям возможность самим переводить интерфейс на их родной язык? В таком случае обычно предлагается помещать нужный файл переводов в соответствующую папку в домашнем каталоге. Итак, что мы имеем: несколько файлов переводов для разных частей приложения, расположенных в разных местах. Их обнаружение и загрузка — дело уже далеко не такое простое, как загрузка одного единственного файла перевода с известным путём.
Что же делать? Выход, конечно же, есть — создать класс, который брал бы на себя обязанности по обнаружению, загрузке и дальнейшему использованию переводов. Начнём, пожалуй:
class Translator
{
private:
QString mfileName;
QLocale mlocale;
bool minstalled;
QList<QTranslator *> mtranslators;
public:
QString fileName() const
{
return mfileName;
}
QLocale locale() const
{
return mlocale;
}
bool isValid() const
{
return !mfileName.isEmpty();
}
bool isInstalled() const
{
return minstalled;
}
void install()
{
if (minstalled || !isValid())
return;
QStringList paths; //в эту переменную добавляем все пути к переводам (то есть список абсолютных путей к папкам)
foreach (QString path, paths)
{
QTranslator *t = new QTranslator;
if (t->load(mlocale, mfileName, "_", path, ".qm"))
{
mtranslators << t;
QCoreApplication::installTranslator(t);
}
else
{
delete t;
}
}
minstalled = true;
}
void remove()
{
if (!minstalled)
return;
foreach (QTranslator *t, mtranslators)
{
QCoreApplication::removeTranslator(t);
delete t;
}
mtranslators.clear();
minstalled = false;
}
void setFileName(QString fn)
{
bool wasInstalled = isInstalled();
if (wasInstalled)
remove();
mfileName = fn;
if (!isValid())
return;
if (wasInstalled)
install();
}
void setLocale(QLocale l)
{
bool wasInstalled = isInstalled();
if (wasInstalled)
remove();
mlocale = l;
if (wasInstalled)
install();
}
public:
explicit Translator(QString fn = QString(), QLocale l = QLocale())
{
minstalled = false;
setFileName(fn);
setLocale(l);
}
~Translator()
{
remove();
}
};
Что это мы тут нагородили? Давайте разбираться. Мы обернули список указателей на QTranslator в удобный для работы класс. Этот класс имеет методы для смены имени файла (например, это appname у файла appname_ru.qm или libname у файла libname_fr.qm, то есть, если можно так выразиться, "значащая" часть имени), а также для смены локали. И, конечно же, мы можем устанавливать и убирать экземпляры нашего класса. При этом внутри используется всё тот же знакомый по документации метод QCoraApplication::installTranslator. Данный класс почти готов к использованию, достаточно только указать, в каких папках искать переводы (метод install, переменная paths).Уфф, половину проблемы решили. Осталось разобраться с получением списка языков, для которых доступны переводы. Приступим:
QList<QLocale> availableLocales(QString fileName)
{
if (fileName.isEmpty())
return QList<QLocale>();
QList<QLocale> list;
QStringList validFiles;
QStringList paths; //тут вновь перечисляем пути к папкам, где могут лежать файлы переводов
foreach (QString path, paths)
{
QDir dir(path);
QStringList files = dir.entryList(QStringList() << "*.qm", QDir::Files); //получаем список файлов .qm
foreach (QString file, files)
{
//проверяем, что "значимая" часть имени файла соответствует имени, переданному в качестве параметра
if (file.left(fileName.length()) == fileName && !validFiles.contains(file))
validFiles << file;
}
}
foreach (QString file, validFiles)
{
int suffixPos = fileName.length() + 1; //индекс, с которого начинается "суффикс" имени файла, содержащий имя локали
int suffixLength = validFile.length() - fileName.length() - 4; //длина суффикса: вычитаем длину "значимой" части, длину "_" и длину ".qm"
QString localeName = validFile.mid(suffixPos, suffixLength);
QLocale l(localeName);
list << l;
}
return list;
}
Вот такая простая, но полезная функция. Тут следует заметить, что при использовании её, а также приведенного выше класса предполагается, что файлы переводов именуются следующим образом: name_xx.qm, где name — "значимая" часть имени, соответствующая названию программы или библиотеки, xx — имя локали, а .qm - стандартный суффикс для файлов переводов, генерируемых средствами Qt.Однако, пока мы решали проблему удобства, другая, не менее важная проблема — проблема производительности — осталась за кадром. А она имеет место быть. Как известно, перевод интерфейса "на лету" осуществляется путём переопределения метода QWidget::changeEvent, проверки типа события (он должен быть QEvent::LanguageChange) и, собственно, перевода видимых элементов с использованием функции tr. А теперь представьте себе программу, содержащую около тысячи видимых и подлежащих переводу элементов. Пользователь меняет язык интерфейса и все пять (к примеру) файлов перевода сначала убираются, а затем устанавливаются вновь (с другой локалью), вызывая аж десять событий смены языка. Итого — десять тысяч вызовов setText, setToolTip и т.д. Это уже будет заметно на глаз (и едва ли приятно, так как приложение на секунду "подвисает").
Надо что-то делать. Например, можно воспользоваться вот этим примером. Суть его такова: создаётся объект-посредник, к его слоту подсоединяется некий сигнал. При вызове слота запускается два таймера: "промежуточный" (в примере он назван "минимальным", что, на мой взгляд, не совсем корректно) и "максимальный". Если в течение некоторого промежутка времени слот вызывается снова, то "промежуточный" таймер перезапускается. Как только один из таймеров срабатывает, объект испускает соответствующий сигнал. Пример по ссылке выше приспособлен под определённую ситуацию, я же несколько "причесал" его под общий случай. Скачать можно тут.
А теперь то, ради чего было это лирическое отступление. Создадим ещё один простенький класс, который установим в качестве фильтра событий на экземпляр QApplication (или QCoreApplication). Вот он:
class LanguageChangeNotifier : public QObject
{
Q_OBJECT
private:
SignalDelayProxy *proxy;
public:
explicit LanguageChangeNotifier()
{
proxy = new SignalDelayProxy(50, 100 this);
connect(proxy, SIGNAL(triggered()), this, SIGNAL(languageChanged()));
}
public:
bool eventFilter(QObject *o, QEvent *e)
{
if (e->type() != QEvent::LanguageChange)
return false;
proxy->trigger();
return true;
}
signals:
void languageChanged();
};
Устанавливаем наш фильтр: LanguageChangeNotifier *notifier = new LanguageChangeNotifier;
QApplication::installEventFilter(notifier);
Готово! Теперь в виджетах, где требуется перевод элементов, добавляем слот: void OurWidget::retranslateUi()
{
//тут переводим интерфейс, например так:
myLabel->setText(tr("some text"));
}
И где-нибудь в конструкторе, ссылаясь на глобальный экземпляр LanguageChangeNotifier: connect(notifier, SIGNAL(languageChanged()), this, SLOT(retranslateUi()));
Теперь мы получим крохотную задержку в одну десятую секунды (которую едва ли можно заметить) перед сменой языка интерфейса, зато избавимся от потенциально гораздо более существенной задержки, а также от зависания приложения из-за десяти тысяч лишних вызовов функций (от которых мы, к слову, тоже избавимся).
Спасибо за статью. Отличный повод привести переводы в порядок.
ОтветитьУдалить