在 Qt 中构建 ActiveX 服务器

The QAxServer 模块属于 ActiveQt 框架。它由 3 个类组成:

某些 范例实现 的 ActiveX 控件和 COM 对象有提供。

话题:

使用库

将标准 Qt 应用程序转换成 COM 服务器是使用 QAxServer 库,必须添加 axserver 到 QT 变量在 .pro 文件。

进程外可执行服务器的生成是从 .pro 文件像这样:

TEMPLATE = app
QT  += axserver
RC_FILE  = qaxserver.rc
...
					

要构建进程内服务器,使用 .pro 文件像这样:

TEMPLATE = lib
QT += axserver
CONFIG  += dll
DEF_FILE = qaxserver.def
RC_FILE  = qaxserver.rc
...
					

文件 qaxserver.rc and qaxserver.def 是框架的一部分,且通常可以使用其位置 (指定路径在 .pro 文件),或拷贝到工程目录。可以修改这些文件,只要它包括如类型库条目 (即:可以添加版本信息或指定不同工具框图标) 的任何文件。

使用 axserver 模块将导致 qmake 工具将要求构建步骤添加到构建系统:

  • 将二进制文件链接到 qaxserver.lib 而不是 qtmain.lib
  • 调用 idc 工具为 COM 服务器生成 IDL 文件
  • 使用 MIDL 工具将 IDL 编译成类型库 (编译器安装的一部分)
  • 将结果类型库作为二进制资源附加到服务器二进制文件 (再次使用 idc 工具)
  • 注册服务器

要跳过后处理步骤,还要设置 qaxserver_no_postlink 配置。

此外,可以指定版本号使用 VERSION 变量,如

TEMPLATE = lib
VERSION = 2.5
...
					

指定的版本号将用作类型库和服务器的版本,当注册时。

进程外 vs. 进程内

COM 服务器是作为独立可执行文件运行,还是作为在客户端进程中的共享库运行,主要取决于由服务器提供的 COM 对象类型。

可执行文件服务器拥有能作为独立应用程序运行的优点,但会增加 COM 客户端和 COM 对象之间的通信开销。若控件拥有编程错误,仅运行控件的服务器进程会崩溃,且客户端应用程序可能会继续运行。并非所有 COM 客户端,都支持可执行文件服务器。

进程内服务器通常更小,且启动时更快。客户端和服务器之间的通信是直接透过虚拟函数调用完成,且不会引入远程过程调用所要求的开销。不管怎样,若服务器崩溃,客户端应用程序也可能崩溃,且并非每个功能都可用于进程内服务器 (即:在 COM 运行对象表中注册)。

2 种服务器类型都可以使用 Qt 作为共享库,或静态链接到服务器二进制文件。

在后期构建步骤中的典型错误

对于 ActiveQt 使服务器工作的特定后处理步骤,必须满足一些要求:

  • 什么都不需要就可以创建所有暴露控件,但 QApplication 实例要存在
  • 服务器的初始链接包括临时类型库资源
  • 运行服务器要求的所有依赖都在系统路径下 (或在用于调用环境的路径下;注意,Visual Studio 在 "工具|选项|目录" 对话框中有列出自己的一组环境变量)。

若不满足这些要求,可能出现一种或多种下列错误:

服务器可执行文件崩溃

要生成 IDL (交互式数据语言),作为 ActiveX 控件暴露的 Widget 需要实例化 (调用构造函数)。此时,什么都没有除了 QApplication 对象存在。Widget 构造函数不得依赖要创建的任何其它对象 (如:应校验 null 指针)。

要调试服务器,采用 -dumpidl outputfile 运行它并校验它在哪里崩溃。

注意,没有调用控件的函数。

服务器可执行文件不是有效 Win32 应用程序

附加类型库破坏服务器二进制。这是 Windows Bug 且只发生于发行构建。

第一链接步骤必须将虚设类型库链接到稍后可以被 idc 替换的可执行文件。将带有类型库的资源文件添加到工程,如范例演示。

无法定位 DLL

构建系统需要运行服务器可执行文件来生成接口定义,并注册服务器。若服务器链接的动态链接库不在路径中,这可能失败 (如:Visual Studio 使用 Directories 选项中指定的环境设置调用服务器)。确保服务器要求的所有 DLL 和插件都位于错误消息框打印的路径列出目录下 (另请参阅 Windows 部署工具 ).

