18.2. ActiveX.

Технология Microsoft ActiveX позволяет приложениям включать интерфейсные компоненты, предоставляемые другими приложениями и библиотеками. Она базируется на технологии Microsoft COM и определяет один набор интерфейсов для приложений, которые которые используют внешние компоненты, и другой набор -- для приложений и библиотек, которые могут предоставить эти компоненты.

Версия библиотеки Qt/Windows Enterprise Edition, включает в себя ActiveQt framework, которая позволяет объединить ActiveX и Qt. ActiveQt состоит из двух модулей:

В нашем первом примере мы встроим Windows Media Player в приложение Qt, используя для этого QAxContainer.

Рисунок 18.2. Внешний вид приложения Media Player.


Главное окно приложения относится к классу PlayerWindow: class PlayerWindow : public QWidget { Q_OBJECT Q_ENUMS(ReadyStateConstants) public: enum PlayStateConstants { Stopped = 0, Paused = 1, Playing = 2 }; enum ReadyStateConstants { Uninitialized = 0, Loading = 1, Interactive = 3, Complete = 4 }; PlayerWindow(QWidget *parent = 0, const char *name = 0); protected: void timerEvent(QTimerEvent *event); private slots: void onPlayStateChange(int oldState, int newState); void onReadyStateChange(ReadyStateConstants readyState); void onPositionChange(double oldPos, double newPos); void sliderValueChanged(int newValue); void openFile(); Класс PlayerWindow порожден от класса QWidget. Макрос Q_ENUMS() используется для того, чтобы сообщить утилите moc о том, что тип ReadyStateConstants, используемый слотом onReadyStateChange(), относится к перечислениям. private: QAxWidget *wmp; QToolButton *openButton; QToolButton *playPauseButton; QToolButton *stopButton; QSlider *seekSlider; QString fileFilters; int updateTimer; }; В приватной секции объявлена переменная-член типа QAxWidget *. PlayerWindow::PlayerWindow(QWidget *parent, const char *name) : QWidget(parent, name) { ... wmp = new QAxWidget(this); wmp->setControl("{22D6F312-B0F6-11D0-94AB-0080C74C7E95}"); В конструкторе создается объект QAxWidget, в который будет внедрен элемент управления ActiveX -- Windows Media Player. Модуль QAxContainer содержит три класса: QAxObject -- инкапсулирующий COM-объект, QAxWidget -- инкапсулирующий элемент ActiveX и QAxBase, реализующий базовую функциональность классов QAxObject и QAxWidget.

Рисунок 18.3. Дерево наследования в модуле QAxContainer.


Методу setControl(), объекта QAxWidget, передается идентификатор (GUID) элемента управления Windows Media Player 6.4. С его помощью будет создан экземпляр требуемого компонента, после чего все свойства, события и методы элемента ActiveX станут доступны как обычные свойства, сигналы и слоты Qt объекта QAxWidget.

Типы COM автоматически конвертируются в соответствующие типы Qt, в соответствии с таблицей, приведенной на рисунке 18.4. Так например, входной параметр типа VARIANT_BOOL преобразуется в тип bool, а выходной VARIANT_BOOL -- в bool &. Если результатом преобразования является класс Qt (QString, QDateTime и т.п.), то входной параметр получает тип константной ссылки на этот класс (например const QString &).

Рисунок 18.4. Отношения между типами COM и Qt.


Чтобы получить список всех свойств, сигналов и слотов, доступных в QAxObject или QAxWidget, вызовите generateDocumentation() или воспользуйтесь утилитой командной строки dumpdoc, размещенной в подкаталоге extensions\activeqt\example. wmp->setProperty("ShowControls", QVariant(false, 0)); wmp->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(wmp, SIGNAL(PlayStateChange(int, int)), this, SLOT(onPlayStateChange(int, int))); connect(wmp, SIGNAL(ReadyStateChange(ReadyStateConstants)), this, SLOT(onReadyStateChange(ReadyStateConstants))); connect(wmp, SIGNAL(PositionChange(double, double)), this, SLOT(onPositionChange(double, double))); Вслед за функцией setControl(), в конструкторе PlayerWindow, вызывается setProperty(), чтобы записать значение false в свойство ShowControls плеера, поскольку приложение предоставляет свои элементы управления. Функция setProperty() реализована в классе QObject и может использоваться для установки значений свойств как в COM-объектах, так и в обычных классах Qt. Ее второй аргумент имеет тип QVariant. Поскольку некоторые компиляторы C++ до сих пор не поддерживают должным образом тип bool, приходится передавать конструктору QVariant заготовку типа int. Для типов отличных от bool, преобразование проходит автоматически.

Вслед за нею вызывается setSizePolicy(), отдавая компоненту все доступное на форме пространство. После чего выполняется подключение трех событий ActiveX плеера к слотам приложения.

Оставшаяся часть конструктора выполняет обычные, для любого Qt класса, действия, за исключением, разве что, подключения трех Qt-сигналов к слотам COM-объекта (Play(), Pause() и Stop()).

Оставим конструктор в покое и рассмотрим функцию timerEvent():

void PlayerWindow::timerEvent(QTimerEvent *event) { if (event->timerId() == updateTimer) { double curPos = wmp->property("CurrentPosition").toDouble(); onPositionChange(-1, curPos); } else { QWidget::timerEvent(event); } } Эта функция регулярно вызывается через определенные интервалы времени, во время проигрывания. С ее помощью устанавливается положение движка. Значение положения движка определяется путем чтения свойства CurrentPosition элемента ActiveX, с помощью функции property().

Мы не привели остальную часть кода конструктора по той причине, что он напрямую не связан с ActiveX и не демонстрирует ничего такого, о чем бы мы не говорили ранее.

Чтобы связать приложение с модулем QAxContainer, необходимо в файл проекта добавить строку:

LIBS += -lqaxcontainer Иногда возникает необходимость прямого вызова методов COM-объекта (без соединения с сигналом Qt). Самый простой способ -- вызвать функцию dynamicCall(), которой в качестве первого аргумента передать имя и сигнатуру вызываемого метода, а аргументы метода -- в виде последующих входных параметров, например: wmp->dynamicCall("TitlePlay(uint)", 6); Функция dynamicCall() может принимать до восьми аргументов типа QVariant и возвращает значение типа QVariant. Если методу нужно передать аргумент типа IDispatch * или IUnknown *, то можно сначала инкапсулировать его в объект QAxObject, а затем вызвать метод asVariant(), чтобы преобразовать его в тип QVariant. Если метод COM-объекта возвращает значение типа IDispatch * или IUnknown, или если нужно получить доступ к свойству COM-объекта одного из этих типов, то нам придется воспользоваться функцией querySubObject(): QAxObject *session = outlook.querySubObject("Session"); QAxObject *defaultContacts = session->querySubObject("GetDefaultFolder(OlDefaultFolders)", "olFolderContacts"); Если вызываемая функция имеет аргументы неподдерживаемых типов, вы должны сначала получить COM-интерфейс, вызовом функции QAxBase::queryInterface(), а затем вызвать нужный метод напрямую. Не забывайте вызвать Release(), когда надобность в интерфейсе отпадет.

Если необходимость в вызове подобных функций возникает достаточно часто, вы можете создать класс-потомок от QAxObject или от QAxWidget, и добавить функции-члены для работы с COM-интерфейсом. При этом вы должны помнить, что потомки классов QAxObject и QAxWidget не могут определять новые свойства, сигналы и слоты.

Теперь перейдем к обзору модуля QAxServer. Этот модуль позволяет превратить обычную Qt-программу в ActiveX-сервер. Сервер может быть выполнен либо в виде динамической библиотеки, либо в виде автономного приложения. Серверы, собранные в виде динамической библиотеки часто называют "внутренними" (in-process) серверами, а автономные приложения -- "внешними" (out-of-process) серверами.

Наш первый пример будет собран в виде "внутреннего" сервера, который реализует виджет, отображающий прыгающий шарик. Мы так же покажем, как встроить виджет в Internet Explorer.

Рисунок 18.5. Виджет AxBouncer в Internet Explorer.


Начнем с определения класса виджета AxBouncer: class AxBouncer : public QWidget, public QAxBindable { Q_OBJECT Q_ENUMS(Speed) Q_PROPERTY(QColor color READ color WRITE setColor) Q_PROPERTY(Speed speed READ speed WRITE setSpeed) Q_PROPERTY(int radius READ radius WRITE setRadius) Q_PROPERTY(bool running READ isRunning) Класс AxBouncer является потомком сразу двух классов: QWidget и QAxBindable. Класс QAxBindable реализует интерфейс между виджетом и ActiveX-клиентом. Любой виджет может быть экспортирован как элемент ActiveX, но порождая его от QAxBindable, мы получаем возможность извещать клиента при изменении значений свойств виджета, а так же реализовать COM-интерфейсы, в дополнение к тем, что уже реализованы в QAxServer.

При использовании множественного наследования, с участием класса QObject, вы всегда должны указывать этот класс первым в списке предков, чтобы обеспечить корректную работу утилиты moc.

В нашем определении мы объявили три свойства, доступные для чтения/записи и одно свойство, доступное только для чтения. Макрос Q_ENUMS() сообщает утилите moc, что Speed -- это перечисление, которое объявлено в публичной секции

public: enum Speed { Slow, Normal, Fast }; AxBouncer(QWidget *parent = 0, const char *name = 0); void setSpeed(Speed newSpeed); Speed speed() const { return ballSpeed; } void setRadius(int newRadius); int radius() const { return ballRadius; } void setColor(const QColor &newColor); QColor color() const { return ballColor; } bool isRunning() const { return myTimerId != 0; } QSize sizeHint() const; QAxAggregated *createAggregate(); public slots: void start(); void stop(); signals: void bouncing(); В конструкторе AxBouncer нет ничего необычного, это стандартный конструктор виджета, с аргументами parent и name. Макрос QAXFACTORY_DEFAULT(), который используется для экспорта компонента, принимает в виде параметра конструктор именно с такой сигнатурой.

Функция createAggregate() перекрывает метод родительского класса QAxBindable. К ее описанию мы вскоре вернемся.

protected: void paintEvent(QPaintEvent *event); void timerEvent(QTimerEvent *event); private: int intervalInMilliseconds() const; QColor ballColor; Speed ballSpeed; int ballRadius; int myTimerId; int x; int delta; }; Защищенная и приватная секции класса не содержат ничего необычного, что отличало бы их от привычных виджетов Qt. AxBouncer::AxBouncer(QWidget *parent, const char *name) : QWidget(parent, name, WNoAutoErase) { ballColor = blue; ballSpeed = Normal; ballRadius = 15; myTimerId = 0; x = 20; delta = 2; } Конструктор выполняет инициализацию приватных переменных. void AxBouncer::setColor(const QColor &newColor) { if (newColor != ballColor amp;amp; requestPropertyChange("color")) { ballColor = newColor; update(); propertyChanged("color"); } } Функция setColor() записывает новое значение цвета в переменную-член color и перерисовывает виджет, вызовом функции update().

Необычными моментами здесь являются обращения к функциям requestPropertyChange() и propertyChanged(). Эти функции унаследованы от класса QAxBindable. В идеале эти функции должны вызываться в паре всякий раз, когда необходимо изменить значение свойства. Функция requestPropertyChange() запрашивает права клиента на изменение свойства и возвращает true, если клиенту позволено это делать. Функция propertyChanged() извещает клиента о том, что изменение произведено.

Функции setSpeed() и setRadius(), которые так же устанавливают новые значения свойств, следуют тому же шаблону. Аналогичные действия выполняют слоты start() и stop(), потому что они изменяют свойство running.

Осталась еще одна функция класса AxBouncer, которая представляет для нас интерес:

QAxAggregated *AxBouncer::createAggregate() { return new ObjectSafetyImpl; } Функция createAggregate() перекрывает метод предка -- QAxBindable. Она позволяет реализовать COM-интерфейс(ы), которые не реализованы в модуле QAxServer, или обойти COM-интерфейсы по-умолчанию. В данном случае возвращается интерфейс IObjectSafety, который используется в Internet Explorer, для безопасного доступа к элементам компонента. Это обычный трюк, помогающий избежать сообщения об ошибке: "Object not safe for scripting".

Ниже приводится определение класса, который реализует интерфейс IObjectSafety:

class ObjectSafetyImpl : public QAxAggregated, public IObjectSafety { public: long queryInterface(const QUuid &iid, void **iface); QAXAGG_IUNKNOWN HRESULT WINAPI GetInterfaceSafetyOptions(REFIID riid, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions); HRESULT WINAPI SetInterfaceSafetyOptions(REFIID riid, DWORD pdwSupportedOptions, DWORD pdwEnabledOptions); }; Кдасс ObjectSafetyImpl порожден от QAxAggregated и IObjectSafety. Класс QAxAggregated -- это абстрактный класс, использующийся в качестве базового, для реализации дополнительных COM-интерфейсов. Доступ к COM-объекту, который является расширением QAxAggregated, может быть получен вызовом функции controllingUnknown(). Этот COM-объект негласно создается самим модулем QAxServer.

Макрос QAXAGG_IUNKNOWN подставляет стандартную реализацию функций QueryInterface(), AddRef() и Release().

long ObjectSafetyImpl::queryInterface(const QUuid &iid, void **iface) { *iface = 0; if (iid == IID_IObjectSafety) *iface = (IObjectSafety *)this; else return E_NOINTERFACE; AddRef(); return S_OK; } Функция queryInterface() вызывается программой-клиентом, управляющей COM-объектом, для того, чтобы получить доступ к интерфейсу, реализуемому классом-наследником от QAxAggregated. Для интерфейсов, реализация которых отсутствует, следует возвращать значение E_NOINTERFACE. HRESULT WINAPI ObjectSafetyImpl::GetInterfaceSafetyOptions( REFIID riid, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions) { *pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_DATA | INTERFACESAFE_FOR_UNTRUSTED_CALLER; *pdwEnabledOptions = *pdwSupportedOptions; return S_OK; } HRESULT WINAPI ObjectSafetyImpl::SetInterfaceSafetyOptions(REFIID, DWORD, DWORD) { return S_OK; } Функции GetInterfaceSafetyOptions() и SetInterfaceSafetyOptions() объявлены в IObjectSafety. Мы выполняем их реализацию для того, чтобы сообщить вызывающей программе о том, что объект безопасен.

Теперь перейдем к содержимому файла main.cpp:

#include <qaxfactory.h> #include "axbouncer.h" QAXFACTORY_DEFAULT(AxBouncer, "{5e2461aa-a3e8-4f7a-8b04-307459a4c08c}", "{533af11f-4899-43de-8b7f-2ddf588d1015}", "{772c14a5-a840-4023-b79d-19549ece0cd9}", "{dbce1e56-70dd-4f74-85e0-95c65d86254d}", "{3f3db5e0-78ff-4e35-8a5d-3d3b96c83e09}") int main() { return 0; } Макрос QAXFACTORY_DEFAULT() экспортирует компонент ActiveX. Этот макрос используется в случаях, когда сервер экспортирует единственный элемент управления ActiveX. В противном случае, сервер должен перекрыть класс QAxFactory и использовать макрос QAXFACTORY_EXPORT(). Следующий пример в этом разделе продемонстрирует, как это делается.

Первый аргумент QAXFACTORY_DEFAULT() -- имя экспортируемого класса Qt. Остальные пять аргументов -- это GUID класса, GUID интерфейса, GUID интерфейса событий, GUID библиотеки типов и GUID приложения. Для генерации этих значений могут использоваться стандартные утилиты guidgen и uuidgen.

Поскольку наш сервер является библиотекой, мы не предусматриваем никаких действий в функции main(). Однако она нужна нам, чтобы "умиротворить" программу-компоновщик (linker).

Файл проекта для .pro нашего сервера:

TEMPLATE = lib CONFIG += activeqt dll HEADERS = axbouncer.h \ objectsafetyimpl.h SOURCES = axbouncer.cpp \ main.cpp \ objectsafetyimpl.cpp RC_FILE = qaxserver.rc DEF_FILE = qaxserver.def Файлы qaxserver.rc и qaxserver.def могут быть скопированы из подкаталога библиотеки Qt: extensions\activeqt\control.

Файл makefile или файл проекта Visual C++, созданные qmake, уже содержат правила регистрации сервера в реестре Windows. Чтобы зарегистрировать сервер на другой машине, можно воспользоваться утилитой regsvr32, которая входит в состав ОС Windows.

После этого мы можем включить компонент Bouncer в HTML-страничку, с помощью тега <object>:

<object id="AxBouncer" classid="clsid:5e2461aa-a3e8-4f7a-8b04-307459a4c08c"> <b>The ActiveX control is not available. Make sure you have built and registered the component server.</b> </object> А для управления объектом -- разместить кнопки: <input type="button" value="Start" onClick="AxBouncer.start()"> <input type="button" value="Stop" onClick="AxBouncer.stop()"> Этим объектом можно управлять из JavaScript или VBScript точно так же, как и любым другим элементом управления ActiveX.

В нашем последнем примере мы рассмотрим реализацию приложения Address Book. Это приложение может использоваться как обычная Qt/Windows программа или как "внешний" ActiveX сервер, например для программ, написанных на Visual Basic.

class AddressBook : public QMainWindow { Q_OBJECT Q_PROPERTY(int count READ count) public: AddressBook(QWidget *parent = 0, const char *name = 0); ~AddressBook(); int count() const; public slots: ABItem *createEntry(const QString &contact); ABItem *findEntry(const QString &contact) const; ABItem *entryAt(int index) const; ... }; Класс AddressBook -- это виджет главного окна приложения. class ABItem : public QObject, public QListViewItem { Q_OBJECT Q_PROPERTY(QString contact READ contact WRITE setContact) Q_PROPERTY(QString address READ address WRITE setAddress) Q_PROPERTY(QString phoneNumber READ phoneNumber WRITE setPhoneNumber) public: ABItem(QListView *listView); void setContact(const QString &contact); QString contact() const { return text(0); } void setAddress(const QString &address); QString address() const { return text(1); } void setPhoneNumber(const QString &number); QString phoneNumber() const { return text(2); } public slots: void remove(); }; Класс ABItem представляет одну запись в адресной книге. Он является производным от класса QListViewItem, что позволяет отображать его в QListView. А поскольку в число его предков входит класс QObject, то это позволяет экспортировать его как COM-объект. int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!QAxFactory::isServer()) { AddressBook addressBook; app.setMainWidget(&addressBook); addressBook.show(); return app.exec(); } return app.exec(); } В функции main() выполняется проверка -- запущено ли приложение как автономная программа, или как сервер. Если программа запускается как автономное приложение, то создается главный виджет и дальше все идет как в обычных Qt-приложениях. Чтобы запустить программу как сервер, нужно передать ей ключ командной строки: -activex.

