В отличие от модулей Qt 4.x, с которыми мы познакомились ранее, появившийся в Qt 4.2 модуль QtDBus присутствует только в версиях Qt, предназначенных для Unix-систем. Объясняется это, конечно же, тем, что полнофункциональной версии D-Bus для Windows не существует (хотя работы в этом направлении ведутся). Я уже рассказывал на страницах журнала об основных принципах устройства шины D-Bus. Тем, кто не знаком с D-Bus, советую перечитать ту статью. В ней я писал о том, что хотя приложение-клиент D-Bus, которое только обращается к сервисам другого приложения, нетрудно написать, даже используя «голый» C и библиотеки D-Bus, написать приложение-сервер значительно сложнее, поскольку сервер должен обрабатывать сообщения D-Bus, поступающее асинхронно. QtDBus упрощает решение этой задачи настолько, насколько это вообще возможно. В качестве демонстрации возможностей QtDBus мы напишем программу-сервер, которая сможет предоставлять доступ к буферу обмена X-Window консольным приложениям.
Во времена господства MS-DOS и Windows 3.1 существовал досовский текстовый редактор (название я, к сожалению, уже не помню), который, будучи запущен в DOS-окне Windows, мог обмениваться данными с буфером обмена Windows. Делалось это с помощью какого-то хитроумного прерывания, и мне тогда казалось, что это очень круто. Нечто подобное, только для X, мы напишем и сейчас.
Прежде чем прикасаться к QtDBus, отметим, что фирменная документация по QtDBus зияет пробелами (чего не скажешь о других модулях). Это тем более странно, что QtDBus появился в Qt 4 уже давно. Нет, описания классов модуля QtDBus присутствуют и довольно подробны, но, вот, например, демонстрационное приложение, которое описано на странице http://doc.trolltech.com/4.4/qdbusadaptorexample.html, не только не работает, но даже и не компилируется (текст этого примера не менялся, кстати, с версии Qt 4.2).
Каждое приложение-сервер D-Bus должно предоставлять как минимум один интерфейс (то есть описание набора методов, к которым можно обратиться при помощи D-Bus из другого приложения). Интерфейсы D-Bus предоставляются объектами. И если на уровне D-Bus объект – понятие скорее условное, то при программировании сервера D-Bus в Qt 4 самый простой способ объявить интерфейс заключается в том, чтобы объявить класс, предоставляющий требуемый интерфейс, а затем создать объект этого класса и зарегистрировать его в качестве поставщика интерфейса. Рассмотрим объект, реализующий интерфейс нашего приложения сервера (полный текст приложения вы найдете по ссылке в концеке страницы):
#include <QApplication>
#include <QtCore>
#include <QtDBus>
#include <QClipboard>
class QCBAdapter: public QDBusAbstractAdaptor
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "DBus.Manager.QClipboard")
Q_PROPERTY(QString cbContent READ content WRITE setContent)
private:
QApplication *app;
public:
QCBAdapter(QApplication *application)
: QDBusAbstractAdaptor(application)
{
cb = QApplication::clipboard();
}
Q_INVOKABLE QString content()
{
printf("Запрос содержимого буфера обмена\n");
return cb->text();
}
Q_INVOKABLE void setContent(const QString &newContent)
{
printf("Содержимое буфера обмена изменено\n");
cb->setText(newContent);
}
public slots:
Q_NOREPLY void emptyClipboard()
{
cb->clear();
}
private:
QClipboard * cb;
};
Кстати, не забудьте добавить строку
QT += DBus
в файл pro вашего приложения.
Основой всех классов, реализующих D-Bus-интерфейсы, должен быть класс QDBusAbstractAdaptor. От него и происходит наш класс QCBAdapter. Макрос Q_CLASSINFO() позволяет определить имя экспортируемого интерфейса в формате, принятом в D-Bus (один класс может экспортировать несколько интерфейсов). Интерфейс нашего класса состоит из свойства cbContent, позволяющего получить доступ к текстовому содержимому буфера обмена (если таковое имеется) и вспомогательных функций content() – для чтения содержимого буфера обмена и setContent() – для записи. Обратите внимание на то, что оба метода помечены макросом Q_INVOKABLE. Это необходимо для того, чтобы программы, которые не умеют выполнять маршаллинг свойств интерфейса, могли обратиться к этим функциям напрямую. Остальные элементы класса QCBAdapter не должны вызвать вопросов. Получив указатель на глобальный объект класса QClipboard, мы манипулируем содержимым буфера обмена с помощью методов этого объекта. Наш интерфейс предназначен только для чтения записи текстовой информации, но консольной программе, скорее всего, ничего большего и не нужно.
Перейдем теперь к функции main(), которая создает и регистрирует объект, реализующий интерфейс.
#include "QCBAdapter.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QCBAdapter * adapter = new QCBAdapter(&app);
QDBusConnection connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus, "DBus.Manager.QClipboard");
if (connection.isConnected())
printf("Соединение установлено\n");
if (!connection.registerService("DBus.Manager.QClipboard")) {
printf("Не могу зарегистрировать сервис\n");
exit(1);
}
if (!connection.registerObject("/QClipboard", adapter, QDBusConnection::ExportAllContents)) {
printf("Не могу зарегистрировать объект\n");
exit(1);
}
app.exec();
}
Прежде всего, обратите внимание на то, что в нашей программе отсутствуют графические элементы. Они нам и не нужны - программа ClipboardViewer должна выполняться как сервер. При этом следует учесть, что хотя программа ClipboardViewer несздает никаких окон, она связывается с X-сервером (иначе как бы она смогла получить доступк буферу обмена), а значит является полноценным приложением X. После того как мы создали объекты классов QApplication и QCBAdapter, мы должны создать соединение с демоном D-Bus. Напомню, что в рамках архитектуры D-Bus любые две программы могут создать собственную шину D-Bus, однако существуют две стандартные шины – системная шина System Bus и пользовательская шина Session Bus. Наш сервер буфера обмена проще всего подключить к пользовательской шине. Соединение Qt-программы с D-Bus инкапсулируется объектом класса QDBusConnection. Мы создаем этот объект с помощью статического метода connectToBus(). Вторым аргументом этого метода должно быть имя соединения, которое программы-клиенты будут использовать для подключения к нашему серверу. Напомню, что по своей структуре это имя напоминает доменное имя Интернет, и если бы у нашей программы был свой сайт, доменной имя сайта можно было бы включить в имя соединения. Многие имена соединений, не связанные с сайтами, все равно начинаются с префиксов org.* или com.*, но, как мы покажем на практике, следование этому правилу вовсе не является обязательным.
После того как соединение с демоном установлено, мы можем зарегистрировать имя сервиса, предоставляемого нашим сервером. Это имя имеет ту же структуру, что и имя соединения и может совпадать с ним (обратите внимание, что имя сервиса должно совпадать с именем интерфейса, указанным в объявлении класса адаптера с помощью Q_CLASSINFO()). Как вы уже догадались, регистрация сервиса выполняется методом registerService() объекта класса QDBusConnection.
Нам осталось зарегистрировать объект, реализующий сервис. Регистрация объекта выполняется методом registerObject(). Первый аргумент метода – путь к объекту, который используется системой D-Bus для идентификации объекта. Выбор пути "/QClipboard" – чисто произвольный. Мы можем указать любое другое значение, например, "/the/longest/path/to/the/object". Последний аргумент метода registerObject() определяет, какие элементы класса адаптера войдут в описание интерфейса D-Bus и станут доступны удаленному приложению. По сути дела мы сталкиваемся здесь с той же проблемой видимости элементов класса за пределами приложения, что и в случае со сценариями Qt. Константа QDBusConnection::ExportAllContents делает класс-адаптер максимально видимым для удаленных приложений, но даже при использовании этой константы другим программам станет доступно не так уж и много. Например, программа-клиент D-Bus сможет обращаться только к методам, помеченным как Q_INVOKABLE. Если все прошло успешно, нам остается запустить цикл обработки сообщений нашего приложения.
Настало время написать программу-клиент для взаимодействия с сервером. Для того чтобы продемонстрировать универсальный характер D-Bus, напишем сначала программу на языке C. Эта программа, естественно, не может использовать средства Qt:
#include <stdio.h>
#include <dbus/dbus.h>
int main (int argc, char **argv)
{
DBusConnection * connection;
DBusError error;
DBusMessage *call; char * text = "Этот текст будет передан в буфер обмена X-Window";
dbus_error_init(&error);
connection = DBus_bus_get(DBUS_BUS_SESSION, &error);
if (!connection) {
printf("Не могу установить соединение: %s\n", error.message);
dbus_error_free(&error); return 1;
}
call = DBus_message_new_method_call("DBus.Manager.QClipboard", "/QClipboard", "DBus.Manager.QClipboard", "setContent");
dbus_message_append_args (call, DBus_TYPE_STRING, &text, DBus_TYPE_INVALID);
if (!dbus_connection_send(connection, call, NULL))
printf("Не могу отправить сообщение\n");
dbus_connection_flush(connection);
printf("%s\n", text);
dbus_message_unref(call);
dbus_connection_unref(connection);
return 0;
}
Программа, исходный текст которой вы найдете в файле sendcontent.c, записывает строку текста в буфер обмена X, и распечатывает эту же строку в окне консоли. ля компиляции программы следует использовать команду
gcc sendcontent.c `pkg-config --cflags
dbus-1` `pkg-config --libs dbus-1` -o sendcontent
Разбирать подробно работу программы sendcontent мы не будем, так как эта программа не имеет отношения к Qt, и, к тому же, фактически представляет собой сокращенный вариант программы из статьи, посвященной D-Bus. Заметим только, что функция DBus_message_new_method_call() использует имя соединения, путь к объекту, имя сервиса, заданные нами в программе сервере.
Запустите программу ClipBoardViewer в окне консоли. Затем в другом окне запустите программу sendcontent. После этого вы сможете вставить переданную консольной программой sendcontent строку в буфер обмена X-Window (рис. 1).
Рисунок 1. Консольный клиент D-Bus
Теперь посмотрим, как можно решить ту же задачу базовыми средствами Qt. Мы напишем настоящую консольную Qt-программу. У консольной Qt-программы нет объекта QApplication, нет доступа к объекту QClipboard, а значит, для передачи данных в буфер обмена X ей придется воспользоваться нашим сервером. Ниже приводится текст программы sendlines (возможно, это первая консольная программа Qt, которую вы видите).
#include <stdio.h>
#include <QtCore>
#include <QtDBus>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QDBusConnection connection = QDBusConnection::connectToBus(QDBusConnection::SessionBus, "example.sendlines");
if (connection.isConnected())
printf("Соединение установлено\n");
else {
printf("Не могу установить соединение\n");
return 1;
}
while(true) {
QString string = "";
int ch;
while ((ch = getchar()) != '\n')
string += ch;
QDBusMessage msg = QDBusMessage::createMethodCall("DBus.Manager.QClipboard", "/QClipboard", "DBus.Manager.QClipboard", "setContent");
QList<QVariant> args;
args.append(QVariant(string));
msg.setArguments(args);
connection.send(msg);
QCoreApplication::processEvents();
}
}
Модуль QtGUI включается по умолчанию во все проекты Qt 4, поэтому при создании консольного приложения Qt в файл pro помимо строки
QT += DBus
следует добавить строку
QT –= gui
Теперь компоненты графического приложения нашему проекту недоступны. Вместо класса QApplication мы используем класс QCoreApplication. Вместо вызова метода exec() объекта QApplication мы «вручную» создаем цикл обработки событий с помощью статического метода QCoreApplication::processEvents(). Но самое интересное в нашей программе, конечно, не это.
Прежде всего, нашей программе понадобится объект QDBusConnection, который мы создаем точно так же, как и в программе ClipboardViewer. Далее наша программа входит в цикл ввода строк (из которого ее можно вывести только с помощью клавиш Ctrl-C). После того как пользователь завершает ввод очередной строки создается объект msg класса QDBusMessage. Этот объект инкапсулирует сообщение D-Bus. Мы создаем объект-сообщение с помощью статического метода createMethodCall(). В результате созданный объект инкапсулирует сообщение типа «вызов метода». Аргументами метода createMethodCall() должны быть соответственно имя сервиса, путь к объекту, имя интерфейса и имя вызываемого метода.
Те, кто читал статью, посвященную D-Bus, должны помнить, что с сообщениями этого типа обычно связываются дополнительные данные, представляющие собой значения аргументов вызываемого метода удаленного объекта. Значение аргументов добавляются в объект msg с помощью метода setArguments(). Аргументом этого метода должен быть список QList, состоящий из элементов типа QVariant. Мы создаем такой список с одним единственным элементом (соответствующим единственному аргументу метода setContent), вызываем метод setArguments(), после чего отправляем созданное сообщение с помощью метода send() объекта класса QDBusConnection. Теперь мы можем передать в буфер обмена X-Window серию строк, что можно проследить, например, с помощью апплета Klipper (рис. 2).
Рисунок 2. Работа с буфером обмена из консольнгой программы Qt
Вы, наверное, заметили, что в отношении работы с D-Bus программа sendlines не проще, чем программа, написанная на языке C. Пришло время показать, как Qt действительно может упростить работу с D-Bus. В идеале нам просто хотелось бы иметь класс с тем же набором методов, что и у интерфейса удаленного приложения, так чтобы при вызове метода локального класса вызывался соответствующий метод интерфейса удаленного приложения. В некоторых случаях этот идеал достижим, причем без особых усилий. Это возможно, если у нас есть описание интерфейса на языке XML. Сейчас мы рассмотрим более общий вариант решения:
#include <QtDebug>
#include <QtCore>
#include <QtDBus>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QDBusInterface clipboard("DBus.Manager.QClipboard", "/QClipboard", "DBus.Manager.QClipboard");
int ch = 0;
while ((ch = getchar()) != 'q') {
if (ch == 'c')
clipboard.call("setContent", "Data sent from console");
if (ch == 'p') {
QDBusReply<QString> reply = clipboard.call("content");
if (reply.isValid())
qDebug() << reply.value();
else
qDebug() << "Error calling content()";
}
QCoreApplication::processEvents();
}
}
Программа copypaste, текст которой приводится выше, представляет собой консольное приложение Qt, которое может вставлять строки в буфер обмена и считывать строки из него. Команда «c» заставляет программу копировать в буфер заданную строку текста, а команда «p» позволяет программе прочитать содержимое буфера обмена (рис. 3).
Рисунок 3. Передача текста между буфером обмена и консольной программой Qt
Программа copypaste взаимодействует с D-Bus с помощью объекта класса QDBusInterface. Класс QDBusInterface предназначен для взаимодействия с интерфейсами удаленных приложений. Этот класс не делает ничего «волшебного». По сути, его методы просто объединяют некоторые рутинные операции, которые нам приходится выполнять, когда мы имеем дело непосредственно с классами QDBusConnection и QDBusMessage, однако удобство работы значительно возрастает. Самым часто используемым методом класса QDBusInterface является метод call(), предназначенный для вызова методов интерфейса удаленного приложения. В первом параметре метода call() передается строка с именем вызываемого удаленного метода. Далее следуют 8 параметров типа «ссылка на QVariant» со значениями, присвоенными по умолчанию. Эти параметры используются для передачи аргументов вызываемому методу. Такое решение может показаться не очень элегантным, но оно заметно упрощает работу с удаленными методами. Если у метода удаленного интерфейса меньше 8 аргументов (а таких методов, разумеется, большинство), мы избавлены от необходимости конструирования списка аргументов на основе QList. Например, в строке
clipboard.call("setContent", "Data sent from console");
мы вызываем метод setContent с аргументом "Data sent from console". Если же вызываемый метод имеет более восьми аргументов, к нашим услугам метод QDBusInterface::callWithArgumentList(), которому аргументы вызываемого удаленного метода передаются в виде списка.
Метод call() так же используется для вызова удаленных методов, возвращающих значения. При вызове такого метода приложению-клиенту посылается сообщение определенного типа, которое, помимо прочего, содержит результат вызова метода. Ситуация осложняется тем, что при вызове удаленного метода может возникнуть ошибка. В этом случае клиенту посылается сообщение другого типа. Для обработки сообщений, возвращаемых удаленными методами, используется шаблон QDBusReply. Параметром шаблона является тип значения, возвращаемого методом. Метод content(), который позволяет прочитать содержимое буфера обмена, возвращает значение типа QString, поэтому объект reply, инкапсулирующий результат вызова метода, имеет тип QDBusReply<QString>. Проверить, не возникло ли в процессе вызова метода ошибки можно с помощью метода isValid() объекта reply.
В заключение рассмотрим еще одну интересную возможность модуля QtDBus. В статье, посвященной D-Bus, я писал о сообщениях-сигналах и сравнивал их с сигналами Qt. Сравнение было более чем уместно, так как в QtDBus действительно существует возможность связывать слот объекта локального приложения с сигналом удаленного приложения. Таким образом, наше приложение получает возможность обрабатывать сигналы, эмитируемые удаленным приложением. Напомню, что эти сигналы могут быть широковещательными, то есть адресованными не только нашей программе.
Для связывания удаленного сигнала с текущим слотом можно воспользоваться методом connect() класса QDBusConnection. Первые три аргумента метода – соответственно имя сервиса, путь к объекту и имя интерфейса. Далее нужно указать имя сигнала, передать указатель на объект-приемник и строку с именем слота, который будет вызываться в ответ на сигнал. Для отсоединения слота от удаленного сигнала служит метод disconnect() того же класса QDBusConnection.
На этом мы заканчиваем обзор новшеств Qt 4.x и переходим на следующий этаж популярной графической системы Linux – KDE 4.x
Исходные тексты примеров (часть 1) Исходные тексты примеров (часть 2)
Статья впервые опубликована в журнале Linux Format
© 2008 Андрей Боровский <anb @ symmetrica.net>