打不开文件 ...

ActiveX 服务器无法正确关闭,当使用它的最后一个客户端停止时。应用程序终止通常花费大约 2 秒钟,但可能必须使用任务管理器来杀除进程 (如:当客户端未正确释放控件时)。

无法实例化控件

在此情况下,以管理员身份可能有助于注册服务器。

实现控件

要采用 Qt 实现 COM (组件对象模型) 对象,创建子类化的 QObject 或任何现有 QObject 子类。若类是子类化的 QWidget ,COM 对象将是 ActiveX 控件。

#include <QWidget>
class MyActiveX : public QWidget
{
    Q_OBJECT
					

The Q_OBJECT 宏要求将 Widget 有关的元对象信息提供给 ActiveQt 框架。

Q_CLASSINFO("ClassID", "{1D9928BD-4453-4bdd-903D-E525ED17FDE5}")
Q_CLASSINFO("InterfaceID", "{99F6860E-2C5A-42ec-87F2-43396F4BE389}")
Q_CLASSINFO("EventsID", "{0A3E9F27-E4F1-45bb-9E47-63099BCCD0E3}")
					

使用 Q_CLASSINFO () 宏以指定用于 COM 对象的 COM 标识符。 ClassID and InterfaceID 是必需的,而 EventsID 才有必要当对象有信号时。要生成这些标识符,使用系统工具像 uuidgen or guidgen .

可以为每个类指定额外属性;见 类信息和调谐 了解细节。

Q_PROPERTY(int value READ value WRITE setValue)
					

使用 Q_PROPERTY () 宏声明 ActiveX 控件特性。

声明接受父级对象的标准构造函数、函数、信号及槽像对于任何 QObject 子类。

public:
    MyActiveX(QWidget *parent = 0)
    ...
    int value() const;
public slots:
    void setValue(int v);
    ...
signals:
    void valueChange(int v);
    ...
};
					

The ActiveQt 框架将特性和公共槽暴露成 ActiveX 特性和方法,将信号暴露成 ActiveX 事件,并在 Qt 数据类型和等效 COM 数据类型之间转换。

数据类型

特性支持的 Qt 数据类型包括:

Qt 数据类型 COM 特性
bool VARIANT_BOOL
QString BSTR
int int
uint 无符号 int
double double
qlonglong CY
qulonglong CY
QColor OLE_COLOR
QDate DATE
QDateTime DATE
QTime DATE
QFont IFontDisp*
QPixmap IPictureDisp*
QVariant VARIANT
QVariantList (same as QList < QVariant >) SAFEARRAY(VARIANT)
QStringList SAFEARRAY(BSTR)
QByteArray SAFEARRAY(BYTE)
QRect 用户定义类型
QSize 用户定义类型
QPoint 用户定义类型

信号和槽中参数支持的 Qt 数据类型:

Qt 数据类型 COM 参数
bool [in] VARIANT_BOOL
bool& [in, out] VARIANT_BOOL*
QString , const QString & [in] BSTR
QString & [in, out] BSTR*
QString & [in, out] BSTR*
int [in] int
int& [in,out] int
uint [in] unsigned int
uint& [in, out] unsigned int*
double [in] double
double& [in, out] double*
QColor , const QColor & [in] OLE_COLOR
QColor & [in, out] OLE_COLOR*
QDate , const QDate & [in] DATE
QDate & [in, out] DATE*
QDateTime , const QDateTime & [in] DATE
QDateTime & [in, out] DATE*
QFont , const QFont & [in] IFontDisp*
QFont & [in, out] IFontDisp**
QPixmap , const QPixmap & [in] IPictureDisp*
QPixmap & [in, out] IPictureDisp**
QList < QVariant >, const QList < QVariant >& [in] SAFEARRAY(VARIANT)
QList < QVariant >& [in, out] SAFEARRAY(VARIANT)*
QStringList , const QStringList & [in] SAFEARRAY(BSTR)
QStringList & [in, out] SAFEARRAY(BSTR)*
QByteArray , const QByteArray & [in] SAFEARRAY(BYTE)
QByteArray & [in, out] SAFEARRAY(BYTE)*
QObject * [in] IDispatch*
QRect & [in, out] struct QRect (用户定义)
QSize & [in, out] struct QSize (用户定义)
QPoint & [in, out] struct QPoint (用户定义)

