ATL GUI (五)

一点一点的搞界面好无聊,还是直接搞ActiveX吧,没有比这个爽的
再说本来就是用来搞这个的。

这一节直接搞个例子玩,

先使用无模式对话框,要选上支持ActiveX控件包容

添加一个浏览器的ActiveX的控件。

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

CAxDialogImpl
第一个类是CAxDialogImpl,你的对话框要能够包容控件就必须从CAxDialogImpl类派生而不是从CDialogImpl类派生。
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控件。
(总算是建立了一个com,等了好久,原来在这里)

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


调用控件的方法
CAxWindow wndIE = GetDlgItem(IDC_IE);

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

QueryControl()调用浏览器控件的QueryInterface()方法,如果成功就会返回IWebBrowser2接口,我们可以调用Navigate():
if ( pWB2 )
        {
        CComVariant v;  // empty variant
 
        pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"),
                         &v, &v, &v, &v );
        }


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

   1. 将CMainDlg变成COM对象
   2. 添加IDispEventSimpleImpl到CMainDlg的继承列表
   3. 填写事件映射链,它指示哪些事件需要处理
   4. 编写事件响应函数
(其实就是写个sink)

将CMainDlg转变成COM对象的原因是事件相应是基于IDispatch的,为了让CMainDlg暴露这个接口,它必须是个COM对象。
IDispEventSimpleImpl提供了IDispatch接口的实现和建立连接点所需的处理函数,当事件发生时 IDispEventSimpleImpl还调用我们想要接收的事件的处理函数。

做点小小的修改就可以把一个普通的类变成一个com
加头,没有这个就没有哪些GUID定义(好像#import也行哦)
#include     // browser control definitions
#include   // browser event dispatch ID

加入继承
 public CComObjectRootEx,
 public CComCoClass,
 public IDispEventSimpleImpl<37, CMainDlg, &DIID_DWebBrowserEvents2>

加个宏
BEGIN_COM_MAP(CMainDlg)
    COM_INTERFACE_ENTRY2(IDispatch, IDispEventSimpleImpl)
END_COM_MAP()

CComObjectRootEx类CComCoClass共同使CMainDlg成为一个COM对象, IDispEventSimpleImpl的模板参数是事件的ID,
我们的类名和连接点接口的IID。事件ID可以是任意正数,连接点对象的IID是 DIID_DWebBrowserEvents2,
可以在浏览器控件的相关文档中找到这些参数,也可以查看exdisp.h。

写个这个
#define _ATL_MAX_VARTYPES 8
 
struct _ATL_FUNC_INFO
{
    CALLCONV cc;
    VARTYPE  vtReturn;
    SHORT    nParams;
    VARTYPE  pVarTypes[_ATL_MAX_VARTYPES];
};

参数:

cc
    我们的事件响应函数的调用方式约定,这个参数必须是CC_STDCALL,表示是__stdcall方式
vtReturn
    事件响应函数的返回值类型
nParams
    事件带的参数个数
pVarTypes
    相应的参数类型,按从左到右的顺序

了解这些之后,我们就可以填写DownloadBegin事件处理的_ATL_FUNC_INFO结构:

_ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 };

现在,回到事件响应链,我们为每一个我们想要处理的事件添加一个SINK_ENTRY_INFO宏,下面是处理DownloadBegin事件的宏:

class CMainDlg : public ...
{
...
  BEGIN_SINK_MAP(CMainDlg)
    SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,
                    OnDownloadBegin, &DownloadInfo)
  END_SINK_MAP()
};

这个宏的参数是事件的ID(37,与我们在IDispEventSimpleImpl的继承列表中使用的ID一样),事件接口的IID,
事件的dispatch ID(可以在MSDN或exdispid.h头文件中查到),事件处理函数的名字和指向描述这个事件处理的_ATL_FUNC_INFO结构的指针。

将CMainDlg转换成对象需要注意几件事情,首先必须修改全局函数Run(),现在CMainDlg是个COM对象,我们必须使用CComObject创建CMainDlg:

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
    CMessageLoop theLoop;
    _Module.AddMessageLoop(&theLoop);
 
CComObject dlgMain;
 
    dlgMain.AddRef();
 
    if ( dlgMain.Create(NULL) == NULL )
        {
        ATLTRACE(_T("Main dialog creation failed!/n"));
        return 0;
        }
 
    dlgMain.ShowWindow(nCmdShow);
 
    int nRet = theLoop.Run();
 
    _Module.RemoveMessageLoop();
    return nRet;
}

运行时创建ActiveX控件

出了使用资源编辑器,还可以在运行其间动态创建ActiveX控件。About对话框演示了这种技术。对话框编辑器预先放置了一个group box用于浏览器控件的定位:
在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 punkCtrl;
CComQIPtr pWB2;
CComVariant v;
 
    // 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。例如,我们可以这样创建浏览器控件:

    // 使用控件的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 );

键盘事件处理

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

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

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


就是这个样子,WTL对话框使用ActiveX控件就是这样的

文章转至http://www.imyaker.com/wtl/,加入了自己的理解。
   


你可能感兴趣的:(ATL GUI (五))