在 IE 上的 ActiveX 控件之间触发事件






4.79/5 (26投票s)
2002年 6月 2日
6分钟阅读

462389

6391
ActiveX 控件通过 Internet Explorer 上的 IConnectionPointContainer 接口向其他控件触发事件。
引言
在 IE 浏览器中的一对可连接对象,我花了一周多的时间为我的新工作检索了一些示例源代码/文章,结果一无所获,只找到几篇需要相同知识的帖子。这也是本文诞生的原因之一。本文的关键技术是 IConnectionPointContainer
和 IOleContainer
的实现。您可以在一个小圈子里自由涂鸦,它会同时将每次涂鸦的步骤服务于一个大圈子,甚至跨越不同的 IE 实例。小的称作连接点服务器 (CPServer
),大的称作客户端 (CPClient
)。它们都是 IE 浏览器的 ActiveX 控件。
演示用法
可下载的演示 Zip 包含以下 5 个文件
- CPServer.dll, CPClient.dll
- 2Server&1Client.html, anotherServer.html
- readme.txt
- 在资源管理器中右键单击每个 DLL,然后在上下文菜单中选择“打开方式”并“注册”(或在命令提示符下为每个 DLL 输入“regsvr32 xxx.dll”)。
- 在资源管理器中双击 xxx.html(或在 Visual Studio 编辑器中右键单击并选择“预览”)。
Windows Installer 演示用法
通过 Windows Installer 下载的演示会为您自动安装并注册。卸载也很简单。安装后,您可能会在桌面上找到两个指向已安装 HTML 文件的快捷方式图标。单击它们。
红色状态表示连接中断
您可能在任何大的 CPClient
圆圈中单击鼠标左键,使其成为仅响应来自小的 CPServer
圆圈的事件。这意味着执行 IConnectionPoint::Unadvise
方法。同时,CPClient
会将背景从红色变为蓝色,如上图所示。
2Server&1Client.html 包含两个 CPServer
和一个 CPClient
模型,而 anotherServer.html 包含一个 CPServer
模型。
用于涂鸦数据的保存/加载/获取菜单
您可以右键单击大的 CPClient
圆圈以获取用于涂鸦数据的保存/加载/获取菜单。本地文件将保存/加载到/从 CPClient.dll 的文件夹中,并具有固定名称 'scribble.dat'。HTML 中插入了两个 URL,作为 <PARAM NAME="SushiURL" VALUE="http://www.informax.co.jp/pen.dat">
和 <PARAM NAME="PhotoURL" VALUE="http://www.informax.co.jp/photo.dat">
添加到 CPClient
对象脚本中。一旦您通过 Internet 获取了 pen.dat/photo.dat,您也可以将其保存为名为 'scribble.dat' 的本地文件。
涂鸦数据格式
它是一种非常简单的文本格式。每个涂鸦矢量/线都以回车符开始,参数为“0 0 0”。之后,直到矢量/线结束,都包含逻辑 X、Y 轴和 RGB 颜色编号,例如“50 111 16777215”。
工作区及其项目
此工作区使用 VC++6.0、ATL 和 STL。ATL 用于 COM 接口实现,STL 用于涂鸦鼠标移动的坐标集合、用于连接点 Cookie 的其他集合等。该工作区包含两个项目,均用于构建 ActiveX 控件。第一个项目构建 CPClient.dll 模块,第二个项目构建 CPServer.dll。
CPServer 实现
我的英语不好,所以基本的 ActiveX/ATL 实现应该由本网站上其他有用的文章来解释。两个 ActiveX 控件都由 IE 浏览器按照 HTML 文件中脚本行的位置顺序启动。以下是 2Server&1Client.html 脚本的主要摘录
<BODY bgColor="#ffff99">
<OBJECT ID="CPServer" CLASSID="CLSID:xxx-...-xxx" HEIGHT=230 WIDTH=200 ALIGN=LEFT>
<PARAM NAME="PenColor" VALUE="#ff0000">
</OBJECT>
<OBJECT ID="CPServer" CLASSID="CLSID:xxx-...-xxx" HEIGHT=230 WIDTH=200 ALIGN=RIGHT>
<PARAM NAME="PenColor" VALUE="#000000">
</OBJECT>
<OBJECT ID="CPClient" CLASSID="CLSID:xxx-...-xxx" HEIGHT=330 WIDTH=300>
...
</OBJECT>
</BODY>
请注意,上面的 PARAM
标签行以 RGB 十六进制格式将每个 CPServer
的画笔颜色设置为红色或黑色,但您可以按照以下方式将任何喜欢的颜色设置为“xxxxxx”
<PARAM NAME="PenColor" VALUE="#xxxxxx">
以下是 anotherServer.html 脚本的主要摘录。另请注意设置 CPServer
画笔颜色为 LIME-GREEN 的一行。
<BODY bgColor="#ffff99">
<OBJECT ID="CPServer" CLASSID="CLSID:xxx-...-xxx" HEIGHT=230 WIDTH=200 ALIGN=LEFT>
<PARAM NAME="PenColor" VALUE="#32CD32">
</OBJECT>
</BODY>
CPServer
的画笔颜色的 put-property 实现为类成员函数,如下所示
STDMETHODIMP CCPServer::put_PenColor(BSTR newVal) { _bstr_t b (newVal, true); // define inclusive charactors as RGB hexadecimal format std::string s, s16 ("0123456789abcdefABCDEF"); s = (LPCSTR)b; // check string's format if like "#ff00aa" if (s.length()) { if (s.at(0) == '#') { s = s.substr(1, s.length() - 1); if (s.length() == 6) { int st = s.find_first_not_of (s16); if (st == -1) { sscanf (s.c_str (), "%x", &m_PenColor); m_PenColor = ((m_PenColor >> 0x0) & 0xff) << 0x10 | ((m_PenColor >> 0x8) & 0xff) << 0x8 | ((m_PenColor >> 0x10) & 0xff) << 0x0; return S_OK; } } } } // set black m_PenColor = 0; return S_OK; }
以下 CCPServer
类定义中继承的三个接口非常重要
//////////////////////////////////////////////////////////////////////// // CCPServer class ATL_NO_VTABLE CCPServer : ... public IObjectSafetyImpl<CCPServer, INTERFACESAFE_FOR_UNTRUSTED_CALLER>, public IProvideClassInfo2Impl<&CLSID_CPServer, &DIID__ICPServerEvents, &LIBID_CPSERVERLib>, public IPersistPropertyBagImpl<CCPServer> { public: ... BEGIN_COM_MAP(CCPServer) ... COM_INTERFACE_ENTRY(IObjectSafety) COM_INTERFACE_ENTRY(IProvideClassInfo2) COM_INTERFACE_ENTRY(IProvideClassInfo) COM_INTERFACE_ENTRY(IPersistPropertyBag) END_COM_MAP() ... BEGIN_PROP_MAP(CCPServer) ... // property PenColor added PROP_ENTRY("PenColor", 1, CLSID_NULL) // Example entries // PROP_ENTRY("Property Description", dispid, clsid) // PROP_PAGE(CLSID_StockColorPage) END_PROP_MAP() ... STDMETHOD(GetInterfaceSafetyOptions)(REFIID riid, DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions); STDMETHOD(SetInterfaceSafetyOptions)(REFIID riid, DWORD dwOptionSetMask, DWORD dwEnabledOptions); ...
特别是,IProvideClassInfo2
接口对于实现 IConnectionPointContainer::FindConnectionPoint
函数是必不可少的。如果您注释掉与 IProvideClassInfo2
接口相关的三行,您肯定会在上面写有 FindConnioectnPoint
的那一行遇到断言。该接口为客户端提供了足够的信息,以便客户端可以在运行时支持出接口。
其次,IObjectSafety
接口允许 IE 浏览器在不访问注册表的情况下确定此 ActiveX 是否安全。如果您注释掉与 IObjectSafety
接口相关的两行,并且还注释掉覆盖的两个方法 GetInterfaceSafetyOptions
和 SetInterfaceSafetyOptions
,您肯定会收到一个关于 ActiveX 安全的消息对话框警告,如下面的图像所示
第三,IPersistPropertyBag
接口是使用 HTML 页面的 PARAM
来设置属性的基类。如果您注释掉上面包含此接口的三行,您肯定只能将画笔颜色设置为黑色,而无法设置任何其他颜色。
CPClient 实现
当 CPClient
对象由 IE 浏览器启动时,它会获取浏览器的 IOleContainer
接口指针。然后,使用 IOleContainer::EnumObjects
函数,它会枚举浏览器页面中嵌入的所有 OLE 对象的 IUnknown
接口,当然也包括 CPClient
本身。然后 CPClient
可以找到 CPServer
的 IUnknown
接口指针。使用此接口,CPClient
最终在成员函数 CCPClient::DoAdvice
中设置连接,在 CPServers
之间触发事件,如下所示
... if (SUCCEEDED (hr)) { _bstr_t bstName ("CPServer"); if (bstName == bstr) { // get the IConnectionPointContainer interface pointer IConnectionPointContainer* pConnPtContaine = NULL; hr = pUnk->QueryInterface (IID_IConnectionPointContainer, (void**)&pConnPtContainer); _ASSERT (SUCCEEDED (hr) && pConnPtContainer != NULL); // get the IConnectionPoint interface pointer IConnectionPoint* pConnPt = NULL; hr = pConnPtContainer->FindConnectionPoint (DIID__ICPServerEvents, &pConnPt); _ASSERT (SUCCEEDED (hr) && pConnPt != NULL); // set/reset the CPClient's Unknown interface // as the CPServer's outgoing interface DWORD dwCookie = 0; if (bAdvise) { // get a Cookie hr = pConnPt->Advise (this->GetUnknown(), &dwCookie); // save a Cookie into Cookie collection m_vectorCookie.push_back (dwCookie); } else { // Unadvise all connection-point through Cookie collection for (vector<DWORD>::iterator iter = m_vectorCookie.begin (); iter != m_vectorCookie.end (); iter++) { dwCookie = (DWORD)*iter; hr = pConnPt->Unadvise (dwCookie); if (hr == CONNECT_E_NOCONNECTION) continue; else break; } } pConnPt->Release (); pConnPtContainer->Release (); } } ...
IConnectionPoint::Advise
函数传递一个 Sink
对象,该对象在每次被 CPServer
事件调用时都会被调用。Sink
对象即 CPClient
的 IUnknown
。而 IConnectionPoint::Unadvise
函数则停止事件连接。
以下是 2Server&1Client.html 脚本的主要摘录
<BODY bgColor="#ffff99">
...
<OBJECT ID="CPClient" CLASSID="CLSID:xxx-...-xxx" HEIGHT=330 WIDTH=300>
<PARAM NAME="SushiURL" VALUE="http://www.informax.co.jp/pen.dat">
<PARAM NAME="PhotoURL" VALUE="http://www.informax.co.jp/photo.dat">
</OBJECT>
</BODY>
SushiURL
属性设置用于获取为您准备的寿司般画笔样本数据的 URL;PhotoURL
属性设置用于获取照片般数据的 URL。
在 CPClient
的圆圈中,当您右键单击并选择“通过 Internet 获取寿司涂鸦”菜单时,成员函数 CCPClient::OnCommandContextMenu
中的以下代码行将被执行
... case ID_MENU_FILENAME_PEN: case ID_MENU_FILENAME_PHOTO: { // create WAIT cursor hWaitCursor = ::LoadCursor(NULL, IDC_WAIT); hOldCursor = ::SetCursor(hWaitCursor); // clear the stroke vector m_stroke.erase(m_stroke.begin(), m_stroke.end()); // if internet connection is available if (::InternetAttemptConnect(0) != ERROR_SUCCESS) goto error; // open connection to internet HINTERNET hInternetConnection = ::InternetOpen("Open", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, NULL); if (hInternetConnection == NULL) goto error; // open URL gotten by 'SushiURL' or 'PhotoURL' property // at <PARAM NAME="SushiURL"...> or // <PARAM NAME="PhotoURL"...> HINTERNET hURLSession = ::InternetOpenUrl(hInternetConnection, wID == ID_MENU_FILENAME_PEN ? m_sSushiUrl.c_str():m_sPhotoUrl.c_str(), NULL, 0, INTERNET_FLAG_RELOAD | INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_RAW_DATA, 0); // read the URL file std::istringstream* pIstringstream = NULL; std::istream* pIstream = NULL; if (hURLSession != NULL) { DWORD dwBytesAvailable = 0, dwBytesDownloaded = 0; std::string sLoad; do { if (::InternetQueryDataAvailable(hURLSession, &dwBytesAvailable, 0, 0)) { std::vector<char> buffer(dwBytesAvailable + 1); ::InternetReadFile(hURLSession, &buffer[0], dwBytesAvailable, &dwBytesDownloaded); std::string s(buffer.begin(), dwBytesAvailable); // append buffer to string sLoad += s; } else // end of data break; } while (dwBytesDownloaded > 0); // create a stream of string pIstringstream = new istringstream(sLoad); // convert string stream to input stream pIstream = new istream(*pIstringstream); // close the URL session ::InternetCloseHandle(hURLSession); // the stroke vector reads from steam m_stroke.in_stream(*pIstream); delete pIstream; delete pIstringstream; } else goto error; // close internet connection ::InternetCloseHandle(hInternetConnection); ::SetCursor(hOldCursor); this->RedrawWindow(); } break; ...
整个任务是将涂鸦数据从 URL 获取到矢量集合 m_stroke
中。m_sSushiUrl
和 m_sPhotoUrl
都是类成员,分别保存一个指向不同寿司涂鸦资源的 URL。首先,有必要将光标更改为等待光标,直到任务完成,因为 Internet 访问通常太慢。std::string
sLoad
封装了读取数据的块,std::istringstream*pIStringstream
将所有 sLoad
封装为流内存缓冲区,并且 std::istream*pIstream
是从 pIStringstream
创建的 I/O 流。最后的工作是恢复之前的光标。
注释
- F5 键会导致
CPClient
或CPServer
通过 IE 重新启动。 - 单击
CPClient
两次,在发生连接中断时重新连接到所有CPServer
。 - 如何从寿司位图制作 photo.dat 要归功于 .dan.g. 的文章(见下文),我已经修改了他的演示源代码(请询问我)。
历史
更新于 2005 年 8 月 17 日
- 提高绘制速度并修复了相关 bug。
- 提供了照片般的数据并修复了相关源代码。
更新于 2004 年 4 月 4 日
- 启用了涂鸦数据到本地文件的保存/加载。
- 启用了从 URL 获取涂鸦数据。
- 修改了关于 ActiveX 安全性的文章/源代码。
- 支持 UNICODE。
更新于 2004 年 4 月 1 日
- 支持在多个 IE 实例之间进行涂鸦。
- HTML 脚本中自由的画笔颜色参数。
更新于 2003 年 12 月 1 日
- 涂鸦时没有闪烁。
- 支持仅在
CPServer
圆圈内进行涂鸦。
发布于 2002 年 6 月 2 日
- 首次分发。
技巧
- 这是我注意到的一个与
IOLeContainer
相关的有趣事项。接口之间的循环关系
致谢
在进行此项目时,我从 Andrew 的出色文章中获得了一个想法
- Andrew Whitechapel 的 Dispinterface vs. Events and Runtime Sinks。
- .dan.g. 的 为 CBitmap 添加快速用户可扩展的图像处理支持。