还支持导出枚举和标志 (见 Q_enums() 和 Q_flags())。也支持将 in 参数类型作为返回值。

拥有使用任何其它数据类型的参数的特性和信号/槽,会被忽略由 ActiveQt 框架。

子对象

COM 对象可以拥有可以表示 COM 对象子元素的多个子对象。例如,表示多文档电子表格应用程序的 COM 对象,可以为每个电子表格提供一个子对象。

任何 QObject 子类可以用作 ActiveX 子对象的类型,只要知道 QAxFactory 。然后,类型可以用于特性,或作为槽的返回类型 (或参数)。

特性通知

要使 ActiveX 客户端特性可绑定,使用多继承自 QAxBindable 类:

#include <QAxBindable>
#include <QWidget>
class MyActiveX : public QWidget, public QAxBindable
{
    Q_OBJECT
					

当实现特性写入函数时,使用 QAxBindable 类的 requestPropertyChange() 和 propertyChanged() 函数以允许将 ActiveX 客户端绑定到控件特性。

服务控件

要使 COM 服务器可用于 COM 系统,必须使用 5 个唯一标识符在系统注册表中注册它。这些标识符的提供是通过工具像 guidgen or uuidgen 。注册信息允许 COM 本地化二进制提供请求的 ActiveX 控件,编组控件的 RPC (远程过程调用) 及读取由控件暴露方法和特性的有关类型信息。

要创建客户端询问时的 COM 对象,服务器必须导出实现的 QAxFactory 。做到这的最轻松方式是使用一组宏:

QAXFACTORY_BEGIN("{ad90301a-849e-4e8b-9a91-0a6dc5f6461f}",
                 "{a8f21901-7ff7-4f6a-b939-789620c03d83}")
    QAXCLASS(MyWidget)
    QAXCLASS(MyWidget2)
    QAXTYPE(MySubType)
QAXFACTORY_END()
					

这将导出 MyWidget and MyWidget2 作为可以由 COM 客户端创建的 COM 对象,和将注册 MySubType 作为类型可以用于特性和参数对于 MyWidget and MyWidget2 .

The QAxFactory 类文档编制 阐述如何使用此宏,及如何实现和使用自定义工厂。

对于进程外的可执行服务器,可以实现 main() 函数以实例化 QApplication 对象,并就像任何正常 Qt 应用程序进入事件循环。默认情况下,应用程序将作为标准 Qt 应用程序启动,但若传递 -activex 在命令行中,它将作为 ActiveX 服务器启动。使用 QAxFactory::isServer () 以创建和运行标准应用程序接口 (或阻止独立执行):

#include <QApplication>
#include <QAxFactory>
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    if (!QAxFactory::isServer()) {
        // create and show main window
    }
    return app.exec();
}
					

不管怎样,这没必要,因为 ActiveQt 提供了 main 函数的默认实现。默认实现调用 QAxFactory::startServer (),创建 QApplication 实例并调用 exec()。

要构建 ActiveX 服务器可执行文件,运行 qmake 以生成 makefile 并使用编译器 make 工具,如同任何其它 Qt 应用程序。make 进程还将在系统注册表中注册控件,通过调用结果可执行文件采用 -regserver 命令行选项。

若 ActiveX 服务器是可执行文件,支持下列命令行选项:

选项 结果
-regserver 在系统注册表中注册服务器
-unregserver 从系统注册表取消服务器的注册
-activex 将应用程序作为 ActiveX 服务器启动
-dumpidl <file> -version x.y 将服务器的 IDL 写入指定文件。类型库将拥有版本 x.y。

进程内服务器的注册是使用 regsvr32 工具,可用于所有 Windows 系统。

典型编译时问题

列出的编译器/链接器错误是基于由 Microsoft Visual C++ 6.0 编译器发出那些。

未获得 2 参数的重载函数

当代码出现错误使用 QAXFACTORY_DEFAULT () 宏,Widget 类没有可以用于默认工厂的构造函数。要么添加标准 Widget 构造函数,要么实现不要求的自定义工厂。

