Журнал LinuxFormat - перейти на главную

LXF107:Qt4

Материал из Linuxformat
Перейти к: навигация, поиск
Программирование в стиле Qt Осваиваем технологии, лежащие в основе нашумевшего KDE4

Содержание

Обмен сообщениями

ЧАСТЬ 6 За последние годы D-Bus стал стандартом де-факто для межпроцессного взаимодействия на рабочем столе Linux. Что здесь может предложить Qt? Разбирается Андрей Боровский.


В отличие от возможностей Qt 4.x, с которыми мы познакомились ранее, появившийся в Qt 4.2 модуль QtDBus присутствует только в версиях Qt, предназначенных для Unix-систем. Объясняется это, конечно же, тем, что полнофункциональной версии D-Bus для Windows не существует (хотя работы в этом направлении ведутся). Мы уже рассматривали архитектуру шины D-Bus в LXF99, и там была высказана мысль, что программуклиент D-Bus, которая только обращается к сервисам другого приложения, нетрудно написать даже на «голом» C. С сервером дела обстоят сложнее: он должен уметь обрабатывать сообщения D-Bus, поступающие асинхронно. QtDBus упрощает решение этой задачи настолько, насколько это вообще возможно. В качестве демонстрации возможностей QtDBus мы напишем программу-сервер, которая будет предоставлять доступ к буферу обмена X-Window консольным приложениям. Во времена господства MS-DOS и Windows 3.1 существовал DOSовский текстовый редактор (название я, к сожалению, уже не помню), который, будучи запущен в DOS-окне Windows, мог обмениваться данными с буфером обмена Windows. Делалось это с помощью какого-то хитроумного прерывания, и мне тогда казалось, что это очень круто. Нечто подобное, только для X, мы сейчас и напишем.

Программа-сервер

Каждое приложение-сервер D-Bus должно предоставлять как минимум один интерфейс (то есть описание набора методов, к которым можно обратиться при помощи D-Bus из другого приложения). Интерфейсы D-Bus предоставляются объектами. И если на уровне D-Bus объект – понятие скорее условное, то при программировании сервера D-Bus в Qt 4 самый простой способ объявить интерфейс заключается в том, чтобы реализовать класс, предоставляющий требуемый интерфейс, а затем создать объект этого класса и зарегистрировать его в качестве поставщика интерфейса. Рассмотрим объект, реализующий интерфейс нашего приложениясервера (полный текст вы найдете на диске в архиве clipboarviewer.tar.gz):

 #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;
  };

Помимо прочего, мы включаем в исходный текст заголовочный файл QtDBus. Кстати, не забудьте добавить строку

QT += D-Bus

в .pro-файл вашего приложения. Основой всех классов, реализующих интерфейсы D-Bus, должен быть класс QDBusAbstractAdaptor. От него и происходит наш класс QCBAdapter. Макрос Q_CLASSINFO() позволяет определить имя экспортируемого интерфейса в формате, принятом в D-Bus (один класс может экспортировать несколь- ко интерфейсов). Интерфейс нашего класса состоит из свойства cbContent, позволяющего получить доступ к текстовому содержимому буфера обмена (если таковое имеется) и вспомогательных функций: content() – для чтения содержимого буфера обмена и setContent() – для его записи. Обратите внимание на то, что оба метода помечены макросом Q_INVOKABLE. Это необходимо для того, чтобы программы, которые не умеют выполнять маршаллинг свойств интерфейса, могли обратиться к ним напрямую. Остальные элементы класса QCBAdapter не должны вызвать вопросов. Получив указатель на глобальный объект класса QClipboard, мы манипулируем содержимым буфера обмена с помощью методов QClipboard::setText() и clear(). Наш интерфейс предназначен для чтения и записи только текстовой информации, но консольной программе, скорее всего, большего и не потребуется.

Перейдем теперь к функции 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("Не могу установить соединение\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 в архиве clipclients.tar.gz, записывает строку текста в буфер обмена X и распечатывает ее же в окне консоли. Для компиляции программы следует использовать команду

 gcc sendcontent.c `pkg-config --cflags dbus-1` `pkg-config --libs dbus-1` -o sendcontent


Разбирать подробно работу программы sendcontent мы не будем, так как она не имеет отношения к Qt, и, к тому же, фактически представляет собой сокращенный вариант примера из статьи, опубликованной в LXF99. Заметим только, что функция dbus_message_new_method_call() использует имя соединения, путь к объекту и имя сервиса, заданные нами в программе-сервере.

Запустите программу ClipBoardViewer в окне консоли. Затем в другом окне запустите программу sendcontent. После этого вы сможете вставить переданную консольной программой sendcontent строку в буфер обмена X-Window (рис. 1).

Базовый клиент D-Bus

Теперь посмотрим, как можно решить ту же задачу базовыми средствами 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 += D-Bus

следует добавить также

 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).

Использование класса QDBusInterface

Вы, наверное, заметили, что в отношении работы с 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).

Программа copypaste взаимодействует с D-Bus с помощью объекта класса 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. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика