WTL入门(6)--- ActiveX控件

[ 源代码下载:http://download.csdn.net/source/3526786 ]

Introduction

这里,我将介绍在对话框中使用ActiveX控件的ATL支持。由于ATL专业处理ActiveX控件,因此WTL不涉及其他辅助类。不过,使用ActiveX控件的ATL方法与MFC有很大的不同,所以需要重点介绍。我将介绍如何使用一个ActiveX控件并处理事件,开发一个程序(与MFC类向导生成的程序相比不丢失功能)。
本章的例子演示如何使用IE浏览器控件,选择浏览器控件有两个好处:
1)每台计算机上都有这个控件
2)它有很多方法和事件处理,是个做演示的好例子

我当然无法与那些花了大量的时间编写基于浏览器控件的定制浏览器的人相比,但是,通过本篇文章,你就知道如何开始自己编写定制的浏览器。

Starting with the AppWizard

Creating the project

WTL程序创建向导可以创建一个包含ActiveX控件的程序。我们创建一个IEHoster的新工程,并使用非模态对话框形式。
WTL入门(6)--- ActiveX控件_第1张图片

The generated code

这节,我们将见到有向导生成的没见过的新代码。下一节将介绍ActiveX包容类的细节。

首先检查的是stdafx.h文件,

#include <atlbase.h>

#include <atlapp.h>

 
extern CAppModule _Module;
 
#include <atlcom.h>

#include <atlhost.h>

#include <atlwin.h>

#include <atlctl.h>

// .. other WTL headers ...

atlcom.h 和 atlhost.h是很重要的。它们含有一些COM相关类的定义(比如智能指针CComPtr),还有用来包容控件的窗口类。

下面,查看maindlg.h中的CMainDlg类:

class CMainDlg : public CAxDialogImpl<CMainDlg>,
                 public CUpdateUI<CMainDlg>,
                 public CMessageFilter, public CIdleHandler

现在,CMainDlg派生于CAxDialogImpl,这是使对话框支持包容ActiveX控件的第一步。

最后在程序入口函数WinMain()中添加一行新代码:

int WINAPI _tWinMain(...)
{
//...

    _Module.Init(NULL, hInstance);
 
    AtlAxWinInit();
 
    int nRet = Run(lpstrCmdLine, nCmdShow);
 
    _Module.Term();
    return nRet;
}

AtlAxWinInit()注册了一个类名为AtlAxWin的窗口类,ATL用它创建ActiveX控件的包容窗口。
由于ATL7的一些改变,需要在_Module.Init()的参数中添加LIBID,论坛中的一些人建议使用下面的代码:

    _Module.Init(NULL, hInstance, &LIBID_ATLLib);

Adding Controls with the Resource Editor

和MFC的程序一样,ATL也可以使用资源编辑器向对话框添加控件。首先,在对话框编辑器上点击鼠标右键,在弹出的菜单中选择“Insert ActiveX control”:
WTL入门(6)--- ActiveX控件_第2张图片

VC将会显示系统中安装的控件类表,滚动列表选择“Microsoft Web Browser”,查看控件的属性,将ID设为IDC_IE。资源编辑器中对话框中的控件显示应该是这个样子的:
WTL入门(6)--- ActiveX控件_第3张图片
如果现在编译运行程序,你会看到对话框中的浏览器控件,它将显示一个空白页,因为我们还没有告诉它打开哪个网页。
在下一节,我将介绍与创建和包容ActiveX控件有关的ATL类,同时我们也会明白这些类是如何与浏览器交换信息的。

ATL Classes for Control Hosting

当在对话框中包容ActiveX控件,需要CAxDialogImplCAxWindow这两个类协同工作。它们提供了所有控件容器必须实现的接口方法,提供通用的功能函数,例如查询控件的某个特殊的COM接口。

CAxDialogImpl

第一个类是CAxDialogImpl,要使你的对话框能够包容ActiveX控件就必须从CAxDialogImpl派生。CAxDialogImpl类重载了Create()和DoModal()函数,这两个函数分别被全局函数AtlAxCreateDialog()和AtlAxDialogBox()调用。既然IEHoster对话框是由Create()创建的,我们看看AtlAxCreateDialog()到底做了什么工作。

AtlAxCreateDialog()装载对话框资源,并使用辅助类_DialogSplitHelper遍历对话框的所有控件,查找由资源编辑器创建的特殊条目,这些特殊条目就表示这是一个ActiveX控件。例如,下面是IEHoster.rc文件中浏览器控件的入口:

CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
        WS_TABSTOP,7,7,116,85