当代码出现错误使用 QAXFACTORY_EXPORT () 宏, QAxFactory 子类没有合适构造函数。提供公共类构造函数像

MyFactory(const QUuid &, const QUuid &);
					

为工厂类。

句法错误:坏的数字后缀

唯一标识符尚未作为字符串被传递给 QAXFACTORY_EXPORT () 或 QAXFACTORY_DEFAULT () 宏。

未解析的外部符号 _ucm_instantiate

服务器未导出实现的 QAxFactory 。使用 QAXFACTORY_EXPORT () 宏在某个工程的实现文件中以实例化并导出工厂,或使用 QAXFACTORY_DEFAULT () 宏以使用默认工厂。

_ucm_initialize 已定义于 ...

服务器导出多个实现的 QAxFactory ,或导出相同实现 2 次。若使用默认工厂, QAXFACTORY_DEFAULT () 宏只得在工程中使用 1 次。使用自定义 QAxFactory 实现和 QAXFACTORY_EXPORT () 宏若服务器提供多个 ActiveX 控件。

分布 QAxServer 二进制文件

采用 Qt 编写 ActiveX 服务器要么可以使用 Qt 作为共享库,要么将 Qt 静态链接到二进制。2 种方式都会产生相当大的包 (要么服务器二进制本身变大,要么必须随附 Qt DLL)。

安装独立服务器

当 ActiveX 服务器还可以作为独立应用程序运行时,运行服务器可执行文件采用 -regserver 命令行参数在目标系统安装可执行文件后。之后,由服务器提供的控件将可用于 ActiveX 客户端。

安装进程内服务器

若 ActiveX 服务器是安装包的一部分,使用 regsvr32 工具由 Microsoft 提供,以在目标系统注册控件。若此工具不存在,将 DLL 加载到安装程序进程,解析 DllRegisterServer 符号并调用函数:

HMODULE dll = LoadLibrary("myserver.dll");
typedef HRESULT(__stdcall *DllRegisterServerProc)();
DllRegisterServerProc DllRegisterServer =
    (DllRegisterServerProc)GetProcAddress(dll, "DllRegisterServer");
HRESULT res = E_FAIL;
if (DllRegisterServer)
    res = DllRegisterServer();
if (res != S_OK)
    // error handling
					

通过 Internet 分发服务器

若想要在网页使用服务器控件,需要使服务器可用于查看页面的浏览器,且需要在页面中指定服务器包位置。

要指定服务器位置,使用网站 OBJECT 标签的 CODEBASE 属性。值会指向服务器文件本身、列出服务器要求的其它文件的 INF 文件 (如 Qt DLL) 或被压缩的 CAB 存档。

INF 和 CAB 文件文档化在关于 ActiveX 和 COM 编程的几乎每一本书中,MSDN 库及各种其它在线资源中。范例包括可以用于构建 CAB 存档的 INF 文件:

[version]
    signature="$CHICAGO$"
    AdvancedINF=2.0
 [Add.Code]
    simpleax.exe=simpleax.exe
 [simpleax.exe]
    file-win32-x86=thiscab
    clsid={DF16845C-92CD-4AAB-A982-EB9840E74669}
    RegisterServer=yes
					

来自 Microsoft 的 CABARC 工具可以轻松生成 CAB 存档:

cabarc N simpleax.cab simpleax.exe simple.inf
					

INF 文件假定 Qt 是静态构建的,所以 INF 文件中未列出其它 DLL 的依赖。要根据 DLL 分发 ActiveX 服务器,必须添加依赖,并提供带有存档的库文件。

使用控件

要使用 ActiveX 控件,如将其嵌入网页,使用 <object> HTML 标签。

<object ID="MyActiveX1" CLASSID="CLSID:ad90301a-849e-4e8b-9a91-0a6dc5f6461f">
   ...
<\object>
					

要初始化控件特性,使用

<object ID=...>
    <param name="name" value="value">
<\object>
					

若 Web 浏览器支持脚本,请使用 JavaScript、VBScript 及控件脚本表单。 ActiveQt 范例 包括范例控件的演示 HTML 页面。

支持和不支持的 ActiveX 客户端

以下很大程度上是基于我们自己的 ActiveX 控件和客户端应用程序的经验,且并不完整。

支持的客户端

这些标准应用程序工作于 ActiveX 控件开发采用 ActiveQt 。注意,某些客户端仅支持进程内控件。

  • Internet Explorer
  • 微软 ActiveX 控件测试容器
  • 微软 Visual Studio 6.0
  • 微软 Visual Studio.NET/2003
  • 微软 Visual Basic 6.0
  • 基于 MFC (微软基础类) 和 ATL (活动模板库) 的容器
  • Sybase PowerBuilder
  • ActiveQt 基容器

支持微软 Office 应用程序,但需要将控件注册为 "Insertable (可插入)" 对象。重实现 QAxFactory::registerClass 以将此属性添加到 COM 类,或将类的 Insertable 类信息设为 yes 使用 Q_CLASSINFO 宏。

不支持的客户端

我们没有成功使 ActiveQt 基 COM 对象工作于下列客户端应用程序。

  • Borland C++ 构建器 (第 5 和第 6 版)
  • Borland Delphi

典型运行时错误

服务器不响应

若系统无法启动服务器 (采用任务管理器校验服务器是否有运行进程),确保服务器依赖的 DLL 没有从系统路径下丢失 (如 Qt DLL)。使用依赖 walker (步行者) 能查看服务器二进制的所有依赖。

若服务器在运行 (如:任务管理器有列出进程),见以下章节,了解调试服务器的有关信息。

无法创建对象

若在构建过程期间可以正确构建并注册服务器,但无法初始化对象 (如:通过 OLE/COM 对象 Viewer 查看器应用程序),确保服务器依赖的 DLL 没有从系统路径下丢失 (如 Qt DLL)。使用依赖 walker (步行者) 能查看服务器二进制的所有依赖。

若服务器在运行,见以下章节了解调试服务器的有关信息。

调试运行时错误

To debug an in-process server in Visual Studio, set the server project as the active project, and specify a client "executable for debug session" in the project settings (e.g. use the ActiveX Test Container). You can set breakpoints in your code, and also step into ActiveQt and Qt code if you installed the debug version.

To debug an executable server, run the application in a debugger and start with the command line parameter -activex . Then start your client and create an instance of your ActiveX control. COM will use the existing process for the next client trying to create an ActiveX control.

类信息和调谐

To provide attributes for each COM class, use the Q_CLASSINFO macro, which is part of Qt's meta object system.

Key 值的含义
版本 类的版本 (默认为 1.0)
描述 描述类的字符串。
ClassID The class ID. You must reimplement QAxFactory::classID if not specified.
InterfaceID The interface ID. You must reimplement QAxFactory::interfaceID if not specified.
EventsID The event interface ID. No signals are exposed as COM events if not specified.
DefaultProperty The property specified represents the default property of this class. Ie. the default property of a push button would be "text".
DefaultSignal The signal specified respresents the default signal of this class. Ie. the default signal of a push button would be "clicked".
LicenseKey Object creation requires the specified license key. The key can be empty to require a licensed machine. By default classes are not licensed. Also see the following section.
StockEvents Objects expose stock events if value is "yes". See QAxFactory::hasStockEvents ()
ToSuperClass Objects expose functionality of all super-classes up to and including the class name in value. See QAxFactory::exposeToSuperClass ()
Insertable If the value is "yes" the class is registered to be "Insertable" and will be listed in OLE 2 containers (ie. Microsoft Office). This attribute is not be set by default.
Aggregatable If the value is "no" the class does not support aggregation. By default aggregation is supported.
Creatable If the value is "no" the class cannot be created by the client, and is only available through the API of another class (ie. the class is a sub-type).
RegisterObject If the value is "yes" objects of this class are registered with OLE and accessible from the running object table (ie. clients can connect to an already running instance of this class). This attribute is only supported in out-of-process servers.
MIME The object can handle data and files of the format specified in the value. The value has the format mime:extension:description. Multiple formats are separated by a semicolon.
CoClassAlias The classname used in the generated IDL and in the registry. This is esp. useful for C++ classes that live in a namespace - by default, ActiveQt just removes the "::" to make the IDL compile.
Implemented Categories List of comma-separated Category ID (CATID) UUIDs. Generic mechanism for specifying additional container capabilities, in addition to "control", "insertable" etc. Typical CATIDs include CATID_InternetAware ("{0DE86A58-2BAA-11CF-A229-00AA003D7352}"), CATID_SafeForScripting ("{7DD95801-9882-11CF-9FA9-00AA006C42C4}") as well as user-defined CATID values.