В дополнение к ключу -activex, серверы ActiveX могут принимать следующие ключи:

Если приложение будет использоваться как сервер, необходимо экспортировать классы AddressBook и ABItem как COM-компоненты: QAXFACTORY_EXPORT(ABFactory, "{2b2b6f3e-86cf-4c49-9df5-80483b47f17b}", "{8e827b25-148b-4307-ba7d-23f275244818}") Макрос QAXFACTORY_EXPORT() экспортирует фабрику COM-объектов. Поскольку наше приложение экспортирует два COM-объекта, то мы уже не можем воспользоваться макросом QAXFACTORY_DEFAULT(), как это было сделано в предыдущем примере.

Первый аргумент макроса QAXFACTORY_EXPORT() -- имя класса-наследника QAxFactory, который реализует COM-объект. Другие два аргумента -- это GUID библиотеки типов и приложения.

class ABFactory : public QAxFactory { public: ABFactory(const QUuid &lib, const QUuid &app); QStringList featureList() const; QWidget *create(const QString &key, QWidget *parent, const char *name); QUuid classID(const QString &key) const; QUuid interfaceID(const QString &key) const; QUuid eventsID(const QString &key) const; QString exposeToSuperClass(const QString &key) const; }; Класс ABFactory порожден от класса QAxFactory, он реализует виртуальную функцию, экспортирующую класс AddressBook, как элемент управления ActiveX, и класс ABItem, как COM-объект. ABFactory::ABFactory(const QUuid &lib, const QUuid &app) : QAxFactory(lib, app) { } Конструктор класса ABFactory просто передает два аргумента конструктору родительского класса. QStringList ABFactory::featureList() const { return QStringList() << "AddressBook" << "ABItem"; } Функция featureList() возвращает список COM-объектов, которые могут быть созданы фабрикой. QWidget *ABFactory::create(const QString &key, QWidget *parent, const char *name) { if (key == "AddressBook") return new AddressBook(parent, name); else return 0; } Функция create() создает экземпляр элемента управления ActiveX. Для случая ABItem возвращается пустой указатель, поскольку пользователь не должен иметь возможность создавать объекты этого типа. QUuid ABFactory::classID(const QString &key) const { if (key == "AddressBook") return QUuid("{588141ef-110d-4beb-95ab-ee6a478b576d}"); else if (key == "ABItem") return QUuid("{bc82730e-5f39-4e5c-96be-461c2cd0d282}"); else return QUuid(); } Функция classId() возвращает идентификаторы классов, которые могут быть экспортированы фабрикой. QUuid ABFactory::interfaceID(const QString &key) const { if (key == "AddressBook") return QUuid("{718780ec-b30c-4d88-83b3-79b3d9e78502}"); else if (key == "ABItem") return QUuid("{c8bc1656-870e-48a9-9937-fbe1ceff8b2e}"); else return QUuid(); } Функция interfaceId() возвращает идентификаторы интерфейсов классов, экспортируемых фабрикой. QUuid ABFactory::eventsID(const QString &key) const { if (key == "AddressBook") return QUuid("{0a06546f-9f02-4f14-a269-d6d56ffeb861}"); else if (key == "ABItem") return QUuid("{105c6b0a-3fc7-460b-ae59-746d9d4b1724}"); else return QUuid(); } Функция eventsId() возвращает идентификаторы интерфейсов событий, для экспортируемых классов. QString ABFactory::exposeToSuperClass(const QString &key) const { return key; } По-умолчанию, элементы управления ActiveX поставляют клиентам не только свои собственные свойства, сигналы и слоты, но и свойства, сигналы и слоты своих базовых классов, вплоть до QWidget. Мы можем перекрыть метод exposeToSuperClass(), чтобы ограничить верхнюю границу (в дереве наследования) поставляемых классов.

Здесь, в виде верхней границы, мы просто возвращаем имя класса компонента ("AddressBook" или "ABItem"). Это означает, что свойства, сигналы и слоты классов-предков для AddressBook и ABItem поставляться не будут.

Ниже приводится содержимое файла проекта для нашего "внешнего" сервера ActiveX:

CONFIG += activeqt HEADERS = abfactory.h \ abitem.h \ addressbook.h \ editdialog.h SOURCES = abfactory.cpp \ abitem.cpp \ addressbook.cpp \ editdialog.cpp \ main.cpp RC_FILE = qaxserver.rc Файл qaxserver.rc может быть скопирован из подкаталога extensions\activeqt\control.

На этом мы завершаем краткий обзор ActiveQt framework. Дистрибутив библиотеки Qt включает в себя ряд дополнительных примеров, которые содержат сведения о модулях QAxContainer и QAxServer и решения наиболее общих проблем совместимости.