第一个参数是窗口文字(空字符串),第二个是控件的ID,第三个是窗口的类名。_DialogSplitHelper::SplitDialogTemplate()函数找到以'{'开始的窗口类名时就知道这是一个表示ActiveX控件的条目。它在内存中创建了一个临时对话框模板,在这个新模板中这些特殊的控件条目被创建的AtlAxWin窗口对应项代替,新的入口是在内存中的等价体:

CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}", 
         IDC_IE, "AtlAxWin", WS_TABSTOP, 7, 7, 116, 85

结果就是创建了一个具有相同ID的AtlAxWin窗口,窗口的标题是ActiveX控件的GUID。所以你调用GetDlgItem(IDC_IE)返回的值是AtlAxWin窗口的句柄而不是ActiveX控件本身。
SplitDialogTemplate()函数完成工作后,AtlAxCreateDialog()接着调用CreateDialogIndirectParam()函数使用修改后的模板创建对话框。

AtlAxWin and CAxWindow

如上面所述,AtlAxWin实际上是ActiveX控件的宿主窗口,AtlAxWin还会用到一个特殊的窗口接口类:CAxWindow。当AtlAxWin从模板创建一个对话框后,AtlAxWin执行窗口处理AtlAxWindowProc(),就会处理WM_CREATE消息并创建相应的ActiveX控件。ActiveX控件还可以在运行其间动态创建,不需要对话框模板,我会在后面介绍这种方法。

WM_CREATE的消息处理函数调用全局函数AtlAxCreateControl(),将AtlAxWin窗口的窗口文本作为参数传递给该函数,那实际就是浏览器控件的GUID。AtlAxCreateControl()有会调用一堆其他函数,不过最终会用到CreateNormalizedObject()函数,这个函数将窗口文本转换成GUID,并最终调用CoCreateInstance()创建ActiveX控件。

由于ActiveX控件是AtlAxWin的子窗口,所以对话框不能直接访问控件,当然CAxWindow提供了这些方法通控件通信,最常用的一个是QueryControl(),这个方法调用控件的QueryInterface()方法。例如,你可以使用QueryControl()从浏览器控件得到IWebBrowser2接口,然后使用这个接口将浏览指定的URL。

Calling the Methods of a Control

因为我们的对话框有一个浏览器控件,我们可以使用COM接口与之交互。我们做得第一件事情就是使用IWebBrowser2接口将其导航到一个新URL处。在OnInitDialog()函数中,我们将一个CAxWindow变量与包容控件的AtlAxWin类关联起来。

BOOL CMainDlg::OnInitDialog()
{
    CAxWindow wndIE = GetDlgItem(IDC_IE);

然后声明一个IWebBrowser2的接口指针并获得浏览器控件的这个接口,使用CAxWindow::QueryControl():

    CComPtr<IWebBrowser2> pWB2;
    HRESULT hr;
 
    hr = wndIE.QueryControl ( &pWB2 );

QueryControl()调用浏览器控件的QueryInterface()方法,如果成功就会返回IWebBrowser2接口,我们可以调用Navigate():
QueryControl()调用浏览器控件的QueryInterface()方法,如果成功就会返回IWebBrowser2接口,我们可以调用Navigate():

    if ( pWB2 )
    {
    CComVariant v;  // empty variant

    pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"), 
                     &v, &v, &v, &v );
    }

Sinking Events Fired by a Control

从浏览器控件得到接口非常简单,通过它可以与控件单向通信。通常控件也会以事件的形式与外界通信,ATL有专用的类包装连接点和事件响应,所以我们可以从控件接收到这些事件。为使用对事件的支持需要做四件事:

1)CMainDlg类的继承列表中添加IDispEventImpl
2)填写事件映射链,它指示哪些事件需要处理
3)编写事件响应函数
4)关联控件和事件映射链(advising

VC IDE 可以帮助我们改变CMainDlg,不仅排序显示ActiveX控件类型列表,还可显示控件事件列表。可以帮助我们添加事件的处理,本文中只介绍VC7中添加事件处理的方法。

Adding handlers in VC 7

这里有两种方法添加事件处理。可以在对话框编辑器中右击鼠标,选择“Add Event Handler ”,弹出一个对话框用于选择事件并命名事件处理函数。
WTL入门(6)--- ActiveX控件_第4张图片

另一种方法是查看CMainDlg的属性页,展开控件节点,在对应浏览器控件ID下,可以添加事件的处理

Advising for events

最后一步,通知(advise)控件CMainDlg想要接受的浏览器控件触发的消息。advising 发生在0OnInitDialog(), unadvising 发生在OnDestroy().

Advising in VC 6

VC6中,ATL的全局函数AtlAdviseSinkMap()通知(advise)对话框中所有控件开始或终止发送事件到C++对象。这个该函数的第一个参数是一个指向拥有事件映射链的C++对象指针(通常是this),第二个参数是一个bool变量,如果为true表示这个对象开始接收事件,否则表示终止接收事件。要使用这个函数,需要处理WM_INITDIALOGWM_DESTROY消息,并调用如下:

BOOL CMainDlg::OnInitDialog(...)
{
  // Begin sinking events

  AtlAdviseSinkMap ( this, true );
}
 
void CMainDlg::OnDestroy()
{
  // Stop sinking events

  AtlAdviseSinkMap ( this, false );
}

AtlAdviseSinkMap()返回一个HRESULT,表示函数执行是否成功。如果OnInitDialog()中AtlAdviseSinkMap()执行失败,那么就会无法接收一些(或所有)ActiveX控件的事件。

Advising in VC 7

VC7中,CAxDialogImpl 提供了一个封装AtlAdviseSinkMap()的方法:AdviseSinkMap()AdviseSinkMap()只有一个bool型参数,它与AtlAdviseSinkMap()的第二个参数意义相同。AdviseSinkMap() 检查拥有事件映射表的类,并调用AtlAdviseSinkMap()
与VC6不同的是CAxDialogImpl已经处理了消息WM_INITDIALOGWM_DESTROY,并调用了AdviseSinkMap(),因此只需在CMainDlg的消息映射表的开始部分添加宏CHAIN_MSG_MAP。

BEGIN_MSG_MAP(CMainDlg)
    CHAIN_MSG_MAP(CAxDialogImpl<CMainDlg>)
    // rest of the message map...

  END_MSG_MAP()

Overview of the Sample Project

现在,我们已经知道消息映射如何工作,我们的IEHost项目,包容一个浏览器控件,处理六个事件:

1)BeforeNavigate2NavigateComplete2:这些事件让程序可以控制URL的导航,如果你响应了BeforeNavigate2事件,你可以在事件的处理函数中取消导航
2)DownloadBeginDownloadComplete::程序使用这些事件控制“wait”消息,这表示浏览器正在工作。一个更优美的程序会像IE一样在此期间使用一段动画。
3)CommandStateChange:这个事件告诉程序向前和向后导航命令何时可用,应用程序将相应的按钮变为可用或不可用。
4)StatusTextChange:这个事件会在几种情况下触发,例如鼠标移到一个超链接上。这个事件发送一个字符串,应用程序响应这个事件,将这个字符串显示在浏览器窗口下的静态控件上。

程序有四个按钮控制浏览器工作:向后,向前,停止和刷新,它们分别调用IWebBrowser2相应的方法。
事件和伴随事件发送的数据都被记录在列表控件中,你可以看到事件的触发,你还可以关闭一些事件记录而仅仅观察其中的一辆个事件。为了演示事件处理的重要作用,我们在BeforeNavigate2事件处理函数中检查URL,如果发现“doubleclick.net”就取消导航。广告和弹出窗口过滤器等一些IE的插件使用的就是这个方法而不是HTTP代理,下面就是做这些检查的代码。

void __stdcall CMainDlg::OnBeforeNavigate2 (
    IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, 
    VARIANT* TargetFrameName, VARIANT* PostData, 
    VARIANT* Headers, VARIANT_BOOL* Cancel )
{
    CString sURL = URL->bstrVal;
 
    // You can set *Cancel to VARIANT_TRUE to stop the 

    // navigation from happening. For example, to stop 

    // navigates to evil tracking companies like doubleclick.net:

    if ( sURL.Find ( _T("doubleclick.net") ) > 0 )
        *Cancel = VARIANT_TRUE;
}

下面就是我们的程序工作起来的样子:
下面就是我们的程序工作起来的样子:WTL入门(6)--- ActiveX控件_第5张图片

IEHoster还使用了前几章介绍过得类:CBitmapButton(用于浏览器控制按钮),CListViewCtrl(用于事件记录),DDX (跟踪checkbox的状态)和CDialogResize。

Creating an ActiveX Control at Run Time

除了使用资源编辑器,还可以在运行其间动态创建ActiveX控件。About对话框演示了这种技术。对话框编辑器预先放置了一个group box用于浏览器控件的定位:WTL入门(6)--- ActiveX控件_第6张图片

在OnInitDialog()函数中我们使用 CAxWindow创建了一个新AtlAxWin,它定位于我们预先放置好的group box的位置上(这个group box随后被销毁):

LRESULT CAboutDlg::OnInitDialog(...)
{
    CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );
    CRect rc;
    CAxWindow wndIE;
 
    // Get the rect of the placeholder group box, then destroy 

    // that window because we don't need it anymore.

    wndPlaceholder.GetWindowRect ( rc );
    ScreenToClient ( rc );
    wndPlaceholder.DestroyWindow();
 
    // Create the AX host window.

    wndIE.Create ( *this, rc, _T(""), 
                   WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );

接下来我们用CAxWindow方法创建一个ActiveX控件,有两个方法可以选择:CreateControl()和CreateControlEx()。CreateControlEx()用一个额外的参数返回接口指针,这样就不需要再调用QueryControl()函数。我们感兴趣的两个参数是第一个和第四个参数,第一个参数是字符串形式的浏览器控件的GUID,第四个参数是一个IUnknown*类型的指针,这个指针指向ActiveX控件的IUnknown接口。创建控件后就可以查询IWebBrowser2接口,然后就可以像前面一样控制它导航到某个URL。

    CComPtr<IUnknown> punkCtrl;
    CComQIPtr<IWebBrowser2> pWB2;
    CComVariant v;    // empty VARIANT

 
    // Create the browser control using its GUID.

    wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}", 
                            NULL, NULL, &punkCtrl );
 
    // Get an IWebBrowser2 interface on the control and navigate to a page.

    pWB2 = punkCtrl;
    pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );
}

对于有ProgID的ActiveX控件可以传递ProgID给CreateControlEx(),代替GUID。例如,我们可以这样创建浏览器控件:

   // Use the control's ProgID, Shell.Explorer:
    wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
                            NULL, &punkCtrl );

CreateControl()和CreateControlEx()还有一些重载函数用于一些使用浏览器的特殊情况,如果你的应用程序使用WEb页面作为HTML资源,你可以将资源ID作为第一个参数,ATL会创建浏览器控件并导航到这个资源。IEHoster包含一个ID为IDR_ABOUTPAGE的WEB页面资源,我们在About对话框中使用这些代码显示这个页面:

    wndIE.CreateControl ( IDR_ABOUTPAGE );

这是结果:
WTL入门(6)--- ActiveX控件_第7张图片
CreateControl()和CreateControlEx()还有一些重载函数用于一些使用浏览器的特殊情况,如果你的应用程序使用WEb页面作为HTML资源,你可以将资源ID作为第一个参数,ATL会创建浏览器控件并导航到这个资源。IEHoster包含一个ID为IDR_ABOUTPAGE的WEB页面资源,我们在About对话框中使用这些代码显示这个页面:接下来我们用CAxWindow方法创建一个ActiveX控件,有两个方法可以选择:CreateControl()和CreateControlEx()。CreateControlEx()用一个额外的参数返回接口指针,这样就不需要再调用QueryControl()函数。我们感兴趣的两个参数是第一个和第四个参数,第一个参数是字符串形式的浏览器控件的GUID,第四个参数是一个IUnknown*类型的指针,这个指针指向ActiveX控件的IUnknown接口。创建控件后就可以查询IWebBrowser2接口,然后就可以像前面一样控制它导航到某个URL。在OnInitDialog()函数中我们使用 CAxWindow创建了一个新AtlAxWin,它定位于我们预先放置好的group box的位置上(这个group box随后被销毁):例子代码对上面提到的三个方法都用到了,你可以查看CAboutDlg::OnInitDialog()中的注释和未注释的代码,看看它们分别是如何工作的。

Keyboard Handling

最后一个但是非常重要的细节是键盘消息。ActiveX控件的键盘处理非常复杂,因为控件和它的宿主程序必须协同工作以确保控件能够看到它感兴趣的消息。例如,浏览器控件允许你使用TAB键在链接之间切换。MFC自己处理了所有工作,所以你永远不会意识到让键盘完美并正确的工作需要多么大的工作量。

不幸的是向导没有为基于对话框的程序生成键盘处理代码,当然,如果你使用Form View作为视图类的SDI程序,你会看到必要的代码已经被添加到PreTranslateMessage()中。当程序从消息队列中得到鼠标或键盘消息时,就使用ATL的WM_FORWARDMSG消息将此消息传递给当前拥有焦点的控件。它们通常不作什么事情,但是如果是ActiveX控件,WM_FORWARDMSG消息最终被送到包容这个控件的AtlAxWin,AtlAxWin识别WM_FORWARDMSG消息并采取必要的措施看看是否控件需要亲自处理这个消息。

如果拥有焦点的窗口没有识别WM_FORWARDMSG消息,PreTranslateMessage()就会接着调用IsDialogMessage()函数,使得像TAB这样的标准对话框的导航键能正常工作。

例子工程的PreTranslateMessage()函数中含有这些必需的代码,由于PreTranslateMessage()只在无模式对话框中有效,所以如果你想在基于对话框的应用程序中正确使用键盘就必须使用无模式对话框。

 

原文:WTL for MFC Programmers, Part VI - Hosting ActiveX Controls

你可能感兴趣的:(WTL入门(6)--- ActiveX控件)