Note that both keys and values are case sensitive.

The following declares version 2.0 of a class that exposes only its own API, and is available in the "Insert Objects" dialog of Microsoft Office applications.

class MyActiveX : public QWidget
{
    Q_OBJECT
    Q_CLASSINFO("Version", "2.0")
    Q_CLASSINFO("ClassID", "{7a4cffd8-cbcd-4ae9-ae7e-343e1e5710df}")
    Q_CLASSINFO("InterfaceID", "{6fb035bf-8019-48d8-be51-ef05427d8994}")
    Q_CLASSINFO("EventsID", "{c42fffdf-6557-47c9-817a-2da2228bc29c}")
    Q_CLASSINFO("Insertable", "yes")
    Q_CLASSINFO("ToSuperClass", "MyActiveX")
    Q_PROPERTY(...)
public:
    MyActiveX(QWidget *parent = 0);
    ...
};
					

开发许可组件

If you develop components you might want to control who is able to instantiate those components. Since the server binary can be shipped to and registered on any client machine it is possible for anybody to use those components in his own software.

Licensing components can be done using a variety of techniques, e.g. the code creating the control can provide a license key, or the machine on which the control is supposed to run needs to be licensed.

To mark a Qt class as licensed specify a "LicenseKey" using the Q_CLASSINFO () 宏。

class MyLicensedControl : public QWidget
{
    Q_OBJECT
    Q_CLASSINFO("LicenseKey", "<key string>")
    ...
};
					

The key is required to be able to create an instance of MyLicensedControl on a machine that is not licensed itself. The licensed developer can now redistributes the server binary with his application, which creates the control using the value of "LicenseKey", while users of the application cannot create the control without the license key.

If a single license key for the control is not sufficient (ie. you want differnet developers to receive different license keys) you can specify an empty key to indicate that the control requires a license, and reimplement QAxFactory::validateLicenseKey () to verify that a license exists on the system (ie. through a license file).

更多接口

ActiveX 控件提供通过 ActiveQt servers support a minimal set of COM interfaces to implement the OLE specifications. When the ActiveX class inherits from the QAxBindable class it can also implement additional COM interfaces.

Create a new subclass of QAxAggregated and use multiple inheritance to subclass additional COM interface classes.

class AxImpl : public QAxAggregated, public ISomeCOMInterface
{
public:
    AxImpl() {}
    long queryInterface(const QUuid &iid, void **iface);
    // IUnknown
    QAXAGG_IUNKNOWN
    // ISomeCOMInterface
    ...
}
					

重实现 QAxAggregated::queryInterface () 函数以支持额外 COM 接口。

long AxImpl::queryInterface(const QUuid &iid, void **iface)
{
    *iface = 0;
    if (iid == IID_ISomeCOMInterface)
        *iface = (ISomeCOMInterface *)this;
    else
        return E_NOINTERFACE;
    AddRef();
    return S_OK;
}
					

由于 ISomeCOMInterface 是子类化的 IUnknown you will have to implement the QueryInterface() , AddRef() ,和 Release() functions. Use the QAXAGG_IUNKNOWN macro in your class definition to do that. If you implement the IUnknown functions manually, delegate the calls to the interface pointer returned by the QAxAggregated::controllingUnknown () function, e.g.

HRESULT AxImpl::QueryInterface(REFIID iid, void **iface)
{
    return controllingUnknown()->QueryInterface(iid, iface);
}
					

不支持 IUnknown 接口本身在您的 queryInterface() 实现。

Implement the methods of the COM interfaces, and use QAxAggregated::object () if you need to make calls to the QObject subclass implementing the control.

QAxBindable 子类,实现 QAxBindable::createAggregate () to return a new object of the QAxAggregated 子类。

class MyActiveX : public QWidget, public QAxBindable
{
    Q_OBJECT
public:
    MyActiveX(QWidget *parent);
    QAxAggregated *createAggregate()
    {
        return new AxImpl();
    }
};
					

另请参阅 ActiveQt 框架 .