Глава 17. Интернационализация
Кроме латинского алфавита, используемого для английского и многих европейских языков, Qt 4 обеспечивает широкую поддержку остальных мировых систем записи:
• Qt применяет Unicode в программном интерфейсе и во внутренних операциях. В приложении можно обеспечить всем пользователям одинаковую поддержку независимо от того, какой язык применяется в пользовательском интерфейсе;
• текстовый процессор Qt может работать со всеми основными нелатинскими системами записи, в том числе с арабской, китайской, кириллицей, ивритом, японской, корейской, тайской и с языками Индии;
• процессор компоновки Qt обеспечивает компоновку справа налево для таких языков, как арабский и иврит;
• для определенных языков требуются специальные методы ввода текста. Такие виджеты редактирования, как QLineEdit и QTextEdit, хорошо работают в условиях применения любого метода ввода текста, существующего в системе пользователя.
Разрешение ввода текста пользователями на их родном языке часто оказывается недостаточным; необходимо также перевести весь пользовательский интерфейс. В Qt это делается просто: все видимые пользователем строки обработайте функцией tr() (как это мы делали в предыдущих главах) и воспользуйтесь утилитами Qt для подготовки файлов перевода на требуемый язык. Qt имеет утилиту с графическим пользовательским интерфейсом, которая называется Qt Linguist и предназначается для переводчиков. Qt Linguist дополняется двумя консольными программами lupdate и lrelease, которые обычно используются разработчиками приложений.
В большинстве приложений файл перевода загружается при запуске приложения с учетом установленных пользователем параметров локализации. Однако в некоторых случаях пользователям необходимо переключаться с одного языка на другой во время выполнения приложения. Это, несомненно, можно делать в Qt, хотя и потребует немного дополнительной работы. А благодаря системе компоновки Qt различные компоненты интерфейса пользователя будут автоматически перенастраиваться, чтобы обеспечить достаточно места для переведенного текста, когда его размер превышает размер исходного текста.
Работа с
Unicode
Unicode является стандартной кодировкой, которая поддерживает большинство мировых систем записи. В основе кодировки Unicode лежит идея использования для хранения символов 16 бит, а не 8, и поэтому она позволяет закодировать примерно 65 000 символов вместо только 256. Unicode содержит коды ASCII и ISO 8859-1 (Latin-1) в качестве своего подмножества с прежним их представлением. Например, английская буква «А» имеет значение 0x41 в кодировках ASCII, Latin-1 и Unicode, а буква «В» имеет значение 0xD1 в кодировках Latin-1 и Unicode. Класс Qt QString хранит строковые значения в кодировке Unicode. Каждый символ QString имеет 16-битовый тип QChar, а не 8-битовый тип char. Ниже приводятся два способа установки первого символа строки на значение «А»:
str[0] = 'A';
str[0] = QChar(0x41);
Если исходный файл имеет кодировку Latin-1, задавать символы Latin-1 очень легко:
str[0] = 'C';
Но если исходный файл имеет другую кодировку, хорошо срабатывает вариант с числовым кодом:
str[0] = QChar(0xD1);
Мы можем задать любой символ Unicode с помощью его числового кода. Например, ниже показано, как задается прописная буква «сигма» греческого алфавита («Σ») и символ валюты евро («€»):
str[0] = QChar(0x3A3);
str[0] = QChar(0x20AC);
Все числовые коды, поддерживаемые кодировкой Unicode, можно найти в сети Интернет по адресу . Если вам приходится редко использовать символы Unicode, не относящиеся к Latin-1, для поиска их кодов вполне достаточно воспользоваться указанным адресом; но Qt обеспечивает более удобный способ ввода в программе Qt строк символов в кодировке Unicode, как мы увидим позднее в данном разделе.
Текстовый процессор в Qt 4 поддерживает на всех платформах следующие системы записи: арабскую, китайскую, кириллическую, греческую, иврит, японскую, корейскую, лаосскую, латинскую, тайскую и вьетнамскую. Он также поддерживает все скрипты 4.1 в кодировке Unicode, которые не требуют специальной обработки. Кроме того, в системе X11 с Fontconfig и в последних версиях системы Windows поддерживаются следующие языки: бенгальский, деванагари, гуйарати, гурмухи, каннада, кхмерский, малайский, сирийский, тамильский, телугу, тхаана (дивехи) и тибетский. Наконец, ория поддерживается в системе X11, а монгольский и синхала поддерживаются в Windows XP. Если в системе установлен соответствующий шрифт, Qt сможет воспроизвести текст на любом из этих языков. А при установке соответствующих программ ввода текста пользователи смогут вводить в своих приложениях Qt текст на этих языках.
Программирование с использованием QChar немного отличается от программирования с применением char. Для получения числового кода символа QChar вызовите для него функцию unicode(). Для получения кода ASCII переменной типа QChar (в виде char) вызовите функцию toLatin1(). Для символов, отсутствующих в кодировке Latin-1, функция toLatin1() возвращает '\0'.
Если нам заранее известно, что все строковые данные в программе представлены в кодировке ASCII или Latin-1, мы можем использовать такие стандартные функции (определенные в файле
if (ch.isDigit() || ch.isUpper())
…
Этот фрагмент кода правильно работает для любых алфавитов, в которых различаются символы верхнего и нижнего регистров, в том числе для латинского, греческого и кириллицы.
Строку в кодировке Unicode мы можем использовать в любом месте программного интерфейса Qt, где допускается применение строки типа QString. Qt сам отвечает за правильное ее отображение и преобразование в соответствущие кодировки при взаимодействии с операционной системой.
Особенно внимательными надо быть при чтении и записи текстовых файлов. Текстовые файлы могут использовать различные кодировки, и часто оказывается невозможным определить кодировку текстового файла по его содержанию. По умолчанию QTextStream использует локальную системную 8-битовую кодировку (которая доступна при помощи функции QTextCodec::codecForLocale()), как для чтения, так и для записи. Для стран Америки и Западной Европы это обычно подразумевает кодировку Latin-1.
Если мы разработали свой собственный формат файлов и собираемся считывать и записывать произвольные символы Unicode, мы можем сохранять данные в кодировке Unicode с помощью вызова
stream.setCodec("UTF-16");
stream.setGenerateByteOrderMark(true);
до начала записи в поток QTextStream. Данные в этом случае будут сохраняться в формате UTF-16, который использует два байта для представления одного символа и который будет иметь префикс из специального 16-битового значения (признак порядка байтов Unicode, 0xFFFE), указывающего на применение файлом кодировки Unicode и на прямой или обратный порядок байтов. Формат UTF-16 идентичен представлению в памяти строк QString, и поэтому чтение и запись представленных в кодировке Unicode строк в формате UTF-16 могут выполняться очень быстро. Однако такой подход связан с перерасходом памяти при сохранении данных, представленных целиком в кодировке ASCII, в формате UTF-16, поскольку в данном случае каждый символ займет два байта вместо одного.
Другие кодировки можно задавать путем вызова функции setCodec() с указанием соответствующего объекта преобразования QTextCodec. QTextCodec осуществляет преобразование между Unicode и заданной кодировкой. Объекты QTextCodec используются в различных контекстах в Qt. Внутренними средствами они применяются для поддержки шрифтов, методов ввода, буфера обмена, технологии «drag-and-drop» и названий файлов. Но мы можем их использовать и непосредственно при написании приложений Qt.
При чтении текстового файла QTextStream автоматически обнаруживает кодировку Unicode, если файл начинается с признака, определяющего порядок байтов. Такой режим работы можно отключить с помощью вызова setAutoDetectUnicode(false). Если нельзя рассчитывать на то, что данные начинаются с признака, определяющего порядок байтов, лучше всего перед чтением вызвать функцию setCodec() с аргументом «UTF-16».
Другой кодировкой, поддерживающей весь Unicode, является UTF-8. Его главное достоинство по сравнению с UTF-16, состоит в том, что он — супермножество по отношению к ASCII. Любой символ с кодом в диапазоне от 0x00 до 0x7F представляется в виде одного байта. Другие символы, включая символы Latin-1, код которых превышает значение 0x7F, представляются в виде последовательности из нескольких байтов. Текст, состоящий в основном из символов ASCII, в формате UTF-8 займет примерно вполовину меньше памяти, чем в формате UTF-16. Для применения UTF-8 с QTextStream перед чтением и записью сделайте вызов setEncoding(QTextStream::UnicodeUTF8).
Если мы всегда собираемся считывать и записывать файлы в кодировке Latin-1, вне зависимости от применяемой пользователем локальной кодировки, мы можем установить кодировку «ISO 8859-1» для потока QTextStream. Например:
QTextStream in(&file);
in.setCodec("ISO 8859-1");
При применении некоторых форматов файлов их кодировка задается в заголовке файла. Заголовок обычно представляется в простом виде в кодировке ASCII, чтобы обеспечить его правильное чтение вне зависимости от используемой кодировки (в предположении, что она является супермножеством по отношению к ASCII). Интересным примером таких форматов являются файлы XML. Обычно файлы XML представлены в кодировке UTF-8 или UTF-16. Для правильного их чтения необходимо вызвать функцию setCodec() с «UTF-8». Если используется формат UTF-16, QTextStream автоматически обнаружит это и настроится на него. Заголовок файла XML иногда содержит аргумент encoding, например:
Поскольку QTextStream не позволяет менять кодировку после начала чтения, чтобы учесть явно заданную кодировку, придется заново прочитать файл, задавая правильное преобразование (полученное функцией QTextCodec::codecForName()). В случае файла XML мы можем сами не делать преобразование кодировок, воспользовавшись классами Qt, предназначенными для XML и описанными в .
Другое применение объектов QTextCodec заключается в указании кодировки строк в исходном коде. Давайте рассмотрим пример, когда группа японских программистов создает приложение, предназначенное главным образом для применения на японском рынке. Эти программисты, вероятно, будут писать свой исходный программный код в текстовом редакторе, использующем такие кодировки, как EUC-JP или Shift-JIS. Такой редактор позволяет им вводить японские иероглифы непосредственно, и, например, они смогут написать следующий код:
QPushButton *button = new QPushButton(tr("♦♦"));
По умолчанию Qt считает, что аргументы функции tr() задаются в кодировке Latin-1. Для изменения этого необходимо вызвать статическую функцию QTextCodec::setCodecForTr(). Например:
QTextCodec *japaneseCodec = QTextCodec::codecForName("EUC-JP");
QTextCodec::setCodecForTr(japaneseCodec);
Это должно быть сделано до первого вызова tr(). Обычно мы делаем это в функции main() непосредственно после создания объекта QApplication.
Другие используемые в программе строки будут по-прежнему интерпретироваться как строки, представленные в кодировке Latin-1. Если программисты хотят вводить японские иероглифы и здесь, они могут явно преобразовывать их в Unicode, используя объект QTextCodec:
QString text = japaneseCodec->toUnicode("♦♦♦♦♦");
Можно поступить по-другому и указать Qt на необходимость применения особого преобразования между типами const char * и QString путем вызова функции QTextCodec::setCodecForCStrings():
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("EUC-JP"));
Описанные выше методы можно применять к любому языку, алфавит которого выходит за рамки кодировки Latin-1, включая языки китайский, греческий, корейский и русский.
Ниже приводится список кодировок, поддерживаемых Qt 4:
• Apple Roman
• Big5
• Big5-HKSCS
• EUC-JP
• EUC-KR
• GB18030-0
• IBM 850
• IBM 866
• IBM 874
• ISO 2022-JP
• ISO 8859-1
• ISO 8859-2
• ISO 8859-3
• ISO 8859-4
• ISO 8859-5
• ISO 8859-6
• ISO 8859-7
• ISO 8859-8
• ISO 8859-9
• ISO 8859-10
• ISO 8859-13
• ISO 8859-14
• ISO 8859-15
• ISO 8859-16
• Iscii-Bng
• Iscii-Dev
• Iscii-Gjr
• Iscii-Knd
• Iscii-Mlm
• Iscii-Ori
• Iscii-Pnj
• Iscii-Tlg
• Iscii-Tml
• JIS X 0201
• JIS X 0208
• KOI8-R
• KOI8-U
• MuleLao-1
• ROMAN8
• Shift-JIS
• TIS-620
• TSCII
• UTF-8
• UTF-16
• UTF-16BE
• UTF-16LE
• Windows-1250
• Windows-1251
• Windows-1252
• Windows-1253
• Windows-1254
• Windows-1255
• Windows-1256
• Windows-1257
• Windows-1258
• WINSAMI2
Для всех этих кодировок функция QTextCodec::codecForName() всегда будет возвращать достоверный указатель. Другие кодировки можно обеспечить путем создания подкласса QTextCodec.
Создание переводимого интерфейса приложения
Если мы хотим иметь многоязыковую версию нашего приложения, мы должны сделать две вещи:
• убедиться, что все строки, которые видит пользователь, проходят через функцию tr();
• загрузить файл перевода (.qm) при запуске приложения.
Ничего подобного не надо делать, если приложения никогда не будут переводиться на другой язык. Однако применение функции tr() почти не требует дополнительных усилий и оставляет дверь открытой для их перевода когда-нибудь в будущем.
Функция tr() является статической функцией, определенной в классе QObject и переопределяемой в каждом подклассе, в котором встречается макрос Q_OBJECT. При ее использовании в рамках подкласса QObject мы можем вызывать tr() без ограничений. Вызов tr() возвращает перевод строки, если он имеется, и первоначальный текст в противном случае.
Для подготовки файлов переводов мы должны запустить утилиту Qt lupdate. Эта утилита собирает все строковые константы, которые встречаются в вызовах tr(), и формирует файлы переводов, содержащие все эти подготовленные к переводу строки. Эти файлы могут затем быть переданы переводчику для добавления к ним перевода строк. Эта процедура рассматривается позже в данной главе в разделе «Перевод приложений».
В общем виде вызов tr() имеет следующий синтаксис:
Контекст::tr(исходныйТекст, комментарий)
Здесь Контекст — имя подкласса QObject, в котором используется макрос Q_OBJECT. Нам не требуется его указывать, если мы вызываем tr() в функции—члене рассматриваемого класса. Аргумент исходныйТекст — текстовая константа, которую нужно будет переводить. Аргумент комментарий является необязательным, и он может использоваться для предоставления переводчику дополнительной информации.
Ниже приводится несколько примеров:
01 RockyWidget::RockyWidget(QWidget *parent)
02 : QWidget(parent)
03 {
04 QString str1 = tr("Letter");
05 QString str2 = RockyWidget::tr("Letter");
06 QString str3 = SnazzyDialog::tr("Letter");
07 QString str4 = SnazzyDialog::tr("Letter", "US paper size");
08 }
Первые два вызова tr() выполняются в контексте объекта RockyWidget (скалистый виджет), а вторые два — в контексте объекта SnazzyDialog (притягательное диалоговое окно). В качестве исходного текста во всех четырех случаях используется слово «Letter» (буква). Последний вызов имеет также комментарий, помогающий переводчику точнее понять смысл исходного текста.
Строки в различных контекстах (классах) переводятся независимо друг от друга. Переводчики, как правило, одновременно работают только с одним контекстом, причем часто при этом работает приложение и на экране отображается виджет или диалоговое окно, которые необходимо перевести.
Когда мы вызываем tr() из глобальной функции, мы должны явно указать контекст. Любой подкласс QObject может использоваться в приложении в качестве контекста. Если такого подкласса нет, мы всегда можем использовать сам класс QObject. Например:
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 QPushButton button(QObject::tr("Hello Qt!"));
05 button.show();
06 return app.exec();
07 }
До сих пор во всех примерах контекст задавался именем класса. Это удобно, поскольку мы почти всегда можем опустить его, но на самом деле это не так. Наиболее общий способ перевода строки в Qt заключается в использовании функции QApplication::translate(), которая принимает три аргумента: контекст, исходный текст и необязательный комментарий. Например, ниже приводится другой способ перевода «Hello Qt!»:
QApplication::translate("Global Stuff", "Hello Qt!");
На этот раз мы поместили текст в контекст «Global Stuff» (глобальное вещество — ну нихрена себе перевод :) ).
Функции tr() и translate() играют двоякую роль: они являются маркерами, которые утилита lupdate использует для поиска видимых пользователем строк, и одновременно они являются функциями С++, которые переводят текст. Это отражается на том, как следует записывать программный код. Например, следующий программный код не сработает:
// НЕПРАВИЛЬНО
const char *appName = "OpenDrawer 2D";
QString translated = tr(appName);
Проблема состоит в том, что утилита lupdate не сможет извлечь строковую константу «OpenDrawer 2D», поскольку она не входит в вызов функции tr(). Это означает, что переводчик не будет иметь возможность перевести эту строку. Эта проблема часто возникает и при построении динамических строк:
// НЕПРАВИЛЬНО
statusBar()->showMessage(tr("Host " + hostName + " found"));
Здесь значение строки, которую мы передаем функции tr(), меняется в зависимости от значения hostName, и поэтому мы не можем ожидать, что перевод функцией tr() будет выполнен правильно.
Решение заключается в применении функции QString::arg():
statusBar()->showMessage(tr("Host %1 found").arg(hostName));
Обратите внимание на то, как это работает: строковый литерал «Host %1 found» (хост %1 найден) передается функции tr(). Если загружен файл перевода на французский язык, tr() возвратит что-то подобное «Нфtе %1 trouvй». Параметр «%1» замещается на содержимое переменной hostName.
Хотя в целом не рекомендуется вызывать tr() для переменной, это может сработать. Мы должны использовать макрос QT_TR_NOOP() для пометки тех строковых литералов, перевод которых должен быть выполнен до их присваивания переменной. Это лучше всего делать для статических массивов строк. Например:
01 void OrderForm::init()
02 {
03 static const char * const flowers[] = {
04 QT_TR_NOOP("Medium Stem Pink Roses"),
05 QT_TR_NOOP("One Dozen Boxed Roses"),
06 QT_TR_NOOP("Calypso Orchid"),
07 QT_TR_NOOP("Dried Red Rose Bouquet"),
08 QT_TR_NOOP("Mixed Peonies Bouquet"),
09 0
10 };
11 for (int i = 0; flowers[i]; ++i)
12 comboBox->addItem(tr(flowers[i]));
13 }
Макрос QT_TR_NOOP() просто возвращает свой аргумент. Но утилита lupdate обнаружит все строки, заданные в виде аргумента макроса QT_TR_NOOP(), и поэтому они смогут быть переведены. При использовании позже этой переменной мы вызываем, как обычно, tr() для выполнения перевода. Несмотря на передачу функции tr() переменной, перевод все-таки будет выполнен.
Существует также макрос QT_TRANSLATE_NOOP(), который работает подобно макросу QT_TR_NOOP(), но для него, кроме того, задается контекст. Этот макрос удобно использовать для инициализации переменных вне класса:
static const char * const flowers[] = {
QT_TRANSLATE_NOOP("OrderForm", "Medium Stem Pink Roses"),
QT_TRANSLATE_NOOP("OrderForm", "One Dozen Boxed Roses"),
QT_TRANSLATE_NOOP("OrderForm", "Calypso Orchid"),
QT_TRANSLATE_NOOP("OrderForm", "Dried Red Rose Bouquet"),
QT_TRANSLATE_NOOP("OrderForm", "Mixed Peonies Bouquet"),
0
};
Здесь аргумент контекста должен совпадать с контекстом при будущем вызове функции tr() или translate().
Когда мы начинаем использовать в приложении функцию tr(), легко можно забыть в каких-то случаях о необходимости задавать видимые пользователем строки через вызов функции tr() (особенно если это делается впервые). Эти пропущенные строки фактически могут быть обнаружены переводчиком или, еще хуже, пользователями переведенного приложения, когда некоторые строки будут отображаться с применением первоначального языка. Чтобы не допустить этого, мы можем указать Qt на необходимость запрета неявных преобразований с типа const char * на тип QString. Это делается путем определения препроцессорного символа QT_NO_CAST_FROM_ASCII перед включением любого заголовочного файла Qt. Наиболее простой способ обеспечения установки этого символа состоит в добавлении следующей строки в файл .pro:
DEFINES += QT_NO_CAST_FROM_ASCII
Это заставит нас каждый строковый литерал использовать через вызов tr() или QLatin1String() в зависимости от того, надо ли его переводить или нет. Строки, которые не будут заданы именно таким образом, приведут к выводу сообщения об ошибке компилятора и заставят нас восполнить пропущенные вызовы функций tr() или QLatin1String().
После заключения всех видимых пользователем строк в вызовы функций tr() для обеспечения перевода нам остается только загрузить файл перевода. Обычно мы это делаем в функции приложения main(). Например, ниже показано, как можно попытаться загрузить файл перевода, который зависит от пользовательской локализации приложения:
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 QTranslator appTranslator;
05 appTranslator.load("myapp_" + QLocale::system().name(),
06 qApp->applicationDirPath());
07 app.installTranslator(&appTranslator);
08 …
09 return app.exec();
10 }
Функция QLocale::system() возвращает объект QLocale, который содержит информацию о пользовательской локализации. Обычно имя локализации является частью имени файла .qm. Локализации можно задавать более или менее точно; например, fr задает европейский французский язык, fr_CA задает канадский французский язык, a fr_CA.ISO8859-15 задает канадский французский язык с использованием кодировки ISO 8859-15 (которая поддерживает символы «^», «КЬ», «№» и «Ы» — в исходном бумажном издании французский куда-то подевался %) ).
Если локализацией является fr_CA.ISO8859-15, функция QTranslator::load() сначала попытается загрузить файл myapp_fr_CA.ISO8859-15.qm. Если этого файла нет, функция load() на следующем шаге попытается загрузить файл myapp_fr_CA.qm, затем myapp_fr.qm и, наконец, myapp.qm, и это будет последней попыткой. В обычных случаях нам необходимо предоставить только файл myapp_fr.qm, содержащий перевод на стандартный французский язык, но если нам нужен другой файл перевода для говорящих на французском в Канаде, мы можем также обеспечить файл myapp_fr_CA.qm, и он будет использован для локализации fr_CA.
Второй аргумент функции QTranslator::load() является каталогом, где функция load() будет искать файл перевода. В данном случае мы предполагаем, что файлы переводов размещаются в том же каталоге, где находится исполняемый модуль.
В самих библиотеках Qt содержится несколько строк, которые необходимо перевести. Компания «Trolltech» располагает переводы на французский, немецкий и упрощенный китайский языки в каталоге Qt translations. Имеются переводы также на другие языки, но они выполнены пользователями Qt и официально не поддерживаются. Необходимо также загрузить файл перевода библиотек Qt:
QTranslator qtTranslator;
qtTranslator.load("qt_" + QLocale::system().name(),
qApp->applicationDirPath());
app.installTranslator(&qtTranslator);
Объект QTranslator может работать одновременно только с одним файлом перевода, и поэтому мы используем отдельный QTranslator для перевода приложения Qt. Возможность применения только одного файла для перевода не составляет проблемы, поскольку мы можем установить любое необходимое нам их количество. QApplication будет рассматривать все такие файлы при поиске перевода.
Некоторые языки, такие как арабский и иврит, используют запись справа налево, а не слева направо. Для таких языков общая компоновка приложения должна быть изменена на зеркальную, что делается при помощи вызова функции QApplication::setLayoutDirection(Qt::RightToLeft). Файлы перевода Qt содержат специальный маркер типа «LTR», указывающий Qt на направление записи используемого языка — слева направо или справа налево, и поэтому нам обычно не приходится самим вызывать функцию setLayoutDirection().
Для наших пользователей может быть более удобно, если файлы перевода будут встраиваться в исполняемый модуль приложения, используя ресурсную систему Qt. Это не только снижает количество файлов в дистрибутиве приложения, но при этом снижается риск случайной потери или удаления файлов переводов.
Предположим, что файлы .qm располагаются в подкаталоге translations исходного дерева, тогда файл myapp.qrc будет содержать следующие строки:
Файл .pro будет иметь следующий элемент:
RESOURCES = myapp.qrc
Наконец, в функции main() мы должны указать :/translations в качестве пути к файлам переводов. Начальное двоеточие говорит о том, что это путь к ресурсу, а не к файлу, размещенному в файловой системе.
Теперь нами рассмотрено все, что необходимо для обеспечения перевода приложения на другие языки. Но язык и направление записи не единственное, что отличает различные страны и культуры. Интернационализация программы должна также учитывать местные форматы дат и времени, денежных единиц, чисел и упорядоченность букв. Qt содержит класс QLocale, обеспечивающий локализованные форматы чисел и даты/времени. Для получения другой информации, характерной для данной местности, мы можем использовать стандартные функции С++ setlocale() и localeconv().
Поведение некоторых классов и функций Qt зависит от локализации:
• сравнение, которое осуществляет функция QString::localeAwareCompare(), зависит от локализации. Этой функцией удобно пользоваться для упорядочивания элементов, которые видит пользователь;
• функция toString() для объектов QDate, QTime и QDateTime возвращает строку в локализованном формате, если вызывается с аргументом Qt::LocalDate;
• по умолчанию виджеты QDateEdit и QDateTimeEdit представляют даты в локализованном формате.
Наконец, в переведенном приложении может потребоваться применение пиктограмм, отличных от используемых в оригинальной версии приложения. Например, стрелки влево и вправо на кнопках Back и Forward (назад и вперед) веб-браузера необходимо поменять местами для языка с записью справа налево. Мы можем это сделать следующим образом:
if (QApplication::isRightToLeft())
{
backAction->setIcon(forwardIcon);
forwardAction->setIcon(backIcon);
} else {
backAction->setIcon(backIcon);
forwardAction->setIcon(forwardIcon);
}
Обычно приходится переводить пиктограммы, содержащие буквы алфавита. Например, буква «I» на кнопке панели инструментов, отображающая опцию Italic (курсив) текстового процессора, должна быть заменена буквой «С» для испанского языка (Cursivo) и буквой «К» для языков датского, голландского, немецкого, норвежского и шведского (Kursiv). Ниже показано, как это можно просто сделать:
if (tr("Italic")[0] == 'C') {
italicAction->setIcon(iconC);
} else if (tr("Italic")[0] == 'K') {
italicAction->setIcon(iconK);
} else {
italicAction->setIcon(iconI);
}
Можно поступить по-другому и использовать средства ресурсной системы, обеспечивающие поддержку нескольких локализаций. В файле .qrc мы можем определять локализацию для ресурса, используя атрибут lang. Например:
Если пользовательской локализацией является es (Espanol), :/italic.png становится ссылкой на изображение cursivo.png. Если пользовательской локализацией является sv (Svenska), используется изображение kursiv.png. Для других локализаций используется italic.png.
Динамическое переключение языков
Для большинства приложений вполне удовлетворительный результат обеспечивают определение предпочитаемого пользователем языка в функции main() и загрузка там соответствующих файлов .qm. Но в некоторых ситуациях пользователям необходимо иметь возможность динамического переключения языка. Если приложение постоянно используется попеременно различными пользователями, может возникнуть необходимость в изменении языка без перезапуска приложения. Например, это часто требуется для приложений, применяемых операторами центров заказов, синхронными переводчиками и операторами компьютеризованных кассовых аппаратов.
Обеспечение в приложении возможности динамического переключения языков требует немного большего, чем просто загрузка одного файла перевода при запуске приложения, но это нетрудно сделать.
Порядок действий должен быть следующим:
• предусмотрите средство, с помощью которого пользователь сможет переключаться с одного языка на другой;
• для каждого виджета или диалогового окна укажите все требующие перевода строки в отдельной функции (эта функция часто называется retranslateUi()), и вызывайте эту функцию всякий раз при изменении языка.
Давайте рассмотрим соответствующую часть исходного кода приложения «call center» (центр заказов). Приложение содержит меню Language (язык), чтобы пользователь имел возможность задавать язык во время работы приложения. По умолчанию применяется английский язык.
Рис. 17.1. Динамическое меню Language.
Поскольку мы не знаем, какой язык захочет использовать пользователь после запуска приложения, мы теперь не будем загружать файлы перевода в функции main(). Вместо этого мы будем их загружать динамически по мере необходимости, и поэтому обеспечивающий перевод программный код должен располагаться в классах главного и диалоговых окон.
Давайте рассмотрим подкласс QMainWindow этого приложения:
01 MainWindow::MainWindow()
02 {
03 journalView = new JournalView;
04 setCentralWidget(journalView);
05 qApp->installTranslator(&appTranslator);
06 qApp->installTranslator(&qtTranslator);
07 qmPath = qApp->applicationDirPath() + "/translations";
08 createActions();
09 createMenus();
10 retranslateUi();
11 }
В конструкторе мы устанавливает центральный виджет JournalView как подкласс QTableWidget. Затем мы настраиваем несколько закрытых переменных—членов, имеющих отношение к переводу:
• переменная appTranslator является объектом QTranslator, который используется для хранения текущего перевода приложения;
• переменная qtTranslator является объектом QTranslator, который используется для хранения перевода библиотеки Qt;
• переменная qmPath имеет тип QString и задает путь к каталогу, который содержит файлы перевода приложения.
В конце мы вызываем закрытые функции createActions() и createMenus() для создания системы меню и также закрытую функцию retranslateUi() для первой установки значений видимых пользователем строк.
01 void MainWindow::createActions()
02 {
03 newAction = new QAction(this);
04 connect(newAction, SIGNAL(triggered()), this, SLOT(newFile()));
05 …
06 aboutQtAction = new QAction(this);
07 connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
08 }
Функция createActions() создает объекты QAction как обычно, но без установки текстов пунктов меню и клавиш быстрого вызова команд. Это будет сделано в функции retranslateUi().
01 void MainWindow::createMenus()
02 {
03 fileMenu = new QMenu(this);
04 fileMenu->addAction(newAction);
05 fileMenu->addAction(openAction);
06 fileMenu->addAction(saveAction);
07 fileMenu->addAction(exitAction);
08 …
09 createLanguageMenu();
10 helpMenu = new QMenu(this);
11 helpMenu->addAction(aboutAction);
12 helpMenu->addAction(aboutQtAction);
13 menuBar()->addMenu(fileMenu);
14 menuBar()->addMenu(editMenu);
15 menuBar()->addMenu(reportsMenu);
16 menuBar()->addMenu(languageMenu);
17 menuBar()->addMenu(helpMenu);
18 }
Функция createMenus() создает пункты меню, но не устанавливает их текст. И снова это будет сделано в функции retranslateUi().
В середине функции мы вызываем createLanguageMenu() для заполнения меню Language списком поддерживаемых языков. Вскоре мы рассмотрим ее исходный код. Во-первых, давайте рассмотрим функцию retranslateUi():
01 void MainWindow::retranslateUi()
02 {
03 newAction->setText(tr("&New"));
04 newAction->setShortcut(tr("Ctrl+N"));
05 newAction->setStatusTip(tr("Create a new journal"));
06 …
07 aboutQtAction->setText(tr("About &Qt"));
08 aboutQtAction->setStatusTip(tr("Show the Qt library's About box"));
09 fileMenu->setTitle(tr("&File"));
10 editMenu->setTitle(tr("&Edit"));
11 reportsMenu->setTitle(tr("&Reports"));
12 languageMenu->setTitle(tr("&Language"));
13 helpMenu->setTitle(tr("&Help"));
14 setWindowTitle(tr("Call Center"));
15 }
Именно в функции retranslateUi() выполняются все вызовы tr() для класса MainWindow. Она вызывается в конце конструктора MainWindow и также при каждом изменении пользователем языка приложения при помощи меню Language.
Мы устанавливаем для каждого пункта меню QAction его текст, клавишу быстрого вызова команды и комментарий в строке состояния. Мы также задаем заголовок окну и каждому меню QMenu.
Рассмотренная ранее функция createMenus() вызывала функцию createLanguageMenu() для заполнения меню Language списком языков:
01 void MainWindow::createLanguageMenu()
02 {
03 languageMenu = new QMenu(this);
04 languageActionGroup = new QActionGroup(this);
05 connect(languageActionGroup, SIGNAL(triggered(QAction *)),
06 this, SLOT(switchLanguage(QAction *)));
07 QDir dir(qmPath);
08 QStringList fileNames = dir.entryList(QStringList("callcenter_*.qm"));
09 for (int i = 0; i < fileNames.size(); ++i) {
10 QString locale = fileNames[i];
11 locale.remove(0, locale.indexOf('_') + 1);
12 locale.truncate(locale.lastIndexOf('.'));
13 QTranslator translator;
14 translator.load(fileNames[i], qmPath);
15 QString language = translator.translate("MainWindow",
16 "English");
17 QAction *action = new QAction(tr("&%1 %2")
18 .arg(i + 1).arg(language), this);
19 action->setCheckable(true);
20 action->setData(locale);
21 languageMenu->addAction(action);
22 languageActionGroup->addAction(action);
23 if (language == "English")
24 action->setChecked(true);
25 }
26 }
Вместо жесткого кодирования поддерживаемых приложением языков мы создаем один пункт меню для каждого файла .qm, расположенного в каталоге приложения translations. Для простоты мы предполагаем, что для английского языка также имеется файл .qm. Можно поступить по-другому и вызывать функцию clear() для объектов QTranslator, когда пользователь выбирает английский язык.
Определенную трудность составляет представление удобных названий языкам файлами .qm. Просто использование сокращений «en» для английского языка или «de» для немецкого языка, основанное на названии файла .qm, выглядит не лучшим образом и может запутать некоторых пользователей. Решение, которое используется функцией createLanguageMenu(), состоит в «переводе» строки «English» (английский язык) в контексте «MainWindow». Эта строка должна принимать значение «Deutsch» при переводе на немецкий язык, «Francais» при переводе на французский язык и «♦♦♦» при переводе на японский язык.
Мы создаем по одному помечаемому пункту меню QAction на каждый язык и храним локальное имя в его элементе данных. Мы добавляем их в объект QActionGroup, чтобы всегда мог быть помечен только один пункт меню Language. Когда пользователь выбирает какую-то команду из группы, объект QActionGroup генерирует сигнал triggered(QAction *), который связан с switchLanguage().
01 void MainWindow::switchLanguage(QAction *action)
02 {
03 QString locale = action->data().toString();
04 appTranslator.load("callcenter_" + locale, qmPath);
05 qtTranslator.load("qt_" + locale, qmPath);
06 retranslateUi();
07 }
Слот switchLanguage() вызывается, когда пользователь выбирает язык из меню Language. Мы загружаем файлы перевода приложения и библиотеки Qt и затем вызываем функцию retranslateUi() для нового перевода всех строк главного окна.
В системе Windows в качестве альтернативы меню Language можно использовать обработку событий LocaleChange — события этого типа генерируются Qt при обнаружении изменения среды локализации. Событие этого типа существует на всех платформах, поддерживаемых Qt, но фактически они генерируются только в системе Windows, когда пользователь изменяет системные настройки локализации (при задании параметров региона и языка на Панели управления). Для обработки событий LocaleChange мы можем переопределить функцию QWidget::changeEvent() следующим образом:
01 void MainWindow::changeEvent(QEvent *event)
02 {
03 if (event->type() == QEvent::LocaleChange) {
04 appTranslator.load("callcenter_"
05 + QLocale::system().name(), qmPath);
06 qtTranslator.load("qt_" + QLocale::system().name(), qmPath);
07 retranslateUi();
08 }
09 QMainWindow::changeEvent(event);
10 }
Если пользователь переключается на другую локализацию во время выполнения приложения, мы пытаемся загрузить файлы перевода, соответствующие новой локализации, и вызываем функцию retranslateUi() для обновления интерфейса пользователя. Во всех случаях мы передаем событие функции базового класса changeEvent(), поскольку один из наших классов тоже может быть заинтересован в обработке событий LocaleChange или других событий.
На этом мы закончили наш обзор программного кода класса MainWindow. Теперь мы рассмотрим программный код одного из классов—виджетов приложения, а именно класса JournalView, чтобы определить, какие изменения потребуются для обеспечения поддержки им динамического перевода.
01 JournalView::JournalView(QWidget *parent)
02 : QTableWidget(parent)
03 {
04 retranslateUi();
05 }
Класс JournalView является подклассом QTableWidget. В конце конструктора мы вызываем закрытую функцию retranslateUi() для перевода строк виджета. Это напоминает то, что мы делали для класса MainWindow.
01 void JournalView::changeEvent(QEvent *event)
02 {
03 if (event->type() == QEvent::LanguageChange)
04 retranslateUi();
05 QTableWidget::changeEvent(event);
06 }
Мы также переопределяем функцию changeEvent() для вызова retranslateUi() при генерации событий LanguageChange. Qt генерирует событие LanguageChange при изменении содержимого объекта QTranslator, который в данный момент используется в QApplication. В нашем приложении это происходит, когда мы вызываем load() для appTranslator или qtTranslator либо из функции MainWindow::switchToLanguage(), либо из функции MainWindow::changeEvent().
События LanguageChange не следует путать с событиями LocaleChange. Событие LocaleChange генерируется системой и говорит приложению: «Возможно, следует загрузить новый файл перевода». В отличие от него, событие LanguageChange генерируется Qt и говорит виджетам приложения: «Возможно, следует заново выполнить перевод всех ваших строк».
При реализации нами класса MainWindow не нужно было реагировать на событие LanguageChange. Вместо этого мы просто всякий раз вызывали функцию retranslateUi() при вызове load() для QTranslator.
01 void JournalView::retranslateUi()
02 {
03 QStringList labels;
04 labels << tr("Time") << tr("Priority") << tr("Phone Number")
05 << tr("Subject");
06 setHorizontalHeaderLabels(labels);
07 }
Функция retranslateUi() заново создает заголовки, используя новый переведенный текст, и этим мы завершаем рассмотрение программного кода, относящегося к переводу созданного вручную виджета. Для виджетов и диалоговых окон, которые разрабатываются при помощи Qt Designer, утилита uic автоматически генерирует функцию, подобную retranslateUi(), которая автоматически вызывается в ответ на события LanguageChange.
Перевод приложений
Перевод приложения Qt, которое содержит вызовы tr(), состоит из трех этапов:
1. Выполнение утилиты lupdate для извлечения из исходного кода приложения всех видимых пользователем строк.
2. Перевод приложения при помощи Qt Linguist.
3. Выполнение утилиты lrelease для получения двоичных файлов .qm, которые приложение может загружать при помощи объекта QTranslator.
Этапы 1 и 3 выполняются разработчиками приложения. Этап 2 выполняется переводчиками. Эта последовательность действий может выполняться любое количество раз в ходе разработки приложения и на протяжении всего его жизненного цикла.
В качестве примера мы продемонстрируем перевод приложения Электронная таблица из . Приложение уже содержит вызовы tr() для всех видимых пользователем строк.
Во-первых, мы должны немного модифицировать файл приложения .pro, указав языки, которые мы собираемся поддерживать. Например, если бы мы хотели поддерживать кроме английского также немецкий и французский, мы бы добавили следующий элемент TRANSLATIONS в файл spreadsheet.pro:
TRANSLATIONS = spreadsheet_de.ts
\ spreadsheet_fr.ts
Здесь мы указали два файла переводов: один для немецкого языка и второй для французского языка.
Эти файлы будут созданы при первом выполнении утилиты lupdate, и затем они будут обновляться при каждом последующем выполнении lupdate.
Эти файлы обычно имеют расширение .ts. Они имеют простой формат XML и не столь компактны, как двоичные файлы .qm, которые «понимают» объекты типа QTranslator. В задачу утилиты lrelease входит преобразование предназначенных для людей файлов .ts в эффективное машинное представление в виде файлов .qm. Между прочим, сокращение .ts означает файл «translation source» (файл с исходным текстом перевода), а .qm — файл «Qt message» (файл сообщений Qt).
Предположим, что мы находимся в каталоге, который содержит исходный код приложения Электронная таблица, и тогда мы можем выполнить утилиту lupdate для spreadsheet.pro, задавая в командной строке следующую команду:
lupdate -verbose spreadsheet.pro
Опция —verbose указывает утилите lupdate на необходимость более интенсивной обратной связи, чем та, которая обеспечивается при нормальном режиме работы. Ниже приводятся сообщения, получения которых следует ожидать в результате работы утилиты:
Updating 'spreadsheet_de.ts'...
Found 98 source texts (98 new and 0 already existing)
Updating 'spreadsheet_fr.ts'...
Found 98 source texts (98 new and 0 already existing)
Все строки, которые задаются в вызовах функции tr() в исходном коде приложения, хранятся в файлах .ts (в том числе и псевдоперевод). Сюда также включаются строки из файлов приложения .ui.
По умолчанию утилита lupdate предполагает, что передаваемые функции tr() строки используют кодировку Latin-1. Если это не так, мы должны добавить элемент CODECFORTR в файл .pro. Например:
CODECFORTR = EUC-JP
Это должно быть сделано в дополнение к вызову QTextCodec::setCodecForTr() из функции приложения main().
Затем в файлы spreadsheet_de.ts и spreadsheet_fr.ts необходимо добавить перевод, выполненный при помощи Qt Linguist.
Для запуска Qt Linguist выберите пункт Qt by Trolltech v4.x.y | Linguist в меню Start в системе Windows, введите linguist в командной строке в системе Unix или дважды щелкните по Linguist в системе Mac OS X Finder. Для добавления перевода в файл .ts выберите пункт меню File | Open и укажите файл для перевода.
С левой стороны главного окна утилиты Qt Linguist отображается список всех контекстов переводимого на другие языки приложения. Для приложения Электронная таблица этими контекстами являются «FindDialog», «GoToCellDialog», «MainWindow», «SortDialog» и «Spreadsheet». Справа вверху выводится список всех исходных строк для текущего контекста. Каждая исходная строка сопровождается переводом и флажком Done (готово). Справа по центру находится область, где мы можем вводить перевод текущей исходной строки. Справа внизу отображаются подсказки по переводу, которые автоматически генерируются Qt Linguist.
После создания нами файла переводов .ts необходимо его преобразовать в двоичный файл .qm, чтобы он был понятен для QTranslator. Для этого в Qt Linguist выберите пункт меню File | Release. Обычно мы начинаем с перевода только нескольких строк и затем выполняем приложение с применением файла .qm, чтобы убедиться, что все работает правильно.
Рис. 17.2. Qt Linguist в действии.
Если мы хотим заново сгенерировать файлы .qm для всех файлов .ts, мы можем запустить утилиту lrelease из командной строки следующим образом:
lrelease -verbose spreadsheet.pro
Если мы выполняли перевод 19 строк на французский язык и отметили флажком Done 17 из них, утилита lrelease выдаст следующий результат:
Updating 'spreadsheet_de.qm'...
Generated 0 translations (0 finished and 0 unfinished)
Ignored 98 untranslated source texts
Updating 'spreadsheet_fr.qm"...
Generated 19 translations (17 finished and 2 unfinished)
Ignored 79 untranslated source texts
Флажок Done игнорируется утилитой lrelease; он может использоваться переводчиками для идентификации законченных переводов и тех, перевод которых необходимо выполнить заново. Непереведенные строки при выполнении приложения выводятся на языке оригинала.
Когда мы модифицируем исходный код приложения, файлы перевода могут устареть. Решение этой проблемы заключается в повторном выполнении утилиты lupdate, обеспечении перевода новых строк и повторной генерации файлов .qm. Одни группы разработчиков могут посчитать удобным частое выполнение утилиты lupdate, а другие могут захотеть это делать только для почти готового программного продукта.
Утилиты lupdate и Qt Linguist достаточно «умные». Переводы, которые с какого-то момента не стали использоваться, сохраняются в файлах .ts на случай, если они потребуются когда-нибудь в будущем. При обновлении файлов .ts утилита lupdate использует «интеллектуальный» алгоритм слияния, позволяющий переводчикам сэкономить много времени при работе с текстом, который совпадает или подобен в различных контекстах.
Более подробную информацию относительно Qt Linguist, lupdate и lrelease можно найти в руководстве по Qt Linguist в сети Интернет по адресу . Это руководство содержит полное описание интерфейса пользователя для Qt Linguist и учебное пособие для поэтапного обучения программистов.
Глава 18. Многопоточная обработка
Обычные приложения с графическим интерфейсом имеют один поток (thread) выполнения и производят в каждый момент времени одну операцию. Если пользователь через интерфейс пользователя вызывает продолжительную операцию, интерфейс, как правило, «застывает» до завершения операции. В («Обработка событий») даются некоторые способы решения этой проблемы. Применение многопоточной обработки — еще один способ решения данной проблемы.
В многопоточном приложении графический пользовательский интерфейс выполняется в своем собственном потоке, а обработка осуществляется в одном или в нескольких других потоках. В результате такие приложения способны реагировать на действия пользователя даже при продолжительной обработке. Еще одним преимуществом многопоточной обработки является возможность в многопроцессорных системах одновременно выполнять несколько потоков на разных процессорах, увеличивая производительность.
В данной главе мы сначала продемонстрируем способы создания подкласса QThread и способы применения классов QMutex, QSemaphore и QWaitCondition для синхронизации потоков. Затем мы рассмотрим способы взаимодействия вторичных потоков с главным потоком в ходе цикла обработки событий. Наконец, мы завершим главу обзором классов Qt, объясняя, какие из них могут использоваться во вторичных потоках.
Многопоточная обработка представляет собой обширную тему, которой посвящается много книг. В данной главе предполагается, что вам уже известны принципы многопоточного программирования, поэтому основное внимание уделяется методам разработки многопоточных приложений средствами Qt, а не теме потоков выполнения в целом.
Создание потоков
Обеспечить многопоточную обработку в приложении Qt достаточно просто: мы только создаем подкласс QThread и переопределяем его функцию run(). Чтобы показать, как это работает, мы начнем с рассмотрения программного кода очень простого подкласса QThread, который периодически выводит на консоль заданный текст:
01 class Thread : public QThread
02 {
03 Q_OBJECT
04 public:
05 Thread();
06 void setMessage(const QString &message);
07 void stop();
08 protected:
09 void run();
10 private:
11 QString messageStr;
12 volatile bool stopped;
12 };
Класс Thread наследует QThread и переопределяет функцию run(). Он содержит две дополнительные функции: setMessage() и stop().
Переменная stopped объявляется со спецификатором volatile (изменчивый), поскольку доступ к ней осуществляется из разных потоков, и мы хотим быть уверенными, что всегда получаем ее обновленное значение. Если мы опустим ключевое слово volatile, компилятор может оптимизировать доступ к этой переменной, что, возможно, приведет к получению неправильного результата.
01 Thread::Thread()
02 {
03 stopped = false;
04 }
Мы устанавливаем в конструкторе переменную stopped на значение false.
01 void Thread::run()
02 {
03 while (!stopped)
04 cerr << qPrintable(messageStr);
05 stopped = false;
06 cerr << endl;
07 }
Функция run() вызывается для запуска потока. Пока переменная stopped имеет значение false, эта функция будет выводить на консоль заданное сообщение. Работа потока завершается, когда завершается функция run().
01 void Thread::stop()
02 {
03 stopped = true;
04 }
Функция stop() устанавливает переменную stopped на значение true, тем самым указывая функции run() на необходимость прекращения вывода текстовых сообщений на консоль. Данная функция может вызываться из любого потока в любое время. В нашем примере мы предполагаем, что присваивание значения переменной типа bool является атомарной операцией. Такое предположение является разумным, учитывая, что переменная типа bool может иметь только два состояния. Позже мы рассмотрим в данном разделе способы применения класса QMutex, гарантирующего атомарность операции присваивания значения переменной.
Класс QThread содержит функцию terminate(), которая прекращает выполнение потока, если он все еще не завершен. Функцию terminate() не рекомендуется применять, поскольку она может остановить поток в произвольной точке и не позволяет потоку выполнить очистку после себя. Всегда надежнее использовать переменную stopped и функцию stop(), как мы уже делали здесь.
Рис. 18.1. Приложение Threads.
Теперь мы рассмотрим способы применения класса Thread в небольшом приложении Qt, которое применяет два потока, А и В, не считая главный поток.
01 class ThreadDialog : public QDialog
02 {
03 Q_OBJECT
04 public:
05 ThreadDialog(QWidget *parent = 0);
06 protected:
07 void closeEvent(QCloseEvent *event);
08 private slots:
09 void startOrStopThreadA();
10 void startOrStopThreadB();
11 private:
12 Thread threadA;
13 Thread threadB;
14 QPushButton *threadAButton;
15 QPushButton *threadBButton;
16 QPushButton *quitButton;
17 };
В классе ThreadDialog объявляются две переменные типа Thread и несколько кнопок для обеспечения основных средств интерфейса пользователя.
01 ThreadDialog::ThreadDialog(QWidget *parent)
02 : QDialog(parent)
03 {
04 threadA.setMessage("А");
05 threadB.setMessage("B");
06 threadAButton = new QPushButton(tr("Start А"));
07 threadBButton = new QPushButton(tr("Start В"));
08 quitButton = new QPushButton(tr("Quit"));
09 quitButton->setDefault(true);
10 connect(threadAButton, SIGNAL(clicked()),
11 this, SLOT(startOrStopThreadA()));
12 connect(threadBButton, SIGNAL(clicked()),
13 this, SLOT(startOrStopThreadB()));
14 …
15 }
В конструкторе мы вызываем функцию setMessage() для периодического вывода на экран первым потоком буквы «А» и вторым потоком буквы «В».
01 void ThreadDialog::startOrStopThreadA()
02 {
03 if (threadA.isRunning()) {
04 threadA.stop();
05 threadAButton->setText(tr("Start А"));
06 } else {
07 threadA.start();
08 threadAButton->setText(tr("Stop А"));
09 }
10 }
Когда пользователь нажимает кнопку потока А, функция startOrStopThreadA() останавливает поток, если он выполняется, и запускает его в противном случае. Она также обновляет текст кнопки.
01 void ThreadDialog::startOrStopThreadB()
02 {
03 if (threadB.isRunning()) {
04 threadB.stop();
05 threadBButton->setText(tr("Start В"));
06 } else {
07 threadB.start();
08 threadBButton->setText(tr("Stop В"));
09 }
10 }
Программный код функции startOrStopThreadB() очень похож.
01 void ThreadDialog::closeEvent(QCloseEvent *event)
02 {
03 threadA.stop();
04 threadB.stop();
05 threadA.wait();
06 threadB.wait();
07 event->accept();
08 }
Если пользователь выбирает пункт меню Quit или закрывает окно, мы даем команду останова для каждого выполняющегося потока и ожидаем их завершения (используя функцию QThread::wait()) прежде, чем сделать вызов CloseEvent::accept(). Это обеспечивает аккуратный выход из приложения, хотя в данном случае это не имеет значения.
Если при выполнении приложения вы нажмете кнопку Start А, консоль заполнится буквами «А». Если вы нажмете кнопку Start В, консоль заполнится попеременно последовательностями букв «А» и «В». Нажмите кнопку Stop А, и тогда на экран будет выводиться только последовательность букв «В».
Синхронизация потоков
Обычным требованием для многопоточных приложений является синхронизация работы нескольких потоков. Для этого в Qt предусмотрены следующие классы: QMutex, QReadWriteLock, QSemaphore и QWaitCondition.
Класс QMutex обеспечивает такую защиту переменной или участка программного кода, что доступ к ним в каждый момент времени может осуществлять только один поток. Этот класс содержит функцию lock(), которая закрывает мьютекс (mutex). Если мьютекс открыт, текущий поток захватывает его и немедленно закрывает; в противном случае работа текущего потока блокируется до тех пор, пока захвативший мьютекс поток не освободит его. В любом случае после вызова lock() текущий поток будет держать мьютекс до вызова им функции unlock(). Класс QMutex содержит также функцию tryLock(), которая сразу же возвращает управление, если мьютекс уже закрыт.
Предположим, что нам нужно обеспечить защиту переменной stopped класса Thread из предыдущего раздела с помощью QMutex. Тогда мы бы добавили к классу Thread следующую переменную—член:
private:
QMutex mutex;
…
};
Функция run() изменилась бы следующим образом:
01 void Thread::run()
02 {
03 forever {
04 mutex.lock();
05 if (stopped) {
06 stopped = false;
07 mutex.unlock();
08 break;
09 }
10 mutex.unlock();
11 cerr << qPrintable(messageStr.ascii);
12 }
13 cerr << endl;
14 }
Функция stop() стала бы такой:
01 void Thread::stop()
02 {
03 mutex.lock();
04 stopped = true;
05 mutex.unlock();
06 }
Блокировка и разблокировка мьютекса в сложных функциях или там, где обрабатываются исключения С++, может иметь ошибки. Qt предлагает удобный класс QMutexLocker, упрощающий обработку мьютексов. Конструктор QMutexLocker принимает в качестве аргумента объект QMutex и блокирует его. Деструктор QMutexLocker разблокирует мьютекс. Например, мы могли бы приведенные выше функции run() и stop() переписать следующим образом:
01 void Thread::run()
02 {
03 forever {
04 {
05 QMutexLocker locker(&mutex);
06 if (stopped) {
07 stopped = false;
08 break;
09 }
10 }
11 cerr << qPrintable(messageStr);
12 }
13 cerr << endl;
14 }
15 void Thread::stop()
16 {
17 QMutexLocker locker(&mutex);
18 stopped = true;
18 }
Одна из проблем применения мьютексов возникает из-за доступности переменной только для одного потока. В программах со многими потоками, пытающимися одновременно читать одну и ту же переменную (не модифицируя ее), мьютекс может серьезно снижать производительность. В этих случаях мы можем использовать QReadWriteLock — класс синхронизации, допускающий одновременный доступ для чтения без снижения производительности.
В классе Thread не имеет смысла заменять мьютекс QMutex блокировкой QReadWriteLock для защиты переменной stopped, потому что в лучшем случае только один поток может пытаться читать эту переменную в любой момент времени. Более подходящий пример мог бы состоять из одного или нескольких считывающих потоков, получающих доступ к некоторым совместно используемым данным, и одного или нескольких записывающих потоков, модифицирующих данные. Например:
01 MyData data;
02 QReadWriteLock lock;
03 void ReaderThread::run()
04 {
05 …
06 lock.lockForRead();
07 access_data_without_modifying_it(&data);
08 lock.unlock();
09 …
10 }
11 void WriterThread::run()
12 {
13 …
14 lock.lockForWrite();
15 modify_data(&data);
16 lock.unlock();
17 …
18 }
Ради удобства мы можем использовать классы QReadLocker и QWriteLocker для блокировки и разблокировки объекта QReadWriteLock.
Класс QSemaphore — это еще одно обобщение мьютекса, но, в отличие от блокировок чтения/записи, он может использоваться для контроля некоторого количества идентичных ресурсов. Следующие два фрагмента программного кода демонстрируют соответствие между QSemaphore и QMutex:
• QSemaphore semaphore(1) — QMutex mutex,
• Semaphore.acquire() — mutex.lock(),
• Semaphore.release() — mutex.unlock().
Передавая 1 конструктору, мы указываем семафору на то, что он управляет работой одного ресурса. Преимущество применения семафора заключается в том, что мы можем передавать конструктору числа, отличные от 1, и затем вызывать функцию acquire() несколько раз для захвата многих ресурсов.
Типичная область применения семафоров — это передача некоторого количества данных (DataSize) при совместном использовании циклического буфера определенного размера (BufferSize):
const int DataSize = 100000;
const int BufferSize = 4096;
char buffer[BufferSize];
Поток, являющийся поставщиком данных, записывает данные в буфер, пока он не заполнится, и затем повторяет эту процедуру сначала, переписывая существующие данные. Поток, принимающий данные, считывает данные по мере их поступления. Это проиллюстрировано на рис. 18.2 для небольшого 16-байтового буфера.
Рис. 18.2. Модель взаимодействия двух потоков: формирующего и принимающего данные.
Необходимость синхронизации для примера взаимодействия потоков, один из которых формирует данные, а другой их считывает, обусловлена двумя причинами: если формирующий данные поток работает слишком быстро, он станет переписывать данные, которые еще не считал поток—приемник; если поток—приемник считывает данные слишком быстро, он перегонит другой поток и станет считывать «мусор».
Грубый способ решения этой проблемы состоит в том, чтобы сначала заполнить буфер и затем ждать, пока поток—приемник не считает буфер целиком и так далее. Однако в многопроцессорных системах это не позволит обоим потокам работать одновременно с разными частями буфера.
Одни из эффективных способов решения этой проблемы заключается в использовании двух семафоров:
QSemaphore freeSpace(BufferSize);
QSemaphore usedSpace(0);
Семафор freeSpace управляет той частью буфера, которая может заполняться потоком, формирующим данные. Семафор usedSpace управляет той областью, которую может считывать поток—приемник. Эти две области взаимно дополняют друг друга. Семафор freeSpace устанавливается на значение переменной BufferSize (4096), то есть он может захватывать именно такое количество ресурсов. Когда приложение запускается, поток, считывающий данные, начинает захватывать «свободные» байты и превращать их в «используемые» байты. Семафор usedSpace инициализируется нулевым значением, чтобы поток—приемник не мог считать «мусор» при запуске приложения.
В этом примере каждый байт рассматривается как один ресурс. В реальном приложении мы, вероятно, использовали бы более крупные блоки памяти (например, по 64 или 256 байт) для снижения затрат, обусловленных применением семафоров.
01 void Producer::run()
02 {
03 for (int i = 0; i < DataSize; ++i) {
04 freeSpace.acquire();
05 buffer[i % BufferSize] = "ACGT"[uint(rand()) % 4];
06 usedSpace.release();
07 }
08 }
Каждая итерация при работе потока, формирующего данные, начинается с захвата одного «свободного» байта. Если весь буфер заполнен данными, которые не считаны потоком—приемником, вызов функции acquire() заблокирует семафор до тех пор, пока поток—приемник не начнет считывать данные. Захватив байт, мы заполняем его некоторым случайным значением («А», «С», «G» или «T») и затем освобождаем байт и помечаем его как «использованный», тем самым указывая на возможность его считывания потоком—приемником.
01 void Consumer::run()
02 {
03 for (int i = 0; i < DataSize; ++i) {
04 usedSpace.acquire();
05 cerr << buffer[i % BufferSize];
06 freeSpace.release();
07 }
08 cerr << endl;
09 }
Работу потока—приемника мы начинаем с захвата одного «использованного» байта. Если буфер не содержит данных для чтения, вызов функции acquire() заблокирует семафор до тех пор, пока первый поток не сформирует какие-то данные. После захвата нами байта мы выводим его на экран и освобождаем байт, помечая его как «свободный», тем самым позволяя первому потоку вновь присвоить ему некоторое значение.
01 int main()
02 {
03 Producer producer;
04 Consumer consumer;
05 producer.start();
06 consumer.start();
07 producer.wait();
08 consumer.wait();
09 return 0;
10 }
Наконец, в функции main() мы запускаем оба потока. После этого происходит следующее: поток, формирующий данные, преобразует некоторое «свободное» пространство в «использованное», после чего поток—приемник может выполнить его обратное преобразование в «свободное» пространство.
Когда программа выполняется, она выводит на консоль случайную последовательность из 100 000 букв «А», «С», «G» и «T» и затем завершает свою работу. Для того чтобы понять, что происходит на самом деле, мы можем отключить вывод указанной последовательности и вместо этого выводить на консоль букву «P» при генерации каждого байта первым потоком и букву «с» при чтении байта вторым потоком. И ради максимального упрощения ситуации мы можем использовать меньшие значения параметров DataSize и BufferSize.
Например, при выполнении программы, когда DataSize равен 10 и BufferSize равен 4, результат может быть таким: «PcPcPcPcPcPcPcPcPcPc». В данном случае поток—приемник считывает байты сразу по мере их формирования первым потоком; оба потока работают на одной скорости. В другом случае первый поток может заполнять буфер целиком еще до начала его считывания вторым потоком: «PPPPccccPPPPccccPPcc». Существует много других вариантов. Семафоры дают большую свободу действий планировщикам потоков в специфических системах, что позволяет им, изучив поведение потоков, выбрать подходящую политику планирования их работы.
Другой подход к решению проблемы синхронизации работы потока, формирующего данные, и потока, принимающего данные, состоит в применении классов QWaitCondition и QMutex. Класс QWaitCondition позволяет одному потоку «пробуждать» другие потоки, когда удовлетворяется некоторое условие. Этим обеспечивается более точное управление, чем путем применения только одних мьютексов. Чтобы показать, как это работает, мы переделаем пример с двумя потоками, используя условия ожидания.
const int DataSize = 100000;
const int BufferSize = 4096;
char buffer[BufferSize];
QWaitCondition bufferIsNotFull;
QWaitCondition bufferIsNotEmpty;
QMutex mutex;
int usedSpace = 0;
Кроме буфера мы объявляем два объекта QWaitCondition, один объект QMutex и одну переменную для хранения количества «использованных» байтов в буфере.
01 void Producer::run()
02 {
03 for (int i = 0; i < DataSize; ++i) {
04 mutex.lock();
05 while (usedSpace == BufferSize)
06 bufferIsNotFull.wait(&mutex);
07 buffer[i % BufferSize] = "ACGT"[uint(rand()) % 4];
08 ++usedSpace;
09 bufferIsNotEmpty.wakeAll();
10 mutex.unlock();
11 }
12 }
Работу потока, формирующего данные, мы начинаем с проверки заполнения буфера. Если он заполнен, мы ждем возникновения условия «буфер не заполнен». Когда это условие удовлетворяется, мы записываем один байт в буфер, увеличиваем на единицу usedSpace и возобновляем работу любого потока, ожидающего возникновения условия «буфер не пустой».
Мы используем мьютекс для контроля любого доступа к переменной usedSpace. Функция QWaitCondition::wait() может принимать в первом своем аргументе заблокированный мьютекс, который она открывает перед блокировкой текущего потока и затем вновь блокирует его перед выходом.
В этом примере мы могли бы заменить цикл while
while (usedSpace == BufferSize)
bufferIsNotFull.wait(&mutex);
на инструкцию if:
if (usedSpace == BufferSize) {
mutex.unlock();
bufferIsNotFull.wait();
mutex.lock();
}
Однако это не будет правильно работать, как только мы станем использовать несколько потоков, формирующих данные, поскольку другой такой поток может захватить мьютекс сразу же после вызова функции wait() и вновь отменить условие «буфер не заполнен».
01 void Consumer::run()
02 {
03 for (int i = 0; i < DataSize; ++i) {
04 mutex.lock();
05 while (usedSpace == 0)
06 bufferIsNotEmpty.wait(&mutex);
07 cerr << buffer[i % BufferSize];
08 --usedSpace;
09 bufferIsNotFull.wakeAll();
10 mutex.unlock();
11 }
12 cerr << endl;
13 }
Поток—приемник работает в точности наоборот относительно первого потока: он ожидает возникновения условия «буфер не пустой» и возобновляет работу любого потока, ожидающего условия «буфер не заполнен».
Во всех приводимых до сих пор примерах наши потоки имеют доступ к одинаковым глобальным переменным. Но для некоторых многопоточных приложений требуется хранить в глобальных переменных неодинаковые данные для разных потоков. Эти переменные часто называют локальной памятью потока (thread-local storage — TLS) или специальными данными потока (thread-specific data — TSD). Мы можем «схитрить» и использовать отображение, в качестве ключей которого применяются идентификаторы потоков (возвращаемые функцией QThread::currentThread()), но более привлекательное решение состоит в использовании класса QThreadStorage
Обычно класс QThreadStorage
01 QThreadStorage
02 void insertIntoCache(int id, double value)
03 {
04 if (!cache.hasLocalData())
05 cache.setLocalData(new QHash
06 cache.localData()->insert(id, value);
07 }
08 void removeFromCache(int id)
09 {
10 if (cache.hasLocalData())
11 cache.localData()->remove(id);
12 }
Переменная cache содержит указатель на используемое потоком отображение QHash
Кроме кэширования класс QThreadStorage
Взаимодействие с главным потоком
При запуске приложения Qt работает только один поток — главный. Только этот поток может создать объект QApplication или QCoreApplication и вызвать для него функцию exec(). После вызова exec() этот поток либо ожидает возникновения какого-нибудь события, либо обрабатывает какое-нибудь событие.
Главный поток может запускать новые потоки, создавая объекты подкласса QThread, как мы это делали в предыдущем разделе. Если эти новые потоки должны взаимодействовать друг с другом, они могут совместно использовать переменные под управлением мьютексов, блокировок чтения/записи, семафоров или специальных событий. Но ни один из этих методов нельзя использовать для связи с главным потоком, поскольку они будут блокировать цикл обработки событий и «заморозят» интерфейс пользователя.
Для связи вторичного потока с главным потоком необходимо использовать межпоточные соединения сигнал—слот. Обычно механизм сигналов и слотов работает синхронно, т.е. связанный с сигналом слот вызывается сразу после генерации сигнала, используя прямой вызов функции.
Однако когда вы связываете объекты, «живущие» в других потоках, механизм взаимодействия сигналов и слотов становится асинхронным. (Такое поведение можно изменить с помощью пятого параметра функции QObject::connect().) Внутри эти связи реализуются путем регистрации события. Слот затем вызывается в цикле обработки событий потока, в котором находится объект получателя. По умолчанию объект QObject существует в потоке, в котором он был создан; в любой момент можно изменить расположение объекта с помощью вызова функции QObject::moveToThread().
Рис. 18.3. Приложение Image Pro.
Для иллюстрации работы соединений сигнал—слот с разными потоками мы рассмотрим программный код приложения Image Pro — процессора изображений, обеспечивающего базовые возможности и позволяющего пользователю поворачивать, изменять размер и цвет изображения. В данном приложении используется один вторичный поток для выполнения операций над изображениями без блокировки цикла обработки событий. Это имеет существенное значение при обработке изображений очень большого размера. Вторичный поток имеет список выполняемых задач или «транзакций», и он генерирует события для главного окна, чтобы сообщать о том, как идет процесс их выполнения.
01 ImageWindow::ImageWindow()
02 {
03 imageLabel = new QLabel;
04 imageLabel->setBackgroundRole(QPalette::Dark);
05 imageLabel->setAutoFillBackground(true);
06 imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
07 setCentralWidget(imageLabel);
08 createActions();
09 createMenus();
10 statusBar()->showMessage(tr("Ready"), 2000);
11 connect(&thread, SIGNAL(transactionStarted(const QString &)),
12 statusBar(), SLOT(showMessage(const QString &)));
13 connect(&thread, SIGNAL(finished()),
14 this, SLOT(allTransactionsDone()));
15 setCurrentFile("");
16 }
Интересной частью конструктора ImageWindow являются два соединения сигнал—слот. В обоих случаях сигнал генерируется объектом TransactionThread, который мы вскоре рассмотрим.
01 void ImageWindow::flipHorizontally()
02 {
03 addTransaction(new FlipTransaction(Qt::Horizontal));
04 }
Слот flipHorizontally() создает транзакцию зеркального отражения и регистрирует ее при помощи закрытой функции addTransaction(). Функции flipVertically(), resizeImage(), convertTo32Bit(), convertTo8Bit() и convertTo1Bit() реализуются аналогично.
01 void ImageWindow::addTransaction(Transaction *transact)
02 {
03 thread.addTransaction(transact);
04 openAction->setEnabled(false);
05 saveAction->setEnabled(false);
06 saveAsAction->setEnabled(false);
07 }
Функция addTransaction() добавляет транзакцию в очередь транзакций вторичного потока и отключает команды Open, Save и Save As на время обработки транзакций.
01 void ImageWindow::allTransactionsDone()
02 {
03 openAction->setEnabled(true);
04 saveAction->setEnabled(true);
05 saveAsAction->setEnabled(true);
06 imageLabel->setPixmap(QPixmap::fromImage(thread.image()));
07 setWindowModified(true);
08 statusBar()->showMessage(tr("Ready"), 2000);
09 }
Слот allTransactionsDone() вызывается, когда очередь транзакций TransactionThread становится пустой.
Теперь давайте рассмотрим класс TransactionThread:
01 class TransactionThread : public QThread
02 {
03 Q_OBJECT
04 public:
05 void addTransaction(Transaction *transact);
06 void setImage(const QImage &image);
07 QImage image();
08 signals:
09 void transactionStarted(const QString &message);
10 protected:
11 void run();
12 private:
13 QMutex mutex;
14 QImage currentImage;
15 QQueue
16 };
Класс TransactionThread содержит список обрабатываемых транзакций, которые выполняются по очереди в фоновом режиме.
01 void TransactionThread::addTransaction(Transaction *transact)
02 {
03 QMutexLocker locker(&mutex);
04 transactions.enqueue(transact);
05 if (!isRunning())
06 start();
07 }
Функция addTransaction() добавляет транзакцию в очередь транзакций и запускает поток транзакции, если он еще не выполняется. Доступ к переменной—члену transactions защищается мьютексом, потому что главный поток мог бы ее модифицировать функцией addTransaction() во время прохода по транзакциям transactions вторичного потока.
01 void TransactionThread::setImage(const QImage &image)
02 {
03 QMutexLocker locker(&mutex);
04 currentImage = image;
05 }
06 QImage TransactionThread::image()
07 {
08 QMutexLocker locker(&mutex);
09 return currentImage;
10 }
Функции setImage() и image() позволяют главному потоку установить изображение, для которого будут выполняться транзакции, и получить обработанное изображение после завершения всех транзакций. И вновь мы защищаем доступ к переменной—члену при помощи мьютекса.
01 void TransactionThread::run()
02 {
03 Transaction *transact;
04 forever {
05 mutex.lock();
06 if (transactions.isEmpty()) {
07 mutex.unlock();
08 break;
09 }
10 QImage oldImage = currentImage;
11 transact = transactions.dequeue();
12 mutex.unlock();
13 emit transactionStarted(transact->message());
14 QImage newImage = transact->apply(oldImage);
15 delete transact;
16 mutex.lock();
17 currentImage = newImage;
18 mutex.unlock();
19 }
20 }
Функция run() просматривает очередь транзакций и по очереди выполняет все транзакции путем вызова для них функции apply().
После старта транзакции мы генерируем сигнал transactionStarted() с сообщением, выводимым в строке состояния приложения. Когда обработка всех транзакций завершается, функция run() возвращает управление и QThread генерирует сигнал finished().
01 class Transaction
02 {
03 public:
04 virtual ~Transaction() { }
05 virtual QImage apply(const QImage &image) = 0;
06 virtual QString message() = 0;
07 };
Класс Transaction является абстрактным базовым классом, предназначенным для определения операций, которые пользователь может выполнять с изображением. Виртуальный деструктор необходим, потому что нам приходится удалять экземпляры подклассов Transaction через указатель transaction. (Кроме того, если мы его не предусмотрим, некоторые компиляторы выдадут предупреждение.) Transaction имеет три конкретных подкласса: FlipTransaction, ResizeTransaction и ConvertDepthTransaction. Нами будет рассмотрен только подкласс FlipTransaction; другие два подкласса имеют аналогичное определение.
01 class FlipTransaction : public Transaction
02 {
03 public:
04 FlipTransaction(Qt::Orientation orientation);
05 QImage apply(const QImage &image);
06 QString message();
07 private:
08 Qt::Orientation orientation;
09 };
Конструктор FlipTransaction принимает один параметр, который задает ориентацию зеркального отражения (по горизонтали или по вертикали).
01 QImage FlipTransaction::apply(const QImage &image)
02 {
03 return image.mirrored(
04 orientation == Qt::Horizontal, orientation == Qt::Vertical);
05 }
Функция apply() вызывает QImage::mirrored() для объекта QImage, полученного в виде параметра, и возвращает сформированный объект QImage.
01 QString FlipTransaction::message()
02 {
03 if (orientation == Qt::Horizontal) {
04 return QObject::tr("Flipping image horizontally...");
05 } else {
06 return QObject::tr("Flipping image vertically...");
07 }
08 }
Функция messageStr() возвращает сообщение, отображаемое в строке состояния в ходе выполнения операции. Данная функция вызывается из функции transactionThread::run(), кoгдa гeнepиpyeтcя cигнaл transactionStarted().
Применение классов Qt во вторичных потоках
Функция называется потокозащищенной (thread—safe), если она может спокойно вызываться одновременно из нескольких потоков. Если две такие функции вызываются из различных потоков и совместно используют одинаковые данные, результат всегда будет вполне определенным. Это определение можно расширить на класс, и тогда класс будет называться потокозащищенным, если все его функции могут вызываться одновременно из различных потоков, не мешая работе друг друга, если они даже работают с одним и тем же объектом.
В Qt потокозащищенными являются классы QMutex, QMutexLocker, QReadWriteLock, QReadLocker, QWriteLocker, QSemaphore, QThreadStorage
Большинство классов Qt неграфического интерфейса удовлетворяют менее строгому ограничению: они являются реентерабельными (reentrant). Класс называется реентерабельным, если разные его экземпляры могут одновременно использоваться разными потоками. Однако одновременный доступ к одному реентерабельному объекту при многопоточной обработке недостаточно надежен и должен контролироваться при помощи мьютекса. Реентерабельность классов отмечается в справочной документации Qt. Обычно любой класс С++, который не использует глобальные переменные (или, другими словами, совместно используемые данные), является реентерабельным.
Класс QObject — реентерабельный, однако не следует забывать о трех ограничениях:
• Дочерние объекты QObject должны создаваться их родительским потоком. В частности, это означает, что созданные во вторичном потоке объекты нельзя создавать с указанием в качестве родительского объекта QThread, потому что этот объект был создан в другом потоке (либо в главном потоке, либо в другом вторичном потоке).
• Все объекты QObject, созданные во вторичном потоке, должны быть удалены до удаления соответствующего объекта QThread. Это можно обеспечить путем создания объектов в стеке функцией QThread::run().
• Объекты QObject должны удаляться в том потоке, в котором они были созданы. Если требуется удалить объект QObject, существующий в другом потоке, мы должны вызвать потокозащищенную функцию QObject::deleteLater(), которая регистрирует событие «отсроченное удаление».
Такие подклассы QObject неграфического интерфейса, как QTimer, QProcess и сетевые классы, являются реентерабельными. Мы можем использовать их в любом потоке, содержащем цикл обработки событий. Во вторичных потоках цикл обработки событий начинается с вызова QThread::exec() или таких удобных функций, как QProcess::waitForFinished() и QAbstractSocket::waitForDisconnected().
Из-за ограничений, унаследованных от низкоуровневых библиотек, на основе которых построена поддержка графического пользовательского интерфейса в Qt, QWidget и его подклассы нереентерабельны. Одним из следствий этого является невозможность прямого вызова функций виджета из вторичного потока. Если мы, например, хотим изменить текст QLabel из вторичного потока, мы можем генерировать сигнал, связанный с QLabel::setText(), или вызвать из этого потока функцию QMetaObject::invokeMethod(). Например:
void MyThread::run()
{
…
QMetaObject::invokeMethod(label, SLOT(setText(const QString &)),
Q_ARG(QString, "Hello"));
…
}
Многие из классов Qt неграфического интерфейса, включая QImage, QString и классы—контейнеры, применяют оптимизацию неявного совместного использования данных. Хотя такая оптимизация делает класс нереентерабельным, в Qt не возникает проблем, потому что Qt использует атомарные инструкции языка ассемблер для реализации потокозащищенного подсчета ссылок, делая реентерабельными Qt—классы, применяющие неявное совместное использование данных.
Модуль QtSql также может использоваться в многопоточных приложениях, но он имеет свои ограничения, которые отличаются для разных баз данных. Более подробную информацию вы найдете в сети Интернет по адресу . Полный список предостережений, относящихся к многопоточной обработке, находится на веб-странице .
Глава 19. Создание подключаемых модулей
Динамические библиотеки (называемые также совместно используемыми библиотеками или библиотеками DLL) — это независимые модули, хранимые в отдельном файле на диске, доступ к которым могут получать несколько приложений. Как правило, необходимые для программы динамические библиотеки определяются на этапе сборки, в таких случаях эти библиотеки автоматически загружаются при запуске приложения. При таком подходе обычно в файл приложения .pro добавляются библиотека и, возможно, путь доступа к ней, а в исходные файлы включаются соответствующие заголовочные файлы. Например:
LIBS += -ldb_cxx
INCLUDEPATH += /usr/local/BerkeleyDB.4.2/include
Альтернативный подход заключается в динамической загрузке библиотеки по мере необходимости и затем разрешении ее символов, которые будут нами использоваться. Qt предоставляет класс QLibrary для решения этой задачи независимым от платформы способом. Получая основную часть имени библиотеки, QLibrary выполняет поиск соответствующего файла в стандартном для платформы месте. Например, если задано имя mimetype, будет выполняться поиск файла mimetype.dll в Windows, mimetype.so в Linux и mimetype.dylib в Mac OS X.
Часто можно расширять функциональные возможности современных приложений с графическим пользовательским интерфейсом за счет применения подключаемых модулей. Подключаемый модуль (plugin) — это динамическая библиотека, которая реализует специальный интерфейс для обеспечения дополнительной функциональности. Например, в мы создали подключаемый модуль для интеграции пользовательского виджета в Qt Designer.
Qt распознает свой собственный набор интерфейсов подключаемых модулей, относящихся к различным областям, включая форматы изображений, драйверы баз данных, стили виджетов, текстовые кодировки и условия доступа. Первый раздел данной главы показывает, как можно расширить возможности Qt с помощью подключаемых модулей.
Кроме того, можно создавать подключаемые модули, предназначенные для конкретных Qt—приложений. Писать такие подключаемые модули в Qt достаточно просто с использованием фреймворка Qt для подключаемых модулей, который повышает безопасность и удобство применения класса QLibrary. В последних двух разделах данной главы мы покажем, как обеспечить в приложении поддержку подключаемых модулей и как создавать пользовательские подключаемые модули для приложения.
Расширение Qt с помощью подключаемых модулей
Qt можно расширять, используя различные типы подключаемых модулей, среди которых наиболее распространенными являются драйверы баз данных, форматы изображений, стили и текстовые кодировки. Каждый тип подключаемых модулей обычно требует наличия по крайней мере двух классов: класса—оболочки, который реализует общие функции программного интерфейса подключаемых модулей, и одного или более классов—обработчиков, которые реализуют программный интерфейс для конкретного типа подключаемых модулей. Обработчики вызываются из класса—оболочки.
Ниже приведен список Qt—классов подключаемых модулей и обработчиков, исключая Qtopia Core (рис. 19.1):
• QAccessibleBridgePlugin — QAccessibleBridge,
• QAccessiblePlugin — QAccessibleIntertace,
• QIconEnginePlugin — QIconEngine,
• QImageIOPlugin — QImageIOHandler,
• QInputContextPlugin — QInputContext,
• QPictureFormatPlugin — нет обработчика,
• QSqlDriverPlugin — QSqlDriver,
• QStylePlugin — QStyle,
• QTextCodecPlugin — QTextCodec.
В демонстрационных целях мы реализуем подключаемый модуль, способный считывать в Windows монохромные файлы курсоров (файлы .cur). Эти файлы могут содержать несколько изображений разного размера для одного курсора. После построения и установки этого подключаемого модуля Qt сможет считывать файлы .cur и получать доступ к отдельным курсорам (например, с помощью классов QImage, QImageReader или QMovie); также можно будет преобразовывать эти курсоры в любой другой формат файлов изображений, воспринимаемый Qt (например, BMP, JPEG и PNG). Кроме того, подключаемый модуль может разворачиваться совместно с Qt—приложениями, поскольку они автоматически проверяют стандартные места расположения подключаемых модулей Qt и загружают все найденные модули.
Новые классы—оболочки подключаемых модулей должны быть подклассом QImageIOPlugin и должны обеспечить реализацию нескольких виртуальных функций:
01 class CursorPlugin : public QImageIOPlugin
02 {
03 public:
04 QStringList keys() const;
05 Capabilities capabilities(QIODevice *device,
06 const QByteArray &format) const;
07 QImageIOHandler *create(QIODevice *device,
08 const QByteArray &format) const;
09 };
Функция keys() возвращает список форматов изображений, которые поддерживает подключаемый модуль. Можно считать, что параметр format функций capabilities() и create() имеет значение из этого списка.
01 QStringList CursorPlugin::keys() const
02 {
03 return QStringList() << "cur";
04 }
Наш подключаемый модуль поддерживает один формат изображений, поэтому возвращается список, содержащий только одно название. В идеале это название должно совпадать с расширением файла, используемым данным форматом. Если форматы имеют несколько расширений (например, .jpg и .jpeg для JPEG), мы можем возвращать список с несколькими элементами, относящимися к одному формату, — по одному элементу на каждое расширение.
01 QImageIOPlugin::Capabilities
02 CursorPlugin::capabilities(QIODevice *device,
03 const QByteArray &format) const
04 {
05 if (format == "cur")
06 return CanRead;
07 if (format.isEmpty()) {
08 CursorHandler handler;
09 handler.setDevice(device);
10 if (handler.canRead())
11 return CanRead;
12 }
13 return 0;
14 }
Функция capabilities() возвращает объект, который показывает, что может делать с данным форматом изображений обработчик изображений. Существует три возможных действия (CanRead, CanWrite и CanReadIncremental), а возвращаемое значение объединяет допустимые варианты порязрадной логической операцией ИЛИ.
Если формат «cur», наша реализация возвращает CanRead. Если формат не задан, мы создаем обработчик курсора и проверяем его способность чтения данных с заданного устройства. Функция canRead() только просматривает данные и проверяет возможность распознавания файла, не изменяя указатель файла. Возвращение 0 означает, что данный обработчик не может ни считывать, ни записывать файл.
01 QImageIOHandler *CursorPlugin::create(QIODevice *device,
02 const QByteArray &format) const
03 {
04 CursorHandler *handler = new CursorHandler;
05 handler->setDevice(device);
06 handler->setFormat(format);
07 return handler;
08 }
Когда файл курсора открыт (например, с помощью класса QImageReader), будет вызвана функция оболочки подключаемого модуля create() с передачей указателя устройства и формата «cur». Мы создаем экземпляр CursorHandler для заданного устройства и формата. Вызывающая программа становится владельцем обработчика и удалит его, когда он не станет нужен. Если приходится считывать несколько файлов, для каждого из них создается новый обработчик.
Q_EXPORT_PLUGIN2(cursorplugin, CursorPlugin)
В конце файла .cpp мы используем макрос Q_EXPORT_PLUGIN2(), чтобы гарантировать распознавание в Qt подключаемого модуля. В первом параметре задается произвольное имя, используемое нами для подключаемого модуля. Второй параметр содержит имя класса подключаемого модуля.
Подкласс QImageIOPlugin создается достаточно просто. Реальная работа подключаемого модуля делается обработчиком. Обработчики форматов изображений должны создать подкласс QImageIOHandler и переопределить некоторые или все его открытые функции. Сначала рассмотрим заголовочный файл:
01 class CursorHandler : public QImageIOHandler
02 {
03 public:
04 CursorHandler();
05 bool canRead() const;
06 bool read(QImage *image);
07 bool jumpToNextImage();
08 int currentImageNumber() const;
09 int imageCount() const;
10 private:
11 enum State { BeforeHeader, BeforeImage, AfterLastImage, Error };
12 void readHeaderIfNecessary() const;
13 QBitArray readBitmap(int width, int height, QDataStream &in) const;
14 void enterErrorState() const;
15 mutable State state;
16 mutable int currentImageNo;
17 mutable int numImages;
18 };
Открытые функции имеют фиксированную сигнатуру. Здесь нет некоторых функций, которые не надо переопределять в обработчике, обеспечивающем только чтение, в частности отсутствует функция write(). Переменные—члены объявляются с ключевым словом mutable, потому что они изменяются внутри константных функций.
01 CursorHandler::CursorHandler()
02 {
03 state = BeforeHeader;
04 currentImageNo = 0;
05 numImages = 0;
06 }
После создания обработчика мы сначала настраиваем его параметры. Номер текущего изображения курсора устанавливается на первый курсор, но поскольку переменная количества изображений numImages принимает значение 0, ясно, что у нас пока еще нет изображений.
01 bool CursorHandler::canRead() const
02 {
03 if (state == BeforeHeader) {
04 return device()->peek(4) == QByteArray("\0\0\2\0", 4);
05 } else {
06 return state != Error;
07 }
08 }
Функция canRead() может вызываться в любой момент для определения возможности считывания обработчиком изображений дополнительных данных с устройства. Если функция вызывается до чтения данных в состоянии BeforeHeader, выполняется проверка конкретной метки, по которой опознаются файлы курсоров в Windows. Вызов QIODevice::peek() считывает первые четыре байта без изменения указателя файла на данном устройстве. Если функция canRead() вызывается позже, мы возвращаем true при отсутствии ошибки.
01 int CursorHandler::currentImageNumber() const
02 {
03 return currentImageNo;
04 }
Эта простая функция возвращает номер курсора, на который позиционирован указатель файла устройства.
После создания обработчика пользователь может вызвать любую его открытую функцию, причем последовательность вызовов функций может быть произвольной. В этом кроется потенциальная проблема, поскольку необходимо исходить из того, что файл можно читать только последовательно, поэтому сначала надо один раз считать заголовок файла и затем выполнять какие-то другие действия. Эту проблему решаем путем вызова readHeaderIfNecessary() в тех функциях, для которых требуется предварительное считывание заголовка файла.
01 int CursorHandler::imageCount() const
02 {
03 readHeaderIfNecessary();
04 return numImages;
05 }
Эта функция возвращает количество изображений, содержащихся в файле. Для правильного файла, при чтении которого не возникает ошибок, она возвращает по крайней мере 1.
Рис. 19.2. Формат файла .cur.
Следующая функция довольно сложная, поэтому мы рассмотрим ее по частям:
01 bool CursorHandler::read(QImage *image)
02 {
03 readHeaderIfNecessary();
04 if (state != BeforeImage)
05 return false;
Функция read() считывает данные изображения, начинающегося в текущей позиции указателя устройства. Если успешно считан заголовок файла или указатель устройства после чтения изображения находится в начале другого изображения, можно считывать следующее изображение.
06 quint32 size;
07 quint32 width;
08 quint32 height;
09 quint16 numPlanes;
10 quint16 bitsPerPixel;
11 quint32 compression;
12 QDataStream in(device());
13 in.setByteOrder(QDataStream::LittleEndian);
14 in >> size;
15 if (size != 40) {
16 enterErrorState();
17 return false;
18 }
19 in >> width >> height >> numPlanes >> bitsPerPixel >> compression;
20 height /= 2;
21 if (numPlanes != 1 || bitsPerPixel != 1 || compression != 0) {
22 enterErrorState();
23 return false;
24 }
25 in.skipRawData((size - 20) + 8);
Мы создаем объект QDataStream для чтения устройства. Необходимо установить порядок байтов в соответствии с тем, который определен спецификацией формата файла .cur. Задавать версию потока QDataStream нет необходимости, поскольку форматы целых чисел и чисел с плавающей запятой не зависят от версии потока данных. Затем считываем элементы заголовка курсора и пропускаем неиспользуемые части заголовка и 8-байтовую таблицу цветов с помощью функции QDataStream::skipRawData().
Необходимо учитывать все характерные особенности формата, например, уменьшая вдвое высоту изображения, потому что она в формате .cur в два раза превышает высоту реального изображения. Переменные bitsPerPixel и compression всегда имеют значения 1 и 0 в монохромных файлах .cur. При возникновении каких-либо проблем вызываем функцию enterErrorState() и возвращаем false.
26 QBitArray xorBitmap = readBitmap(width, height, in);
27 QBitArray andBitmap = readBitmap(width, height, in);
28 if (in.status() != QDataStream::Ok) {
29 enterErrorState();
30 return false;
31 }
Следующими элементами файла являются две битовые маски: одна XOR—маска, а другая AND—маска. Мы их считываем в массивы QBitArray, а не в QBitmap. Класс QBitmap предназначен для выполнения с ним операций рисования и вывода рисунка на экран, а нам нужен простой массив битов.
Завершив чтение файла, проверяем состояние потока QDataStream. Так можно поступать, потому что, если QDataStream переходит в состояние ошибки, это состояние сохраняется в дальнейшем и последующие операции чтения могут выдать только нули. Например, если чтение первого массива бит завершается неудачей, попытка чтения второго массива в результате даст пустой массив QBitArray.
32 *image = QImage(width, height, QImage::Format_ARGB32);
33 for (int i = 0; i < int(height); ++i) {
34 for (int j = 0; j < int(width); ++j) {
35 QRgb color;
36 int bit = (i * width) + j;
37 if (andBitmap.testBit(bit)) {
38 if (xorBitmap.testBit(bit)) {
39 color = 0x7F7F7F7F;
40 } else {
41 color = 0x00FFFFFF;
42 }
43 } else {
44 if (xorBitmap.testBit(bit)) {
45 color = 0xFFFFFFFF;
46 } else {
47 color = 0xFF000000;
48 }
50 }
51 image->setPixel(j, i, color);
52 }
53 }
Мы конструируем новый объект QImage с правильными размерами и устанавливаем на него указатель изображения. Затем проходим по каждому пикселю битовых массивов XOR и AND и преобразуем их в 32-битовый цветовой формат ARGB. С помощью массивов битов AND и XOR цвет каждого пикселя курсора всегда получается в соответствии со следующей таблицей:
С получением черного, белого и прозрачного пикселей нет проблем, однако нельзя получить инвертированный пиксель фона, используя цветовой формат ARGB, если не знаешь цвет исходного пикселя фона. В качестве замены используем полупрозрачный серый цвет (0x7F7F7F7F).
54 ++currentImageNo;
55 if (currentImageNo == numImages)
56 state = AfterLastImage;
57 return true;
58 }
Завершив чтение изображения, мы обновляем текущий номер изображения и обновляем состояние, если прочитано последнее изображение. В конце функции устройство будет указывать на начало следующего изображения или на конец файла.
01 bool CursorHandler::jumpToNextImage()
02 {
03 QImage image;
04 return read(&image);
05 }
Функция jumpToNextImage() используется для пропуска изображения. Для простоты мы всего лишь вызываем read() и игнорируем полученный QImage. В более эффективной реализации использовалась бы информация, содержащаяся в заголовке файла .cur, для непосредственного смещения по файлу на соответствующее значение.
01 void CursorHandler::readHeaderIfNecessary() const
02 {
03 if (state != BeforeHeader)
04 return;
05 quint16 reserved;
06 quint16 type;
07 quint16 count;
08 QDataStream in(device());
09 in.setByteOrder(QDataStream::LittleEndian);
10 in >> reserved >> type >> count;
11 in.skipRawData(16 * count);
12 if (in.status() != QDataStream::Ok || reserved != 0
13 || type != 2 || count == 0) {
14 enterErrorState();
15 return;
16 }
17 state = BeforeImage;
18 currentImageNo = 0;
19 numImages = int(count);
20 }
Закрытая функция readHeaderIfNecessary() вызывается из imageCount() и read(). Если заголовок файла уже был прочитан, состояние не будет иметь значение BeforeHeader (перед заголовком) и сразу же делается возврат управления. В противном случае открываем на устройстве поток данных, считываем некоторые общие данные (в частности, количество курсоров, содержащихся в файле) и устанавливаем состояние в значение BeforeImage (перед изображением). В конце указатель файла данного устройства устанавливается перед первым изображением.
01 void CursorHandler::enterErrorState() const
02 {
03 currentImageNo = 0;
04 numImages = 0;
05 state = Error;
06 }
При возникновении ошибки считаем, что файл не содержит изображений требуемого формата, и устанавливаем состояние в значение Error. В дальнейшем такое состояние обработчика не может быть изменено.
01 QBitArray CursorHandler::readBitmap(int width, int height,
02 QDataStream &in) const
03 {
04 QBitArray bitmap(width * height);
05 quint8 byte;
06 quint32 word;
07 for (int i = 0; i < height; ++i) {
08 for (int j = 0; j < width; ++j) {
09 if ((j % 32) == 0) {
10 word = 0;
11 for (int k = 0; k < 4; ++k) {
12 in >> byte;
13 word = (word << 8) | byte;
14 }
15 }
16 bitmap.setBit(((height - i - 1) * width) + j,
17 word & 0x80000000);
18 word <<= 1;
19 }
20 }
21 return bitmap;
22 }
Функция readBitmap() используется для чтения масок курсора AND и XOR. Эти маски обладают двумя необычными свойствами. Во-первых, строки в них располагаются, начиная с нижних, вместо обычного расположения строк сверху вниз. Во-вторых, оказывается, что используемый здесь порядок байтов отличается от порядка байтов любых других данных в файлах .cur. В связи с этим нам приходится инвертировать координату у в вызове setBit() и считывать маски побайтно, сдвигая биты и используя маску для получения правильных значений.
Этим завершается реализация класса CursorHandler — подключаемого модуля, предназначенного для работы с изображениями курсоров. Подключаемые модули для изображений других форматов могли бы создаваться аналогично, хотя в некоторых случаях может потребоваться реализация дополнительных функций программного интерфейса QImageIOHandler, в частности функций, используемых для записи изображений. Подключаемые модули другого вида, например кодировки текста или драйверы баз данных, создаются по тому же самому образцу: реализуются класс—оболочка, обеспечивающий общий программный интерфейс подключаемых модулей, который может использоваться приложением, и обработчик, обеспечивающий базовую функциональность.
Файл .pro для подключаемых модулей отличается от файлов .pro, используемых для приложений, поэтому мы покажем его состав:
TEMPLATE = lib
CONFIG += plugin
HEADERS = cursorhandler.h \
cursorplugin.h
SOURCES = cursorhandler.cpp \
cursorplugin.cpp
DESTDIR = $(QTDIR)/plugins/imageformats
По умолчанию файлы .pro используют шаблон app, но здесь мы должны указать шаблон lib, потому что подключаемый модуль является библиотекой, а не автономным приложением. Строка с элементом CONFIG указывает Qt на то, что у нас не простая библиотека, а библиотека подключаемого модуля. Элемент DESTDIR определяет каталог размещения подключаемого модуля. Каждый подключаемый модуль Qt должен находиться в соответствующем подкаталоге каталога plugins, и поскольку наш подключаемый модуль обеспечивает новый формат изображений, помещаем его в plugins/imageformats. Список имен каталогов и типов подключаемых модулей приводится на веб-странице . В данном случае мы предполагаем, что переменная среды QTDIR определяет каталог, в котором находится Qt.
Для Qt в рабочем (release) и отладочном (debug) режимах создаются различные подключаемые модули, поэтому, если установлены обе версии Qt, имеет смысл указать в файле .pro ту из них, которая будет использоваться, добавляя строку
CONFIG += release
Приложения, использующие подключаемые модули Qt, должны разворачиваться совместно со своими подключаемыми модулями. Подключаемые модули Qt должны располагаться в конкретных подкаталогах (например, в imageformats для форматов изображений). Приложения Qt ищут подключаемые модули в каталоге plugins, который располагается в каталоге размещения исполняемого модуля приложения, поэтому поиск подключаемых модулей изображений будет выполняться в application_dir/plugins/imageformats. Если требуется развернуть подключаемые модули Qt в другом каталоге, можно установить дополнительный путь поиска, используя функцию QCoreApplication::addLibraryPath().
Как обеспечить в приложении возможность подключения модулей
Подключаемый к приложению модуль является динамической библиотекой, которая реализует какой-нибудь один или несколько интерфейсов. Интерфейс — это класс, содержащий только чисто виртуальные функции. Связь между приложением и подключаемыми модулями осуществляется через виртуальную таблицу интерфейса. В этом разделе мы основное внимание уделим способам взаимодействия приложения Qt с подключаемым модулем через его интерфейсы, а в следующем разделе покажем, как можно реализовать подключаемый модуль.
Чтобы продемонстрировать конкретный пример, создадим простое приложение Text Art (искусство отображения текста), показанное на рис. 19.3. Специальные эффекты отображения текста обеспечиваются подключаемыми модулями; приложение получает список текстовых эффектов, создаваемых каждым подключаемым модулем, и проходит в цикле по этому списку, показывая результат каждого эффекта в соответствующем элементе списка QListWidget.
Рис. 19.3. Приложение Text Art.
В приложении Text Art определяется один интерфейс:
01 class TextArtInterface
02 {
03 public:
04 virtual ~TextArtInterface() { }
05 virtual QStringList effects() const = 0;
06 virtual QPixmap applyEffect(const QString &effect,
07 const QString &text,
08 const QFont &font,
09 const QSize &size,
10 const QPen &pen,
11 const QBrush &brush) = 0;
12 };
13 Q_DECLARE_INTERFACE(TextArtInterface,
14 "com.software-inc.TextArt.TextArtInterface/1.0")
В классе интерфейса обычно объявляются виртуальный деструктор, виртуальная функция, возвращающая список QStringList, и одна или несколько других виртуальных функций. Деструктор объявляется прежде всего для того, чтобы компилятор не жаловался на отсутствие виртуального деструктора в классе, который имеет виртуальные функции. В данном примере функция effects() возвращает список текстовых эффектов, которые могут создаваться подключаемым модулем. Этот список можно рассматривать как список ключей. При каждом вызове одной из функций мы передаем эти ключи в качестве первого аргумента, позволяя реализовать в одном подключаемом модуле несколько эффектов.
В конце мы используем макрос Q_DECLARE_INTERFACE() для назначения некоторого идентификатора интерфейсу. Этот идентификатор обычно имеет четыре компонента: инвертированное имя домена, определяющее создателя интерфейса, имя приложения, имя интерфейса и номер версии. При любом изменении интерфейса (например, при добавлении новой виртуальной функции или при изменении сигнатуры существующей функции) мы должны не забыть увеличить номер версии; в противном случае приложение может завершиться аварийно при попытке получения доступа к старой версии подключаемого модуля.
Это приложение реализуется в виде класса TextArtDialog. Мы будем показывать только тот программный код, который связан с применением подключаемых модулей. Давайте начнем с конструктора:
01 TextArtDialog::TextArtDialog(const QString &text, QWidget *parent)
02 : QDialog(parent)
03 {
04 listWidget = new QListWidget;
05 listWidget->setViewMode(QListWidget::IconMode);
06 listWidget->setMovement(QListWidget::Static);
07 listWidget->setIconSize(QSize(260, 80));
08 …
09 loadPlugins();
10 populateListWidget(text);
11 …
12 }
Конструктор создает виджет QListWidget, содержащий список доступных эффектов. Он вызывает закрытую функцию loadPlugins() для поиска и загрузки всех подключаемых модулей, реализующих интерфейс TextArtInterface, и заполняет список виджетов с помощью вызова другой закрытой функции — populateListWidget().
01 void TextArtDialog::loadPlugins()
02 {
03 QDir pluginDir(QApplication::applicationDirPath());
04 #if defined(Q_OS_WIN)
05 if (pluginDir.dirName().toLower() == "debug"
06 || pluginDir.dirName().toLower() == "release")
07 pluginDir.cdUp();
08 #elif defined(Q_OS_MAC)
09 if (pluginDir.dirName() == "MacOS") {
10 pluginDir.cdUp();
11 pluginDir.cdUp();
12 pluginDir.cdUp();
13 }
14 #endif
15 if (!pluginDir.cd("plugins"))
16 return;
17 foreach (QString fileName, pluginDir.entryList(QDir::Files)) {
18 QPluginLoader loader(pluginDir.absoluteFilePath(fileName));
19 if (TextArtInterface *interface =
20 qobject_cast
21 interfaces.append(interface);
22 }
23 }
В функции loadPlugins() мы пытаемся загрузить все файлы, находящиеся в каталоге приложения plugins. (В Windows исполняемый модуль приложения обычно находится в подкаталоге debug или release, поэтому поднимаемся на один каталог выше. В Mac OS X учитываем структуру группового каталога (bundle directory).)
Если файл, который мы пытаемся загрузить, является подключаемым модулем Qt и имеет ту же саму версию Qt, какую имеет приложение, функция QPluginLoader::instance() возвратит указатель QObject *, ссылающийся на подключаемый модуль Qt. Используем qobject_cast
Для некоторых приложений может потребоваться загрузка двух или более различных интерфейсов, и в этом случае программный код по получении этих интерфейсов мог бы выглядеть следующим образом:
01 QObject *plugin = loader.instance();
02 if (TextArtInterface *i = qobject_cast
03 textArtInterfaces.append(i);
04 if (BorderArtInterface *i = qobject_cast
05 borderArtInterfaces.append(i);
06 if (TextureInterface *i = qobject_cast
07 textureInterfaces.append(i);
Тип одного подключаемого модуля может успешно приводиться к нескольким указателям интерфейсов, поскольку подключаемые модули могут обеспечивать несколько интерфейсов, используя множественное наследование.
01 void TextArtDialog::populateListWidget(const QString &text)
02 {
03 QSize iconSize = listWidget->iconSize();
04 QPen pen(QColor("darkseagreen"));
05 QLinearGradient gradient(0, 0, iconSize.width() / 2,
06 iconSize.height() / 2);
07 gradient.setColorAt(0.0. QColor("darkolivegreen"));
08 gradient.setColorAt(0.8, QColor("darkgreen"));
09 gradient.setColorAt(1.0, QColor("lightgreen"));
10 QFont font("Helvetica", iconSize.height(), QFont::Bold);
11 foreach (TextArtInterface *interface, interfaces) {
12 foreach (QString effect, interface->effects()) {
13 QListWidgetItem *item = new QListWidgetItem(
14 effect, listWidget);
15 QPixmap pixmap = interface->applyEffect(effect,
16 text, font, iconSize, pen, gradient);
17 item->setData(Qt::DecorationRole, pixmap);
18 }
19 }
20 listWidget->setCurrentRow(0);
21 }
Функция populateListWidget() начинается с создания некоторых переменных, передаваемых функции applyEffect(), в частности пера, линейного градиента и шрифта. Затем она просматривает в цикле все интерфейсы TextArtInterface, найденные функцией loadPlugins(). Для любого эффекта, обеспечиваемого каждым интерфейсом, создается новый элемент QListWidgetItem, текст которого определяет название создаваемого им эффекта, и создается QPixmap, используя applyEffect().
В данном разделе мы увидели, как можно загружать подключаемые модули, вызывая в конструкторе функцию loadPlugins(), и как можно их использовать в функции populateListWidget(). Программный код элегантно обрабатывает ситуации, когда подключаемые модули вообще не обеспечивают интерфейс TextArtInterface или когда только один из них или несколько обеспечивают такой интерфейс. Более того, другие подключаемые модули могут добавляться позже. При каждом запуске приложения производится загрузка всех подключаемых модулей, имеющих нужный интерфейс. Это позволяет легко расширять функциональность приложения без изменения самого приложения.
Написание подключаемых к приложению модулей
Подключаемый к приложению модуль является подклассом QObject и интерфейсов, которые он собирается обеспечить. Прилагаемый к этой книге компакт-диск содержит два подключаемых модуля, предназначенных для приложения Text Art, представленного в предыдущем разделе, и показывающих, что это приложение правильно работает с несколькими подключаемыми модулями.
Здесь мы рассмотрим программный код только одного из них — Basic Effects Plugin (модуль основных эффектов). Предполагаем, что исходный код подключаемого модуля находится в каталоге basiceffectsplugin и что приложение Text Art находится в параллельном каталоге textart. Ниже приводится объявление класса подключаемого модуля:
01 class BasicEffectsPlugin
02 : public QObject, public TextArtInterface
03 {
04 Q_OBJECT
05 Q_INTERFACES(TextArtInterface)
06 public:
07 QStringList effects() const;
08 QPixmap applyEffect(const QString &effect, const QString &text,
09 const QFont &font, const QSize &size,
10 const QPen &pen, const QBrush &brush);
11 };
Этот подключаемый модуль реализует только один интерфейс — TextArtInterface. Кроме Q_OBJECT необходимо использовать макрос Q_INTERFACES() для каждого интерфейса, для которого создается подкласс, чтобы обеспечить безболезненное восприятие компилятором moc оператора приведения типа qobject_cast
01 QStringList BasicEffectsPlugin::effects() const
02 {
03 return QStringList() << "Plain" << "Outline" << "Shadow";
04 }
Функция effects() возвращает список текстовых эффектов, поддерживаемых подключаемым модулем. Этот подключаемый модуль обеспечивает три эффекта, поэтому возвращаем список, содержащий имена каждого из них.
Функция applyEffect() обеспечивает функциональность подключаемого модуля и слегка запутанна, поэтому рассмотрим ее по частям:
01 QPixmap BasicEffectsPlugin::applyEffect(const QString &effect,
02 const QString &text, const QFont &font, const QSize &size,
03 const QPen &pen, const QBrush &brush)
04 {
05 QFont myFont = font;
06 QFontMetrics metrics(myFont);
07 while ((metrics.width(text) > size.width() ||
08 metrics.height() > size.height())
09 && myFont.pointSize() > 9) {
10 myFont.setPointSize(myFont.pointSize() - 1);
11 metrics = QFontMetrics(myFont);
12 }
Мы хотим обеспечить по мере возможности достаточность указанного размера для размещения заданного текста. По этой причине используем метрики шрифта и, если текст оказывается слишком большим, входим в цикл, где уменьшаем размер, пока он не окажется подходящим или не достигнет 9 точек, что соответствует нашему минимальному размеру.
13 QPixmap pixmap(size);
14 QPainter painter(&pixmap);
15 painter.setFont(myFont);
16 painter.setPen(pen);
17 painter.setBrush(brush);
18 painter.setRenderHint(QPainter::Antialiasing, true);
19 painter.setRenderHint(QPainter::TextAntialiasing, true);
20 painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
21 painter.eraseRect(pixmap.rect());
Мы создаем пиксельную карту требуемого размера и рисовальщик для рисования на пиксельной карте. Также устанавливаем некоторые особенности воспроизведения, чтобы обеспечить максимальное сглаживание при выводе текста. Вызов функции eraseRect() очищает пиксельную карту, заполняя ее цветом фона.
22 if (effect == "Plain") {
23 painter.setPen(Qt::NoPen);
24 } else if (effect == "Outline") {
25 QPen pen(Qt::black);
26 pen.setWidthF(2.5);
27 painter.setPen(pen);
28 } else if (effect == "Shadow") {
29 QPainterPath path;
30 painter.setBrush(Qt::darkGray);
31 path.addText(((size.width() - metrics.width(text)) / 2) + 3,
32 (size.height() - metrics.descent()) + 3, myFont, text);
33 painter.drawPath(path);
34 painter.setBrush(brush);
35 }
Для эффекта «Plain» (простой) не требуется никакой рамки. Для эффекта «Outline» (рамка) игнорируем исходное перо и создаем наше собственное перо шириной в 2.5 пикселя. Для эффекта «Shadow» (тень) сначала рисуется тень, чтобы можно было выводить текст поверх нее.
36 QPainterPath path;
37 path.addText((size.width() - metrics.width(text)) / 2,
38 size.height() - metrics.descent(), myFont, text);
39 painter.drawPath(path);
40 return pixmap;
41 }
Теперь у нас имеются перо и кисти, соответствующим образом установленные для каждого текстового эффекта, а для эффекта «Shadow» нарисована тень. После этого мы готовы воспроизвести текст. Текст центрируется по горизонтали и выводится достаточно далеко от нижнего края пиксельной карты, чтобы оставить достаточно места для размещения нижних выносных элементов.
Q_EXPORT_PLUGIN2(basiceffectsplugin, BasicEffectsPlugin)
В конце файла .cpp используем макрос Q_EXPORT_PLUGIN2(), чтобы этот подключаемый элемент был доступен для Qt.
Файл .pro аналогичен файлу, который мы использовали ранее в данной главе для подключаемого модуля курсоров Windows:
TEMPLATE = lib
CONFIG += plugin
HEADERS = ../textart/textartinterface.h \
basiceffectsplugin.h
SOURCES = basiceffectsplugin.cpp
DESTDIR =../textart/plugins
Если данная глава повысила ваш интерес к подключаемым к приложению модулям, вы можете изучить имеющийся в Qt более сложный пример Plug & Paint (подключи и рисуй). Приложение поддерживает три различных интерфейса и включает в себя полезное диалоговое окно Plugin Information (информация о подключаемых модулях), которое содержит списки подключаемых модулей и интерфейсов, доступных в приложении.
Глава 20. Возможности, зависимые от платформы
В данной главе мы рассмотрим некоторые доступные программистам Qt возможности, которые зависят от платформы. Мы начнем с рассмотрения способов доступа к таким «родным» программным интерфейсам, как Win32 API в системе Windows, Carbon в системе Mac OS X и Xlib в системе X11. Затем мы перейдем к изучению расширения ActiveQt, демонстрируя способы применения элементов управления ActiveX в приложениях Qt, работающих в системе Windows, а также способы создания приложений, выполняющих функции серверов ActiveX. В последнем разделе мы рассмотрим способы взаимодействия приложений Qt с менеджером сеансов системы X11.
Кроме представленных здесь возможностей компания «Trolltech» предлагает несколько зависимых от платформы решений в рамках проекта Qt Solutions, в частности миграционные фреймворки Qt/Motif и Qt/MFC, позволяющие упростить перевод в Qt приложений Motif/Xt и MFC. Подобное расширение для приложений Tcl/Tk обеспечивается фирмой «Froglogic», а компанией «Klaralvdalens Datakonsult» разработан конвертор ресурсов Windows компании Microsoft. Дополнительную информацию вы найдете на следующих веб-страницах:
•
•
•
Для встроенных приложений компания «Trolltech» обеспечивает Qtopia — рабочую среду для разработки таких приложений. Она рассматривается в .
Применение «родных» программных интерфейсов
Всесторонний программный интерфейс Qt удовлетворяет большинству требований на всех платформах, но при некоторых обстоятельствах нам может потребоваться базовый, платформозависимый программный интерфейс. В данном разделе мы продемонстрируем способы применения «родных» программных интерфейсов различных платформ, поддерживаемых Qt, для решения конкретных задач.
Для каждой платформы класс QWidget поддерживает функцию winId(), которая возвращает идентификатор или описатель окна. QWidget также обеспечивает статическую функцию find(), которая возвращает QWidget с идентификатором конкретного окна. Мы можем передавать этот идентификатор функциям «родного» программного интерфейса для достижения эффектов, зависимых от платформы. Например, в следующем программном коде используется функция winId() для отображения слева заголовка панели инструментов, используя «родные» функции Mac OS X:
#ifdef Q_WS_MAC
ChangeWindowAttributes(HIViewGetWindow(HIViewRef(toolWin.winId())),
kWindowSideTitlebarAttribute, kWindowNoAttributes);
#endif
Рис. 20.1. Окно панели инструментов Mac OS X с отображением заголовка сбоку.
Ниже показано, как в системе X11 мы можем модифицировать свойство окна:
#ifdef Q_WS_X11
Atom atom = XInternAtom(QX11Info::display(), "MY_PROPERTY", False);
long data = 1;
XChangeProperty(QX11Info::display(), window->winId(), atom, atom,
32, PropModeReplace, reinterpret_cast
#endif
Использование директив #ifdef и #endif вокруг зависимого от платформы программного кода гарантирует компиляцию приложения на других платформах.
Приведенный ниже пример показывает, как в приложениях, предназначенных только для Windows, можно использовать вызовы GDI для рисования на виджете Qt:
01 void GdiControl::paintEvent(QPaintEvent * /* event */)
02 {
03 RECT rect;
04 GetClientRect(winId(), &rect);
05 HDC hdc = GetDC(winId());
06 FillRect(hdc, &rect, HBRUSH(COLOR_WINDOW + 1));
07 SetTextAlign(hdc, TA_CENTER | TA_BASELINE);
08 TextOutW(hdc, width() / 2, height() / 2,
09 text.utf16(), text.size());
10 ReleaseDC(winId(), hdc);
11 }
Чтобы это сработало, мы должны также переопределить функцию QPaintDevice::paintEngine() для возврата нулевого указателя и установить атрибут Qt::WA_PaintOnScreen в конструкторе виджета.
Следующий пример показывает, как можно сочетать QPainter и GDI в обработчике события рисования, используя функции getDC() и releaseDC() класса QPaintEngine:
01 void MyWidget::paintEvent(QPaintEvent * /* event */)
02 {
03 QPainter painter(this);
04 painter.fillRect(rect().adjusted(20, 20, -20, -20), Qt::red);
05 #ifdef Q_WS_WIN
06 HDC hdc = painter.paintEngine()->getDC();
07 Rectangle(hdc, 40, 40, width() - 40, height() - 40);
08 painter.paintEngine()->releaseDC();
09 #endif
10 }
Подобное совмещение вызовов QPainter и GDI иногда может дать странный результат, особенно когда вызовы QPainter выполняются после вызовов GDI, потому что QPainter делает некоторые предположения о состоянии базового уровня рисования.
Qt определяет один из следующих четырех символов оконной системы: Q_WS_WIN, Q_WS_X11, Q_WS_MAC и Q_WS_QWS (Qtopia). Мы должны обеспечить включение хотя бы одного заголовка Qt перед их использованием в приложениях. Qt также обеспечивает препроцессорные символы для идентификации операционной системы:
• Q_OS_AIX
• Q_OS_BSD4
• Q_OS_BSDI
• Q_OS_CYGWIN
• Q_OS_DGUX
• Q_OS_DYNIX
• Q_OS_FREEBSD
• Q_OS_HPUX
• Q_OS_HURD
• Q_OS_IRIX
• Q_OS_LINUX
• Q_OS_LYNX
• Q_OS_MAC
• Q_OS_NETBSD
• Q_OS_OPENBSD
• Q_OS_OS2EMX
• Q_OS_OSF
• Q_OS_QNX6
• Q_OS_QNX
• Q_OS_RELIANT
• Q_OS_SCO
• Q_OS_SOLARIS
• Q_OS_ULTRIX
• Q_OS_UNIXWARE
• Q_OS_WIN32
• Q_OS_WIN64
Мы можем считать, что по крайней мере один из этих символов будет определен. Для удобства Qt также определяет Q_OS_WIN, когда обнаруживается Win32 или Win64, и Q_OS_UNIX, когда обнаруживается любая операционная система типа Unix (включая Linux и Mac OS X). Во время выполнения приложений мы можем проверить QSysInfo::WindowsVersion или QSysInfo::MacintoshVersion для установки отличий между различными версиями Windows (2000, ME и так далее) или Mac OS X (10.2, 10.3 и так далее).
Кроме макросов операционной и оконной систем существует также ряд макросов компилятора. Например, Q_CC_MSVC определяется в том случае, если компилятором является Visual С++ компании Microsoft. Такие макросы полезны, когда приходится обходить ошибки компилятора.
Несколько классов графического пользовательского интерфейса Qt обеспечивают зависимые от платформы функции, которые возвращают описатели (handle) базового объекта для низкоуровневой обработки. Они перечислены на рис. 20.2:
Mас OS X:
• ATSFontFormatRef QFont::handle();
• CGImageRef QPixmap::macCGHandle();
• GWorldPtr QPixmap::macQDAlphaHandle();
• GWorldPtr QPixmap::macQDHandle();
• RgnHandle QRegion::handle();
• HIViewRef QWidget::winId();
Windows:
• HCURSOR QCursor::handle();
• HDC QPaintEngine::getDC();
• HDC QPrintEngine::getPrinterDC();
• HFONT QFont::handle();
• HPALETTE QColormap::hPal();
• HRGN QRegion::handle();
• HWND QWidget::winId();
X11:
• Cursor QCursor::handle();
• Font QFont::handle();
• Picture QPixmap::x11PictureHandle();
• Picture QWidget::x11PictureHandle();
• Pixmap QPixmap::handle();
• QX11Info QPixmap::x11Info();
• QX11Info QWidget::x11Info();
• Region QRegion::handle();
• Screen QCursor::x11Screen();
• SmcConn QSessionManager::handle();
• Window QWidget::handle();
• Window QWidget::winId();
В системе X11 функции QPixmap::x11Info() и QWidget::x11Info() возвращают объект QX11Info, который обеспечивает различные указатели и описатели с помощью ряда функций, включая display(), screen(), colormap() и visual(). Мы можем использовать их для настройки графического контекста, например QWidget или QPixmap.
Приложениям Qt, которым необходимо взаимодействовать с другими инструментальными средствами и библиотеками, часто приходится осуществлять доступ к низкоуровневым событиям (XEvent в системе X11, MSG в системе Windows, Eventref в системе Mac OS X, QWSEvent для Qtopia), прежде чем они будут преобразованы в события QEvent. Мы можем делать это путем создания подкласса QApplication и переопределения соответствующего зависимого от платформы фильтра событий — одну из следующих функций: x11EventFilter(), winEventFilter(), macEventFilter() и qwsEventFilter(). Мы можем поступать по-другому и осуществлять доступ к зависимым от платформы событиям, которые передаются заданному QWidget путем переопределения какой-то одной из функций winEvent(), x11Event(), macEvent() и qwsEvent(). Это может пригодиться для обработки событий определенного типа, которые Qt обычно игнорирует, например события джойстика.
Более подробную информацию относительно применения зависимых от платформы средств, в том числе как развертывать приложения Qt на различных платформах, можно найти в сети Интернет по адресу .
Применение ActiveX в системе Windows
Технология ActiveX компании Microsoft позволяет приложениям включать в себя компоненты интерфейса пользователя других приложений или библиотек. Она построена на применении технологии СОМ компании Microsoft и определяет один набор интерфейсов приложений, использующих компоненты, и другой набор интерфейсов приложений и библиотек, предоставляющих компоненты.
Версия Qt/Windows для настольных компьютеров (Desktop Edition) обеспечивает рабочую среду ActiveQt для «бесшовного соединения» ActiveX и Qt. ActiveQt состоит из двух модулей:
• Модуль QAxContainer позволяет нам использовать объекты СОМ и встраивать элементы управления ActiveX в приложения Qt.
• Модуль QAxServer позволяет нам экспортировать пользовательские объекты СОМ и элементы управления ActiveX, написанные с помощью средств разработки Qt.
Наш первый пример встраивает Media Player (медиаплеер) системы Windows вприложение Qt при помощи модуля QAxContainer. Приложение Qt добавляет кнопку Open, кнопку Play/Pause, кнопку Stop и ползунок в элемент управления ActiveX Media Player системы Windows.
Рис. 20.3. Приложение Media Player.
Главное окно приложения имеет тип PlayerWindow:
01 class PlayerWindow : public QWidget
02 {
03 Q_OBJECT
04 Q_ENUMS(ReadyStateConstants)
05 public:
06 enum PlayStateConstants {
07 Stopped = 0, Paused = 1, Playing = 2 };
08 enum ReadyStateConstants {
09 Uninitialized = 0, Loading = 1, Interactive = 3, Complete = 4 };
10 PlayerWindow();
11 protected:
12 void timerEvent(QTimerEvent *event);
13 private slots:
14 void onPlayStateChange(int oldState, int newState);
15 void onReadyStateChange(ReadyStateConstants readyState);
16 void onPositionChange(double oldPos, double newPos);
17 void sliderValueChanged(int newValue);
18 void openFile();
19 private:
20 QAxWidget *wmp;
21 QToolButton *openButton;
22 QToolButton *playPauseButton;
23 QToolButton *stopButton;
24 QSlider *seekSlider;
25 QString fileFilters;
26 int updateTimer;
27 };
Класс PlayerWindow наследует QWidget. Макрос Q_ENUMS(), расположенный сразу после Q_OBJECT, необходим для указания компилятору moc, что константы ReadyStateConstants, используемые в слоте onReadyStateChange(), имеют тип enum. В закрытой секции мы объявляем переменную—член QAxWidget *.
01 PlayerWindow::PlayerWindow()
02 {
03 wmp = new QAxWidget;
04 wmp->setControl("{22D6F312-B0F6-11D0-94AB-0080C74C7E95}");
Конструктор начинается с создания объекта QAxWidget для инкапсулирования элемента управления ActiveX Media Player системы Windows. Модуль QAxContainer состоит из трех классов: QAxObject инкапсулирует объект COM, QAxWidget инкапсулирует элемент управления ActiveX и QAxBase реализует основную функциональность СОМ для QAxObject и QAxWidget.
Мы вызываем функцию setControl() для объекта QAxWidget с идентификатором класса элемента управления Media Player 6.4 системы Windows. Это создает экземпляр требуемого компонента. С этого момента все свойства, события и методы элемента управления ActiveX доступны как свойства, сигналы и методы Qt объекта QAxWidget.
Рис. 20.4. Дерево наследования для модуля QAxContainer.
Типы данных СОМ автоматически преобразуются в соответствующие типы объектов, как показано на рис. 20.5:
• VARIANT_BOOL — bool,
• char, short, int, long — int,
• unsigned char, unsigned short, unsigned int, unsigned long — uint,
• float, double — double,
• CY — qlonglong, qulonglong,
• BSTR — QString,
• DATE — QDateTime, QDate, QTime,
• OLE_COLOR — QColor,
• SAFEARRAY(VARIANT) — QList
• SAFEARRAY(BSTR) — QStringList,
• SAFEARRAY(BYTE) — QByteArray,
• VARIANT — QVariant,
• IFontDisp * — QFont,
• IPictureDisp * — QPixmap,
• Тип, определяемый пользователем — QRect, QSize, QPoint.
Например, входной параметр типа VARIANT_BOOL становится типом bool, а выходной параметр типа VARIANT_BOOL становится типом bool &. Ecли пoлyчeнный тип являeтcя клaccoм Qt (QString, QDateTime и так далее), входной параметр становится ссылкой с модификатором const (например, const QString &).
Для получения списка всех свойств, сигналов и слотов, доступных в объектах QAxObject или QAxWidget вместе с их типами Qt, сделайте вызов функции QAxBase::generateDocumentation() или используйте утилиту командной строки Qt dumpdoc, расположенную в каталоге Qt tools\activeqt\dumpdoc.
Теперь продолжим рассмотрение конструктора PlayerWindow:
05 wmp->setProperty("ShowControls", false);
06 wmp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
07 connect(wmp, SIGNAL(PlayStateChange(int, int)),
08 this, SLOT(onPlayStateChange(int, int)));
09 connect(wmp, SIGNAL(ReadyStateChange(ReadyStateConstants)),
10 this, SLOT(onReadyStateChange(ReadyStateConstants)));
11 connect(wmp, SIGNAL(PositionChange(double, double)),
12 this, SLOT(onPositionChange(double, double)));
После вызова QAxWidget::setControl() мы вызываем функцию QObject::setProperty() для установки свойства ShowControls (отображать элементы управления) элемента управления Media Player системы Windows на значение false, поскольку мы предоставляем свои собственные кнопки для работы с компонентом. Функция QObject::setProperty() может использоваться как для свойств СОМ, так и для обычных свойств Qt. Ее второй параметр имеет тип QVariant.
Затем мы вызываем функцию setSizePolicy(), чтобы элемент управления ActiveX мог занять все имеющееся в менеджере компоновки пространство, и мы подсоединяем три события ActiveX компонента СОМ к трем слотам.
13 stopButton = new QToolButton;
14 stopButton->setText(tr("&Stop"));
15 stopButton->setEnabled(false);
16 connect(stopButton, SIGNAL(clicked()), wmp, SLOT(Stop()));
17 …
18 }
Остальная часть конструктора PlayerWindow следует обычному образцу, за исключением того, что мы подсоединяем некоторые сигналы Qt к слотам объекта COM (Play(), Pause() и Stop()). Мы показали здесь реализацию только кнопки Stop, поскольку другие кнопки реализуются аналогично.
01 void PlayerWindow::timerEvent(QTimerEvent *event)
02 {
03 if (event->timerId() == updateTimer) {
04 double curPos = wmp->property("CurrentPosition").toDouble();
05 onPositionChange(-1, curPos);
06 } else {
07 QWidget::timerEvent(event);
08 }
09 }
Функция timerEvent() вызывается через определенные интервалы времени во время проигрывания мультимедийного клипа. Мы используем ее для продвижения ползунка. Это делается путем вызова функции property() для элемента управления ActiveX, чтобы получить значение свойства CurrentPosition (текущая позиция) в виде объекта типа QVariant и вызова функции toDouble() для преобразования его в тип double. Мы затем вызываем функцию onPositionChange() для обновления положения ползунка.
Мы не будем рассматривать остальную часть программного кода, поскольку большая часть его не имеет непосредственного отношения к ActiveX и не содержит ничего такого, что мы уже не обсуждали ранее. Данный программный код имеется на компакт-диске.
В файле .pro нам необходимо задать элемент для связи с модулем QAxContainer.
CONFIG += qaxcontainer
При работе с объектами СОМ одной из часто возникающих потребностей является необходимость непосредственного вызова метода СОМ (вместо подсоединения его к сигналу Qt). Наиболее просто это сделать путем вызова функции QAxBase::dynamicCall() с указанием имени и сигнатуры метода в первом параметре и аргументов метода в дополнительных параметрах. Например:
wmp->dynamicCall("TitlePlay(uint)", 6);
Функция dynamicCall() принимает до восьми параметров типа QVariant и возвращает объект типа QVariant. Если нам необходимо передавать таким образом IDispatch * или IUnknown *, мы можем инкапсулировать компонент в QAxObject и вызвать для него функцию asVariant() для преобразования его в тип QVariant. Если нам необходимо вызвать метод СОМ, который возвращает IDispatch * или IUnknown *, или если нам необходимо осуществлять доступ к свойству СОМ одного из этих типов, мы можем вместо этого использовать функцию querySubObject():
QAxObject *session = outlook.querySubObject('"Session");
QAxObject *defaultContacts =
session->querySubObject("GetDefaultFolder(01DefaultFolders)",
"olFolderContacts");
Если мы собираемся вызывать методы, которые имеют неподдерживаемые типы данных в их списке параметров, мы можем использовать QAxBase::queryInterface() для получения интерфейса СОМ и непосредственного вызова метода. Мы должны вызвать функцию Release() после завершения использования интерфейса, что является обычным при работе с СОМ. Если нам приходится часто вызывать такие методы, мы можем создать подкласс QAxObject или QAxWidget и обеспечить функции—члены, которые инкапсулируют вызовы интерфейса СОМ. Однако убедитесь, что подклассы QAxObject и QAxWidget не могут определять свои собственные свойства, сигналы и слоты.
Теперь мы рассмотрим модуль QAxServer. Этот модуль позволяет нам превратить стандартную программу Qt в сервер ActiveX. Сервер может быть как совместно используемой библиотекой, так и автономным приложением. Серверы в виде совместно используемых библиотек часто называют внутрипроцессными серверами (in-process servers), а автономные приложения — внепроцессными серверами (out-of-process servers).
Наш первый пример QAxServer является внутрипроцессным сервером, отображающим виджет с шариком, который может прыгать вправо и влево. Мы рассмотрим также способы встраивания этого виджета в Internet Explorer.
Рис. 20.6. Виджет AxBouncer в Internet Explorer.
Ниже приводится начало определения класса виджета AxBouncer:
01 class AxBouncer : public QWidget, public QAxBindable
02 {
03 Q_OBJECT
04 Q_ENUMS(SpeedValue)
05 Q_PROPERTY(QColor color READ color WRITE setColor)
06 Q_PROPERTY(SpeedValue speed READ speed WRITE setSpeed)
07 Q_PROPERTY(int radius READ radius WRITE setRadius)
08 Q_PROPERTY(bool running READ isRunning)
AxBouncer наследует как QWidget, так и QAxBindable. Класс QAxBindable обеспечивает интерфейс между виджетом и клиентом ActiveX. Любой QWidget может быть экспортирован как элемент управления ActiveX, но путем создания подкласса QAxBindable мы можем уведомлять клиента об изменениях значения свойства и peализовывать интерфейсы СОМ в дополнение к уже реализованным при помощи QAxServer.
Если при использовании множественного наследования имеются классы, производные от QObject, мы должны всегда располагать производные от QObject классы первыми для того, чтобы компилятор moc мог их извлечь.
Мы объявляем три свойства для чтения и записи и одно свойство только для чтения. Макрос Q_ENUMS() необходим для указания компилятору moc на то, что SpeedValue имеет тип enum (перечисление). Это перечисление объявляется в открытой секции класса:
09 public:
10 enum SpeedValue { Slow, Normal, Fast };
11 AxBouncer(QWidget *parent = 0);
12 void setSpeed(SpeedValue newSpeed);
13 SpeedValue speed() const { return ballSpeed; }
14 void setRadius(int newRadius);
15 int radius() const { return ballRadius; }
16 void setColor(const QColor &newColor);
17 QColor color() const { return ballColor; }
18 bool isRunning() const { return myTimerId != 0; }
19 QSize sizeHint() const;
20 QAxAggregated *createAggregate();
21 public slots:
22 void start();
23 void stop();
24 signals:
25 void bouncing();
Конструктор AxBouncer является стандартным конструктором виджета с параметром parent. Макрос QAXFACTORY_DEFAULT(), который мы используем для экспорта компонента, предполагает, что у конструктора именно такая сигнатура.
Функция createAggregate() класса QAxBindable переопределяется. Мы рассмотрим ее вскоре.
26 protected:
27 void paintEvent(QPaintEvent *event);
28 void timerEvent(QTimerEvent *event);
29 private:
30 int intervalInMilliseconds() const;
31 QColor ballColor;
32 SpeedValue ballSpeed;
33 int ballRadius;
34 int myTimerId;
35 int x;
36 int delta;
37 };
Защищенная и закрытая секции этого класса имеют тот же вид, как и для стандартного виджета Qt.
01 AxBouncer::AxBouncer(QWidget *parent)
02 : QWidget(parent)
03 {
04 ballColor = Qt::blue;
05 ballSpeed = Normal;
06 ballRadius = 15;
07 myTimerId = 0;
08 x = 20;
09 delta = 2;
10 }
Конструктор AxBouncer инициализирует закрытые переменные этого класса.
01 void AxBouncer::setColor(const QColor &newColor)
02 {
03 if (newColor != ballColor &&
04 requestPropertyChange("color")) {
05 ballColor = newColor;
06 update();
07 propertyChanged("color");
08 }
09 }
Функция setColor() устанавливает значение свойства color (цвет). Она вызывает функцию update() для перерисовки виджета.
Необычной частью являются вызовы функций requestPropertyChange() и propertyChanged(). Эти функции наследуются от класса QAxBindable и в идеальном случае должны вызываться при всяком изменении свойства. Функция requestPropertyChange() спрашивает у клиента разрешение на изменение свойства и возвращает true, если клиент дает такое разрешение. Функция propertyChanged() уведомляет клиента о том, что свойство изменилось.
Устанавливающие свойства функции setSpeed() и setRadius() следуют этому же образцу, и так же работают слоты start() и stop(), поскольку они изменяют значение свойства running (приложение выполняется).
Осталось рассмотреть еще одну интересную функцию—член класса AxBouncer:
QAxAggregated *AxBouncer::createAggregate()
{
return new ObjectSafetyImpl;
}
Функция createAggregate() класса QAxBindable переопределяется. Она позволяет нам реализовать интерфейсы СОМ, которые модуль QAxServer еще не реализовал, или обойти определенные по умолчанию в QAxServer интерфейсы СОМ. Ниже мы делаем это для обеспечения интерфейса IObjectSafety, который используется в Internet Explorer для доступа к свойствам безопасности компонента. Это является стандартным способом устранения непопулярного сообщения об ошибке «Object not safe for scripting» (объект небезопасен при использовании в сценарии) в Internet Explorer.
Ниже приводится определение класса, которое реализует интерфейс IObjectSafety:
01 class ObjectSafetyImpl : public QAxAggregated, public IObjectSafety
02 {
03 public:
04 long queryInterface(const QUuid &iid, void **iface);
05 QAXAGG_IUNKNOWN
06 HRESULT WINAPI GetInterfaceSafetyOptions(REFIID riid,
07 DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions);
08 HRESULT WINAPI SetInterfaceSafetyOptions(REFIID riid,
09 DWORD pdwSupportedOptions, DWORD pdwEnabledOptions);
10 };
Класс ObjectSafetyImpl наследует как QAxAggregated, так и IObjectSafety. Класс QAxAggregated является абстрактным базовым классом, предназначенным для реализации дополнительных интерфейсов СОМ. Объект СОМ, который расширяет QAxAggregated, доступен при помощи функции controllingUnknown(). Этот объект СОМ создается незаметно для пользователя модулем QAxServer.
Макрос QAXAGG_IUNKNOWN обеспечивает стандартную реализацию функций QueryInterface(), AddRef() и Release(). В этих реализациях просто делается вызов одноименных функций для управляющего объекта СОМ.
01 long ObjectSafetyImpl::queryInterface(const QUuid &iid, void **iface)
02 {
03 *iface = 0;
04 if (iid == IID_IObjectSafety) {
05 *iface = static_cast
06 } else {
07 return E_NOINTERFACE;
08 }
09 AddRef();
10 return S_OK;
11 }
Функция queryInterface() — чистая виртуальная функция класса QAxAggregated. Она вызывается управляющим объектом СОМ для предоставления доступа к интерфейсу, который обеспечивается подклассом QAxAggregated. Мы должны возвращать E_NOINTERFACE для интерфейсов, которые мы не определили, и также для IUnknown.
01 HRESULT WINAPI ObjectSafetyImpl::GetInterfaceSafetyOptions(
02 REFIID /* riid */, DWORD *pdwSupportedOptions,
03 DWORD *pdwEnabledOptions)
04 {
05 *pdwSupportedOptions =
06 INTERFACESAFE_FOR_UNTRUSTED_DATA
07 | INTERFACESAFE_FOR_UNTRUSTED_CALLER;
08 *pdwEnabledOptions = *pdwSupportedOptions;
09 return S_OK;
10 }
11 HRESULT WINAPI ObjectSafetyImpl::SetInterfaceSafetyOptions(
12 REFIID /* riid */, DWORD /* pdwSupportedOptions */,
13 DWORD /* pdwEnabledOptions */)
14 {
15 return S_OK;
16 }
Функции GetInterfaceSafetyOptions() и SetInterfaceSafetyOptions() объявляются в IObjectSafety. Мы реализуем их, чтобы уведомить всех о том, что наш объект безопасен для использования в сценариях.
Давайте теперь рассмотрим main.cpp:
01 #include
02 #include "axbouncer.h"
03 QAXFACTORY_DEFAULT(AxBouncer,
04 "{5e2461aa-a3e8-4f7a-8b04-307459a4c08c}",
05 "{533af11f-4899-43de-8b7f-2ddf588d1015}",
06 "{772c14a5-a840-4023-b79d-19549ece0cd9}",
07 "{dbce1e56-70dd-4f74-85e0-95c65d86254d}",
08 "{3f3db5e0-78ff-4e35-8a5d-3d3b96c83e09}")
Макрос QAXFACTORY_DEFAULT() экспортирует элемент управления ActiveX. Мы можем использовать его для серверов ActiveX, которые экспортируют только один элемент управления. В следующем примере данного раздела будет показано, как можно экспортировать много элементов управления ActiveX.
Первым аргументом макроса QAXFACTORY_DEFAULT() является имя экспортируемого класса Qt. Такое же имя используется для экспорта элемента управления. Остальные пять аргументов следующие: идентификатор класса, идентификатор интерфейса, идентификатор интерфейса событий, идентификатор библиотеки типов и идентификатор приложения. Мы можем использовать стандартные инструментальные средства, например guidgen или uuidgen, для получения этих идентификаторов. Поскольку сервер реализован в виде библиотеки, нам не требуется иметь функцию main().
Ниже приводится файл .pro для внутрипроцессного сервера ActiveX:
TEMPLATE = lib
CONFIG += dll qaxserver
HEADERS = axbouncer.h \
objectsafetyimpl.h
SOURCES = axbouncer.cpp \
main.cpp \
objectsafetyimpl.cpp
RC_FILE = qaxserver.rc
DEF_FILE = qaxserver.def
Файлы qaxserver.rc и qaxserver.def, на которые имеются ссылки в файле .pro, —стандартные файлы, которые можно скопировать из каталога Qt src\activeqt\control.
Файл makefile или сгенерированный утилитой qmake файл проекта Visual С++ содержат правила для регистрации сервера в реестре Windows. Для регистрации сервера на машине пользователя мы можем использовать утилиту regsvr32, которая имеется во всех системах Windows.
Мы можем затем включить компонент Bouncer в страницу HTML, используя тег
Мы можем создать кнопку для вызова слотов:
Мы можем манипулировать виджетом при помощи языков JavaScript или VBScript точно так же, как и любым другим элементом управления ActiveX (см. расположенный на компакт-диске файл demo.html, содержащий очень простую страницу, в которой используется сервер ActiveX.
Наш последний пример — приложение Address Book (адресная книга), применяющее сценарий. Это приложение может рассматриваться в качестве стандартного приложения Qt для Windows или внепроцессного сервера ActiveX. В последнем случае мы можем создавать сценарий работы приложения, используя, например, Visual Basic.
01 class AddressBook : public QMainWindow
02 {
03 Q_OBJECT
04 Q_PROPERTY(int count READ count)
05 Q_CLASSINFO("ClassID",
06 "{588141ef-110d-4beb-95ab-ee6a478b576d}")
07 Q_CLASSINFO("InterfaceID",
08 "{718780ec-b30c-4d88-83b3-79b3d9e78502}")
09 Q_CLASSINFO("ToSuperClass", "AddressBook")
10 public:
11 AddressBook(QWidget *parent = 0);
12 ~AddressBook();
13 int count() const;
14 public slots:
15 ABItem *createEntry(const QString &contact);
16 ABItem *findEntry(const QString &contact) const;
17 ABItem *entryAt(int index) const;
18 private slots:
19 void addEntry();
20 void editEntry();
21 void deleteEntry();
22 private:
23 void createActions();
24 void createMenus();
25 QTreeWidget *treeWidget;
26 QMenu *fileMenu;
27 QMenu *editMenu;
28 QAction *exitAction;
29 QAction *addEntryAction;
30 QAction *editEntryAction;
31 QAction *deleteEntryAction;
32 };
Виджет AddressBook является главным окном приложения. Предоставляемые им свойства и слоты можно применять при создании сценария. Макрос Q_CLASSINFO() используется для определения идентификаторов класса и интерфейсов, связанных с классом. Они генерируются с помощью таких утилит, как guid или uuid.
В предыдущем примере мы определяли идентификаторы класса и интерфейса при экспорте класса QAxBouncer, используя макрос QAXFACTORY_DEFAULT(). В этом примере мы хотим экспортировать несколько классов, поэтому нельзя использовать макрос QAXFACTORY_DEFAULT(). Мы можем поступать двумя способами:
• можно создать подкласс QAxFactory, переопределить его виртуальные функции для представления информации об экспортируемых нами типах и использовать макрос QAXFACTORY_EXPORT() для регистрации фабрики классов;
• можно использовать макросы QAXFACTORY_BEGIN(), QAXFACTORY_END(), QAXCLASS() и QAXTYPE() для объявления и регистрации фабрики классов. В этом случае потребуется использовать макрос Q_CLASSINFO() для определения идентификаторов класса и интерфейса.
Вернемся к определению класса AddressBook. Третий вызов макроса Q_CLASSINFO() может показаться немного странным. По умолчанию элементы управления ActiveX предоставляют в распоряжение клиентов не только свои собственные свойства, сигналы и слоты, но и свои суперклассы вплоть до QWidget. Атрибут ToSuperClass позволяет определить суперкласс самого высокого уровня (в дереве наследования), который мы собираемся предоставить клиенту. Здесь мы указываем имя класса компонента («AddressBook») в качестве имени экспортируемого класса самого высокого уровня — это значит, что не будут экспортироваться свойства, сигналы и слоты, определенные в суперклассах AddressBook.
01 class ABItem : public QObject, public QListViewItem
02 {
03 Q_OBJECT
04 Q_PROPERTY(QString contact READ contact WRITE setContact)
05 Q_PROPERTY(QString address READ address WRITE setAddress)
06 Q_PROPERTY(QString phoneNumber
07 READ phoneNumber WRITE setPhoneNumber)
08 Q_CLASSINFO("ClassID",
09 "{bc82730e-5f39-4e5c-96be-461c2cd0d282}")
10 Q_CLASSINFO("InterfaceID",
11 "{c8bc1656-870e-48a9-9937-fbe1ceff8b2e}")
12 Q_CLASSINFO("ToSuperClass", "ABItem")
13 public:
14 ABItem(QTreeWidget *treeWidget);
15 void setContact(const QString &contact);
16 QString contact() const { return text(0); }
17 void setAddress(const QString &address);
18 QString address() const { return text(1); }
19 void setPhoneNumber(const QString &number);
20 QString phoneNumber() const { return text(2); }
21 public slots:
22 void remove();
23 };
Класс ABItem представляет один элемент в адресной книге. Он наследует QTreeWidgetItem и поэтому может отображаться в QTreeWidget, и он также наследует QObject и поэтому может экспортироваться как объект СОМ.
01 int main(int argc, char *argv[])
02 {
03 QApplication app(argc, argv);
04 if (!QAxFactory::isServer()) {
05 AddressBook addressBook;
06 addressBook.show();
07 return app.exec();
08 }
09 return app.exec();
10 }
В функции main() мы проверяем, в каком качестве работает приложение: как автономное приложение или как сервер. Опция командной строки —activex распознается объектом QApplication и обеспечивает работу приложения в качестве сервера. Если приложение не является сервером, мы создаем главный виджет и выводим его на экран, как мы обычно делаем для любого автономного приложения Qt.
Кроме опции —activex серверы ActiveX «понимают» следующие опции командной строки:
• —regserver — регистрация сервера в системном реестре;
• —unregserver — отмена регистрации сервера в системном реестре;
• —dumpidl файл — записывает описание сервера на языке IDL (Interface Description Language — язык описания интерфейсов) в указанный файл.
Когда приложение выполняет функции сервера, нам необходимо экспортировать классы AddressBook и ABItem как компоненты СОМ:
QAXFACTORY_BEGIN("{2b2b6f3e-86cf-4c49-9df5-80483b47f17b}",
"{8e827b25-148b-4307-ba7d-23f275244818}")
QAXCLASS(AddressBook)
QAXTYPE(ABItem)
QAXFACTORY_END()
Приведенные выше макросы экспортируют фабрику классов для создания объектов СОМ. Поскольку мы собираемся экспортировать два типа объектов СОМ, мы не можем просто использовать макрос QAXFACTORY_DEFAULT(), как мы делали в предыдущем примере.
Первым аргументом макроса QAXFACTORY_BEGIN() является идентификатор библиотеки типов; второй аргумент представляет собой идентификатор приложения. Между макросами QAXFACTORY_BEGIN() и QAXFACTORY_END() мы указываем все классы, которые могут быть инстанцированы, и все типы данных, доступные как объекты СОМ.
Ниже приводится файл .pro для внепроцессного сервера ActiveX:
TEMPLATE = app
CONFIG += qaxserver
HEADERS = abitem.h \
addressbook.h \
editdialog.h
SOURCES = abitem.cpp \
addressbook.cpp \
editdialog.cpp \
main.cpp
FORMS = editdialog.ui
RC_FILE = qaxserver.rc
Файл qaxserver.rc, на который имеется ссылка в файле .pro, является стандартным файлом, который может быть скопирован из каталога Qt src\activeqt\control.
Вы можете посмотреть в каталоге примеров vb проект Visual Basic, который использует сервер Address Book.
Этим мы завершаем наш обзор рабочей среды ActiveQt. Дистрибутив Qt включает дополнительные примеры, и в документации содержится информация о способах построения модулей QAxContainer и QAxServer и решения обычных вопросов взаимодействия.
Управление сеансами в системе X11
Когда мы выходим из системы X11, некоторые оконные менеджеры спрашивают нас о необходимости сохранения сеанса. Если мы отвечаем утвердительно, то при следующем входе в систему работа приложений будет автоматически возобновлена с того же экрана и, в идеальном случае, с того же состояния, которое было во время выхода из системы.
Компонент системы X11, который обеспечивает сохранение и восстановление сеанса, называется менеджером сеансов (session manager). Для того чтобы приложение Qt/X11 «осознавало» присутствие менеджера сеансов, мы должны переопределить функцию QApplication::saveState() и сохранить там состояние приложения.
Рис. 20.7. Выход из системы KDE.
Windows 2000 и XP, а также некоторые системы Unix предлагают другой механизм, который носит название «спящих процессов» (hibernation). Когда пользователь останавливает компьютер, операционная система просто выгружает оперативную память компьютера на диск и загружает ее обратно, когда компьютер «просыпается». Приложениям ничего не надо делать, и они даже могут ничего не знать об этом.
Когда пользователь инициирует завершение работы, мы можем перехватить управление непосредственно перед завершением путем переопределения функции QApplication::commitData(). Это позволяет нам сохранять измененные данные и при необходимости вступать в диалог с пользователем. Эта часть схемы управления сеансом поддерживается как в системе X11, так и в Windows.
Мы рассмотрим управление сеансом через программный код приложения Tic-Tac-Toe (крестики-нолики), которое работает под управлением менеджера сеансов. Во-первых, давайте рассмотрим функцию main():
01 int main(int argc, char *argv[])
02 {
03 Application app(argc, argv);
04 TicTacToe toe;
05 toe.setObjectName("toe");
06 app.setTicTacToe(&toe);
07 toe.show();
08 return app.exec();
09 }
Мы создаем объект Application. Класс Application наследует QApplication и переопределяет две функции commitData() и saveState() для обеспечения управления сеансом.
Затем мы создаем виджет TicTacToe, даем знать об этом объекту Application и отображаем его. Мы дали виджету TicTacToe имя «toe». Мы должны давать уникальные имена виджетам верхнего уровня, если мы хотим, чтобы менеджер сеансов мог восстановить размеры и позиции окон.
Рис. 20.8. Приложение Tic-Tac-Toe.
Ниже приводится определение класса Application:
01 class Application : public QApplication
02 {
03 Q_OBJECT
04 public:
05 Application(int &argc, char *argv[]);
06 void setTicTacToe(TicTacToe *tic);
07 void saveState(QSessionManager &sessionManager);
08 void commitData(QSessionManager &sessionManager);
09 private:
10 TicTacToe *ticTacToe;
11 };
Класс Application сохраняет указатель виджета TicTacToe в закрытой переменной.
01 void Application::saveState(QSessionManager &sessionManager)
02 {
03 QString fileName = ticTacToe->saveState();
04 QStringList discardCommand;
05 discardCommand << "rm" << fileName;
06 sessionManager.setDiscardCommand(discardCommand);
07 }
В системе X11 функция saveState() вызывается, когда менеджер сеансов собирается сохранить состояние приложения. Данная функция также имеется на других платформах, но никогда не вызывается. Параметр QSessionManager позволяет нам поддерживать связь с менеджером сеансов.
Мы начинаем с попытки сохранения виджетом TicTacToe своего состояния в некоторый файл. Затем мы задаем команду для выполнения сброса состояния менеджером сеансов. Команда сброса (discard command) — это команда, которую должен выполнять менеджер сеансов для удаления любой сохраненной ранее информации, связанной с текущим состоянием. В этом примере мы задаем ее в виде
rm файл_сеанса
где файл_сеанса — имя файла, который содержит сохраненное состояние сеанса, a rm — стандартная команда удаления файлов в системе Unix.
Менеджер сеансов имеет также команду рестарта (restart command). Эту команду менеджер сеансов должен выполнять для возобновления работы приложения. По умолчанию Qt обеспечивает следующую команду рестарта:
приложение -session идентификатор_ключ
Первая часть, приложение, извлекается из argv[0]. Идентификатор — это идентификатор сеанса, переданный менеджером сеансов; гарантированно обеспечивается его уникальность для различных приложений и различных сеансов работы одного приложения. Ключ добавляется для однозначной идентификации времени сохранения сеанса. По различным причинам менеджер сеансов может вызывать функцию saveState() несколько раз в одном сеансе, и различные состояния должны отличаться.
Из-за ограничений существующих менеджеров сеансов нам необходимо убедиться, что каталог приложения содержится в переменной среды PATH, если мы хотим обеспечить правильный рестарт приложения. В частности, если вы сами собираетесь попробовать пример TicTacToe, вы должны установить его в каталог, например, /usr/bin и вызывать его по команде tictactoe.
Для простых приложений, в том числе и для TicTacToe, мы могли бы для обеспечения команды рестарта сохранять состояние в дополнительном аргументе командной строки. Например:
tictactoe -state 0X-X0-X-0
Это избавило бы нас от сохранения данных в файле и выдачи команды сброса состояния для удаления файла.
01 void Application::commitData(QSessionManager &sessionManager)
02 {
03 if (ticTacToe->gameInProgress()
04 && sessionManager.allowsInteraction()) {
05 int r = QMessageBox::warning(ticTacToe, tr("Tic-Tac-Toe"),
06 tr("The game hasn't finished.\n"
07 "Do you really want to quit?"),
08 QMessageBox::Yes | QMessageBox::Default,
09 QMessageBox::No | QMessageBox::Escape);
10 if (г == QMessageBox::Yes) {
11 sessionManager.release();
12 } else {
13 sessionManager.cancel();
14 }
15 }
16 }
Функция commitData() вызывается, когда пользователь выходит из системы. Мы можем переопределить ее для вывода сообщения, предупреждающего пользователя о потенциальной потере данных. В используемой по умолчанию реализации закрываются все виджеты верхнего уровня, что равносильно ситуации, когда пользователь последовательно закрывает все окна, нажимая кнопку закрытия в заголовках окон. В главе 3 мы показали, как можно переопределять функцию closeEvent(), перехватывающую этот момент и выводящую на экран сообщение.
Рис. 20.9. «Вы действительно хотите завершить работу?».
Теперь давайте рассмотрим класс TicTacToe:
01 class TicTacToe : public QWidget
02 {
03 Q_OBJECT
04 public:
05 TicTacToe(QWidget *parent = 0);
06 bool gameInProgress() const;
07 QString saveState() const;
08 QSize sizeHint() const;
09 protected:
10 void paintEvent(QPaintEvent *event);
11 void mousePressEvent(QMouseEvent *event);
12 private:
13 enum { Empty = '-', Cross = 'X', Nought = '0' };
14 void clearBoard();
15 void restoreState();
16 QString sessionFileName() const;
17 QRect cellRect(int row, int column) const;
18 int cellWidth() const { return width() / 3; }
19 int cellHeight() const { return height() / 3; }
20 bool threeInARow(int row1, int col1, int row3, int col3) const;
21 char board[3][3];
22 int turnNumber;
23 };
Класс TicTacToe наследует QWidget и переопределяет функции sizeHint(), paintEvent() и mousePressEvent(). Он также обеспечивает функции gameInProgress() и saveState(), которые мы использовали в нашем классе Application.
01 TicTacToe::TicTacToe(QWidget *parent, const char *name)
02 : QWidget(parent, name)
03 {
04 clearBoard();
05 if (qApp->isSessionRestored())
06 restoreState();
07 setWindowTitle(tr("Tic-Tac-Toe"));
08 }
В конструкторе мы стираем игровое поле и, если приложение было вызвано с опцией —session, вызываем закрытую функцию restoreState() для восстановления старого сеанса.
01 void TicTacToe::clearBoard()
02 {
03 for (int row= 0; row < 3; ++row) {
04 for (int column = 0; column < 3; ++column) {
05 board[row][column] = Empty;
06 }
07 }
08 turnNumber = 0;
09 }
В функции clearBoard() мы стираем все ячейки и устанавливаем turnNumber на значение 0.
01 QString TicTacToe::saveState() const
02 {
03 QFile file(sessionFileName());
04 if (file.open(QIODevice::WriteOnly)) {
05 QTextStream out(&file);
06 for (int row = 0; row < 3; ++row) {
07 for (int column = 0; column < 3; ++column) {
08 out << board[row][column];
09 }
10 }
11 }
12 return file.fileName();
13 }
В функции saveState() мы записываем состояние игрового поля на диск. Формат достаточно простой: «X» для крестиков, «0» для ноликов и «—» для пустых ячеек.
01 QString TicTacToe::sessionFileName() const
02 {
03 return QDir::homePath() + "/.tictactoe_"
04 + qApp->sessionId() + "_" + qApp->sessionKey();
05 }
Закрытая функция sessionFileName() возвращает имя файла для текущего идентификатора сеанса и ключа сеанса. Данная функция используется как в saveState(), так и в restoreState(). Имя файла определяется на основе идентификатора сеанса и ключа сеанса.
01 void TicTacToe::restoreState()
02 {
03 QFile file(sessionFileName());
04 if (file.open(QIODevice::ReadOnly)) {
05 QTextStream in(&file);
06 for (int row = 0; row < 3; ++row) {
07 for (int column = 0; column < 3; ++column) {
08 in >> board[row][column];
09 if (board[row][column] != Empty)
10 ++turnNumber;
11 }
12 }
13 }
14 update();
15 }
В функции restoreState() мы загружаем файл восстанавливаемого сеанса и заполняем игровое поле его информацией. Мы рассчитываем значение переменной turnNumber исходя из количества крестиков и ноликов на игровом поле.
В конструкторе TicTacToe мы вызывали restoreState(), если функция QApplication::isSessionRestored() возвращала true. В этом случае sessionId() и sessionKey() возвращают именно те значения, которые были при прошлом сохранении состояния приложения, а функция sessionFileName() возвращает имя файла того сеанса.
Тестирование и отладка программного кода по управлению сеансами могут быть достаточно утомительным делом, поскольку нам приходится все время входить и выходить из системы. Один из способов, позволяющий избежать этого, заключается в применении стандартной утилиты xsm, предусмотренной в системе X11. При первом вызове xsm на экран выводятся окно менеджера сеансов и окно консольного режима. Все приложения, запускаемые с данного окна консольного режима, будут использовать xsm в качестве своего менеджера сеансов, а не стандартный общесистемный менеджер сеансов. Мы можем затем использовать окно xsm для завершения, рестарта или сброса сеанса и проконтролировать правильность поведения приложения. Подробное описание того, как это делается, вы найдете в сети Интернет по адресу .
Глава 21. Программирование встроенных систем
Разработка программного обеспечения для таких мобильных устройств, как карманные компьютеры и мобильные телефоны, может представлять собой очень сложную задачу, поскольку встроенные системы обычно имеют более медленные процессоры, меньший объем постоянной памяти (на флеш-картах или на жестких дисках), меньший объем основной памяти и дисплеи меньшего размера, чем настольные компьютеры.
Система Qtopia Core (ранее она называлась Qt/Embedded) — это версия Qt, оптимизированная для разработки встроенных систем под Linux. Qtopia Core имеет такие же утилиты и программный интерфейс, какие предусмотрены в версиях Qt для настольных компьютеров (Qt/Windows, Qt/X11 и Qt/Mac), а также дополнительно предлагает классы и утилиты, необходимые для программирования встроенных систем. Через двойное лицензирование эта система доступна как для разработок с открытым исходным кодом, так и для коммерческих разработок.
Qtopia Core может работать на любом оборудовании, функционирующем под управлением Linux (включая архитектуры Intel x86, MIPS, ARM, StrongARM, Motorola 68000 и PowerPC). Эта система имеет буфер фреймов основной памяти, отображаемой на дисплей, и поддерживает компилятор С++. В отличие от Qt/X11, она не нуждается в системе X Window; вместо этого в ней реализуется собственная оконная система (own window system — QWS), которая приводит к значительной экономии постоянной и основной памяти. Для еще большего уменьшения расхода памяти можно перекомпилировать Qtopia Core и исключить неиспользуемые возможности. Если заранее известны используемые устройством приложения и компоненты, они могут быть скомпилированы совместно в один исполняемый модуль и собраны статически с библиотеками Qtopia Core.
Кроме того, Qtopia Core использует преимущества многих функций, присущих также версиям Qt для настольных компьютеров, в частности широко применяется неявное совместное использование данных («копирование при записи») как метод экономии основной памяти, поддерживаются пользовательские стили виджетов с помощью класса QStyle и обеспечивается система компоновки виджетов, позволяющая максимально использовать пространство экрана.
Qtopia Core представляет собой базовый компонент, на котором строятся другие предложения по встроенным системам компании «Trolltech»; к ним относятся Qtopia Platform, Qtopia PDA и Qtopia Phone. Они содержат классы и приложения, специально предназначенные для мобильных устройств и способные интегрироваться с некоторыми виртуальными машинами Java независимых разработчиков.
Первое знакомство с Qtopia
Приложения Qtopia Core могут разрабатываться на любой платформе, позволяющей запускать цепочки многоплатформенных инструментальных средств. Наиболее распространено построение кросс-компилятора GNU С++ в системе Unix. Этот процесс упрощается благодаря наличию скрипта и набора пакетов обновлений Дана Кегеля (Dan Kegel), доступного на веб-странице . Поскольку Qtopia Core имеет программный интерфейс Qt, в большинстве разработках, как правило, можно использовать версию Qt для настольных компьютеров, например Qt/X11 или Qt/Windows.
Система конфигурации Qtopia Core поддерживает кросс-компиляторы с помощью опции —embedded скрипта configure. Например, для построения ARM—архитектуры мы могли бы ввести команду
./configure -embedded arm
Можно создавать пользовательские конфигурации путем добавления новых файлов в каталог Qt mkspecs/qws.
Qtopia Core рисует непосредственно в буфере фреймов системы Linux (область основной памяти, связанная с дисплеем). Для обращения к буферу фреймов, возможно, потребуется получить разрешение для записи на устройство /dev/fb0.
Для выполнения приложений Qtopia Core сначала необходимо запустить один процесс, выполняющий функции сервера. Этот сервер отвечает за распределение между клиентами областей экрана и за генерацию событий мышки и клавиатуры. Любое приложение Qtopia Core может стать сервером, если в командной строке указать опцию —qws или в качестве третьего параметра конструктора QApplication передать QApplication::GuiServer.
Клиентские приложения связываются с сервером Qtopia Core при помощи совместно используемой области в основной памяти. Внутренне операции рисования реализованы так, что клиенты рисуют самих себя в совместно используемой области памяти и отвечают за оформление собственных окон. Это сводит к минимуму объем данных, передаваемых между клиентами и сервером, и в результате интерфейс пользователя работает без задержек. Приложения Qtopia Core обычно используют рисовальщик QPainter для рисования самих себя, но они могут также получать непосредственный доступ к видеооборудованию, используя класс QDirectPainter.
Клиенты могут связываться друг с другом при помощи протокола QCOP. Клиент может прослушивать именованный канал, создавая объект QCopChannel и устанавливая связь с его сигналом received(). Например:
QCopChannel *channel = new QCopChannel("System", this);
connect(channel, SIGNAL(received(const QString &, const QByteArray &)),
this, SLOT(received(const QString &, const QByteArray &)));
Сообщение QCOP состоит из имени и необязательного массива QByteArray. Статическая функция QCopChannel::send() передает в широковещательном режиме сообщение по каналу. Например:
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out << QDateTime::currentDateTime();
QCopChannel::send("System", "clockSkew(QDateTime)", data);
Предыдущий пример иллюстрирует общий прием: для кодирования данных используется поток QDataStream, и для гарантирования правильной интерпретации получателем массива формат данных в сообщении принимает вид функции С++.
На работу приложений Qtopia Core влияют различные переменные среды. Наиболее важными являются QWS_MOUSE_PROTO и QWS_KEYBOARD, которые определяют тип устройства мышки и клавиатуры. Полный список переменных среды приводится на веб-странице .
Если в качестве платформы разработки используется Unix, приложение можно тестировать с использованием виртуального буфера фреймов Qtopia (qvfb) — приложения X11, которое имитирует пиксель за пикселем реальный буфер фреймов. Это значительно сокращает цикл разработки. Для включения поддержки в Qtopia Core виртуального буфера необходимо передать опцию —qvfb скрипту configure. Следует помнить, что эта опция не предназначена для промышленного применения. Приложение виртуального буфера фреймов располагается в каталоге tools/qvfb и может вызываться следующим образом:
qvfb -width 320 -height 480 -depth 32
Другой опцией, работающей на большинстве платформ, является VNC (Virtual Network Computing — вычисление в виртуальной сети), которая используется для удаленного выполнения приложения. Для включения поддержки VNC в Qtopia Core передайте опцию —qt—gfx—vnc в скрипт configure. Затем запустите ваше приложение Qtopia Core с опцией командной строки —display VNC:0 и клиента VNC, ссылающегося на хост, на котором выполняется ваше приложение. Размер экрана и разрядность цвета можно установить с помощью переменных среды QWS_SIZE и QWS_DEPTH на хосте, на котором выполняются приложения Qtopia Core (например, QWS_SIZE=320x480 и QWS_DEPTH=32).
Настройка Qtopia Core
При установке Qtopia Core можно указать функции, которые мы хотим устранить, чтобы снизить расход памяти. В состав Qtopia Core входит сотня конфигурируемых функций, каждой из которых соответствует какой-то препроцессорный символ. Например, QT_NO_FILEDIALOG исключает класс QFileDialog из библиотеки QtGui, a QT_NO_I18N удаляет всю поддержку интернационализации. Эти функции перечислены в файле src/corelib/qfeatures.txt.
Qtopia Core содержит пять примеров конфигурации (minimum, small, medium, large и dist), которые находятся в файлах src/corelib/qconfig_xxx.h. Эти конфигурации можно задавать, используя опции —qconfig xxx для скрипта configure, например:
./configure -qconfig small
Для создания пользовательских конфигураций можно вручную создать файл qconfig—xxx.h и использовать его, как будто он определяет стандартную конфигурацию. Можно поступить по-другому — использовать графическую утилиту qconfig, расположенную в подкаталоге Qt tools.
Qtopia Core предоставляет следующие классы для интерфейса с входными и выходными устройствами и для настройки пользовательского интерфейса оконной системы:
• QScreen — драйверы экрана,
• QScreenDriverPlugin — подключаемые модули драйверов экрана,
• QWSMouseHandler — драйверы мышки,
• QMouseDriverPlugin — подключаемые модули драйверов мышки,
• QWSKeyboardHandler — драйверы клавиатуры,
• QKbdDriverPlugin — подключаемые модули драйверов клавиатуры,
• QWSInputMethod — методы ввода,
• QDecoration — стили оформления окон,
• QDecorationPlugin — подключаемые модули стилей оформления окон.
Для получения списка заранее определенных драйверов, методов ввода и стилей оформления экрана запустите скрипт configure с опцией —help.
Драйвер экрана можно задать с помощью опции командной строки —display при запуске сервера Qtopia Core, как это было показано в предыдущем разделе, или путем установки переменной среды QWS_DISPLAY. Драйвер мышки и связанное с ним устройство можно задавать, используя переменную среды QWS_MOUSE_PROTO, значение которой задается в виде тип:устройство, где тип — один из поддерживаемых драйверов, а устройство — путь к устройству (например, QWS_MOUSE_PROTO=IntelliMouse:/dev/mouse). Клавиатуры задаются аналогично при помощи переменной среды QWS_KEYBOARD. Методы ввода и оформления окон устанавливаются программно в сервере при помощи функций QWSServer::setCurrentInputMethod() и QApplication::qwsSetDecoration().
Стили оформления окон можно задавать отдельно от стиля виджетов, который наследует класс QStyle. Например, вполне допускается установить Windows в качестве стиля оформления окон и Plastique в качестве стиля виджетов. При желании для каждого окна можно задавать свой стиль оформления.
Класс QWSServer содержит различные функции по настройке оконной системы. Приложения, функционирующие как сервер Qtopia Core, могут получать доступ к уникальному экземпляру QWSServer через глобальную переменную qwsServer, которую инициализирует конструктор QApplication.
Qtopia Core поддерживает следующие форматы шрифтов: TrueType (TTF), PostScript Type 1, Bitmap Distribution Format (BDF) и Qt Prerendered Fonts (QPF).
Поскольку QPF является растровым форматом, он быстрее и компактнее, чем такие векторные форматы, как TTF и PostScript Туре 1, если требуется использовать только один или два различных размера. Утилита makeqpf позволяет воспринимать файлы TTF или PostScript Туре 1 и сохранять результат в формате QPF. Можно поступить по-другому — запустить наши приложения с опцией командной строки —savefonts.
На момент написания книги компания «Trolltech» разрабатывает дополнительный уровень, расположенный над Qtopia Core и позволяющий еще быстрее и удобнее разрабатывать приложения для встроенных систем. Можно надеяться, что следующее издание данной книги будет содержать больше информации по этому вопросу.