65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (26投票s)

2002年 6月 2日

6分钟阅读

viewsIcon

462389

downloadIcon

6391

ActiveX 控件通过 Internet Explorer 上的 IConnectionPointContainer 接口向其他控件触发事件。

引言

在 IE 浏览器中的一对可连接对象,我花了一周多的时间为我的新工作检索了一些示例源代码/文章,结果一无所获,只找到几篇需要相同知识的帖子。这也是本文诞生的原因之一。本文的关键技术是 IConnectionPointContainerIOleContainer 的实现。您可以在一个小圈子里自由涂鸦,它会同时将每次涂鸦的步骤服务于一个大圈子,甚至跨越不同的 IE 实例。小的称作连接点服务器 (CPServer),大的称作客户端 (CPClient)。它们都是 IE 浏览器的 ActiveX 控件。

演示用法

可下载的演示 Zip 包含以下 5 个文件

  • CPServer.dll, CPClient.dll
  • 2Server&1Client.html, anotherServer.html
  • readme.txt
  1. 在资源管理器中右键单击每个 DLL,然后在上下文菜单中选择“打开方式”并“注册”(或在命令提示符下为每个 DLL 输入“regsvr32 xxx.dll”)。
  2. 在资源管理器中双击 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”。

工作区及其项目

Workspace

此工作区使用 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 接口相关的两行,并且还注释掉覆盖的两个方法 GetInterfaceSafetyOptionsSetInterfaceSafetyOptions,您肯定会收到一个关于 ActiveX 安全的消息对话框警告,如下面的图像所示

第三,IPersistPropertyBag 接口是使用 HTML 页面的 PARAM 来设置属性的基类。如果您注释掉上面包含此接口的三行,您肯定只能将画笔颜色设置为黑色,而无法设置任何其他颜色。

CPClient 实现

CPClient 对象由 IE 浏览器启动时,它会获取浏览器的 IOleContainer 接口指针。然后,使用 IOleContainer::EnumObjects 函数,它会枚举浏览器页面中嵌入的所有 OLE 对象的 IUnknown 接口,当然也包括 CPClient 本身。然后 CPClient 可以找到 CPServerIUnknown 接口指针。使用此接口,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 对象即 CPClientIUnknown。而 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_sSushiUrlm_sPhotoUrl 都是类成员,分别保存一个指向不同寿司涂鸦资源的 URL。首先,有必要将光标更改为等待光标,直到任务完成,因为 Internet 访问通常太慢。std::string sLoad 封装了读取数据的块,std::istringstream*pIStringstream 将所有 sLoad 封装为流内存缓冲区,并且 std::istream*pIstream 是从 pIStringstream 创建的 I/O 流。最后的工作是恢复之前的光标。

注释

  • F5 键会导致 CPClientCPServer 通过 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 的出色文章中获得了一个想法

© . All rights reserved.