世界之窗论坛's Archiver

henryouly 发表于 2008-8-31 14:22

[原创]十五分钟教你写出TheWorld的COM状态栏插件

[b]本文的对象是有基本visual studio开发经验,懂得使用visual studio appwizard来创建各种项目和类,懂得C++和Windows编程的基本概念,但可能没有COM组件开发经验的开发者。[/b]

什么是COM?通俗来说,COM是一套在Windows内部广泛使用的接口标准。它的好处很多,例如跨语言,跨平台,二进制兼容等等,但是我们不需要了解过多,在这篇文章里面,我们只需要记住,IE平台其实也是由成百上千个COM组件搭建起来的,各个组件之间通过COM定义了一套标准的通讯语言。TheWorld是基于IE内核的一个浏览器框架实现,所以也遵循着这些接口标准。

TheWorld的状态栏插件其实是一个简单的COM组件,它使用了标准的IDeskBand接口,是一个标准的符合微软规范的Band Object(不知道BandObject的可以参考知识库文章[url=http://msdn.microsoft.com/en-us/library/bb776819]http://msdn.microsoft.com/en-us/library/bb776819[/url](VS.85).aspx,不过这不影响我们后面内容的理解)。TheWorld浏览器通过这个标准的COM接口和插件进行通讯,包括通知插件进行加载,重画等,插件由此可以实现强大的几乎无所不能的功能。(Javascript插件受语言本身的限制很大,基本只能实现一些很简单的网页渲染和数据转换功能。)

我们选择用ATL来实现这个COM组件。ATL是一套封装COM的通用实现,以便开发者进行快速开发的轻量级类库。本文以VC6为例讲解具体的操作流程,VC2005及VC2008的操作大同小异,不再赘述。

[b]COM组件的实现
[/b]
首先,新建一个项目,选择ATL COM AppWizard,项目名字我们选TWPluginDev,Server类型我们选DLL,保持其他选项为默认值,确定。这样VC自动帮我们创建好了一个ATL项目。

然后,在ClassView当中创建一个新的ATL Class,类名CTWPlugin,接口类型我们选择相对简单的Custom,保持其他选项为默认值,确定。此时,ClassView里面就新增添了一个COM接口ITWPlugin和对应的实现类CTWPlugin。

我们打开CTWPlugin的头文件TWPlugin.h,VC的Wizard已经为我们自动产生了这个类的定义:

[font=Courier New][color=#38761d][code]class CTWPlugin :
    public ITWPlugin,
    public CComObjectRoot,
    public CComCoClass<CTWPlugin,&CLSID_TWPlugin>[/code][/color][/font]

一个完整的Band Object需要支持IDispatch、IObjectWithSite和IDeskBand三个接口。前两个我们可以用ATL的默认实现,第三个是从浏览器组件里面来的,ATL并没有带默认实现,所以我们一会儿还需要实现它。修改后如下:

[font=Courier New][color=#38761d][code]class CTWPlugin :
    public CComObjectRoot,
    public CComCoClass<CTWPlugin,&CLSID_TWPlugin>,
    public IDispatchImpl<ITWPlugin, &IID_ITWPlugin, &LIBID_TWPLUGINDEVLib>,
    public IObjectWithSiteImpl<CTWPlugin>,
    public IDeskBand[/code][/color][/font]


添加头文件,让编译器能够找到IDeskBand

[font=Courier New][color=#38761d][code]#include <shlobj.h>
#include <comdef.h>[/code][/color][/font]


由于我们新增了三个接口,为了实现COM的接口转换,我们还需要添加如下的接口映射,修改后:

[color=#38761d][font=Courier New][code]BEGIN_COM_MAP(CTWPlugin)
    COM_INTERFACE_ENTRY(ITWPlugin)
    COM_INTERFACE_ENTRY2(IDispatch, ITWPlugin)
    COM_INTERFACE_ENTRY(IObjectWithSite)
    COM_INTERFACE_ENTRY(IDeskBand)
    COM_INTERFACE_ENTRY2(IOleWindow, IDeskBand)
    COM_INTERFACE_ENTRY2(IDockingWindow, IDeskBand)
END_COM_MAP()[/code][/font][/color]

接下来,我们需要给出IDeskBand的具体实现。把IDeskBand中需要被实现的接口加到TWPlugin.h的public函数当中

[color=#38761d][font=Courier New][code]    // *** IOleWindow methods ***
    STDMETHOD(GetWindow) (THIS_ HWND * lphwnd);
    STDMETHOD(ContextSensitiveHelp) (THIS_ BOOL fEnterMode);

    // *** IDockingWindow methods ***
    STDMETHOD(ShowDW)         (THIS_ BOOL fShow);
    STDMETHOD(CloseDW)        (THIS_ DWORD dwReserved);
    STDMETHOD(ResizeBorderDW) (THIS_ LPCRECT   prcBorder,
                                     IUnknown* punkToolbarSite,
                                     BOOL      fReserved);
    // *** IDeskBand methods ***
    STDMETHOD(GetBandInfo)    (THIS_ DWORD dwBandID, DWORD dwViewMode,
                                DESKBANDINFO* pdbi);[/code][/font][/color]

在TWPlugin.cpp当中,我们给出上面接口的空实现(mock up)。

[color=#38761d][font=Courier New][code]STDMETHODIMP CTWPlugin::GetWindow(HWND *phWnd)
{
    return E_NOTIMPL;
}

STDMETHODIMP CTWPlugin::ContextSensitiveHelp(BOOL fEnterMode)
{
    return S_OK;
}

STDMETHODIMP CTWPlugin::ShowDW(BOOL fShow)
{
    return E_NOTIMPL;
}

STDMETHODIMP CTWPlugin::CloseDW(DWORD dwReserved)
{
    return S_OK;
}

STDMETHODIMP CTWPlugin::ResizeBorderDW( LPCRECT prcBorder, IUnknown* punkSite, BOOL fReserved)
{
    return E_NOTIMPL;
}

STDMETHODIMP CTWPlugin::GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO* pdbi)
{
    return E_NOTIMPL;
}[/code][/font][/color]

此时,我们插件的COM接口部分代码已经实现完毕。

[b]插件界面的实现[/b]

下面让我们来把UI加上去。实现方法有多种,如Win32API、MFC等,本文的例子选择用WTL——一套基于ATL的轻量级窗口运行库。

在TWPlugin.h当中加入头文件<atlwin.h>,并给CTWPlugin再添加一个基类CWindowImpl<CTWPlugin>和一个私有成员DWORD m_dwBandID,在构造函数中初始化为-1,加入窗口消息映射
[color=#38761d][font=Courier New][code]BEGIN_MSG_MAP(CTWPlugin)
    MESSAGE_HANDLER(WM_PAINT, OnPaint)
    MESSAGE_HANDLER(WM_CREATE, OnCreate)
END_MSG_MAP()[/code][/font][/color]


另外创建两个消息**函数[font=Courier New]OnPaint[/font]和[font=Courier New]OnCreate[/font]:
[color=#38761d][font=Courier New][code]LRESULT OnPaint( UINT, WPARAM, LPARAM, BOOL& );
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& );[/code][/font][/color]


并在TWPlugin.cc当中实现
[code]LRESULT CTWPlugin::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bResult) {
    PAINTSTRUCT ps;
    HDC hDC = BeginPaint(&ps);
    TextOut(hDC, 0, 0, _T("Hello"), 5);
    EndPaint(&ps);
    return 0;
}  
LRESULT CTWPlugin::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bResult) {
    // Rebar Info
    if( m_dwBandID != -1 )
    {
        REBARBANDINFO    rbBand;
        rbBand.cbSize        = sizeof(REBARBANDINFO);
        rbBand.fMask        = RBBIM_CHILDSIZE | RBBIM_IDEALSIZE | RBBIM_ID;
        rbBand.wID            = m_dwBandID;


        HWND hWndReBar = GetParent();
        int nIndex = ::SendMessage( hWndReBar, RB_IDTOINDEX, m_dwBandID, NULL );
        ::SendMessage( hWndReBar, RB_GETBANDINFO, nIndex, (LPARAM)&rbBand );

        rbBand.cxIdeal        = 50;
        rbBand.cxMinChild    = 50;
        ::SendMessage( hWndReBar, RB_SETBANDINFO, nIndex, (LPARAM)&rbBand );
    }
    return 0;
}[/code]
窗体实现完毕。

[[i] 本帖最后由 needed 于 2008-9-1 13:47 编辑 [/i]]

henryouly 发表于 2008-8-31 14:23

[b]把COM组件接口和插件关联起来[/b]

为了让TW的主程序能够通过接口调用我们的插件,我们还需要在接口当中实现我们的调用逻辑。为此,我们需要实现SetSite,GetBandInfo,GetWindow三个函数

SetSite是从IObjectWithSite继承下来的,作用是通知插件标签页被切换。我们可以在这里做一些重画工作。

[font=Courier New][code]STDMETHODIMP CTWPlugin::SetSite(IUnknown* punkSite)
{
     if(punkSite)
    {
        if( !IsWindow() )
        {
            HWND hWndReBar = NULL;
            {
                CComQIPtr<IOleWindow> pOleWindow(punkSite);
                pOleWindow->GetWindow(&hWndReBar);
            }
            if( !::IsWindow( hWndReBar ) ) return E_FAIL;
            if (IsWindow()) DestroyWindow();
            Create(hWndReBar, CWindow::rcDefault);
            if (!IsWindow()) return E_FAIL;
        }
    }

    return S_OK;
}[/code]
[/font]

GetBandInfo用于获得Band对象的显示大小,这里我们假设插件的显示大小固定为120×24。代码如下:

[font=Courier New][code]STDMETHODIMP CTWPlugin::GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO* pdbi)
{
    m_dwBandID = dwBandID;
    int nHeight = 24;
    int nWidth  = 120;

    if( pdbi)
    {
        if( pdbi->dwMask & DBIM_MINSIZE)
        {
            pdbi->ptMinSize.x = nWidth;            //最小显示尺寸
            pdbi->ptMinSize.y = nHeight;
        }

        if( pdbi->dwMask & DBIM_MAXSIZE)
        {
            pdbi->ptMaxSize.x = 0;            //该参数没有用
            pdbi->ptMaxSize.y = nHeight;
        }


        if( pdbi->dwMask & DBIM_MODEFLAGS)
        {
            pdbi->dwModeFlags = DBIMF_NORMAL;
        }

        if( pdbi->dwMask & DBIM_ACTUAL)
        {
            pdbi->ptActual.x = 50;            //超过这个尺寸不显示图标
            pdbi->ptActual.y = nHeight;
        }


        if (pdbi->dwMask & DBIM_TITLE)
        {
            ZeroMemory(pdbi->wszTitle, sizeof(pdbi->wszTitle));
        }
    }
    return S_OK;
}[/code][/font]

最后,我们需要实现GetWindow,用于获取插件的窗口句柄HWND

[font=Courier New][code]STDMETHODIMP CTWPlugin::GetWindow(HWND *phWnd)
{
    if ( NULL == phWnd )
        return E_INVALIDARG;
    *phWnd = m_hWnd;
    return S_OK;
}[/code][/font]

至此,插件的代码开发工作就全部完成了。发布前记住要把dll编译成Release版本,而不要直接发布debug版。我一般会选择Release MinDependency。


[font=Verdana]作为最后的收官工作,要写一个plugin.ini放到插件目录下,声明版本号,插件文件名,CLSID等信息,以便TW的插件管理器读取。下面是一个例子[/font]

[font=Courier New][code][General]
Name=TWPluginDev
NameCN=开发示例
Author=henryouly
Version=1.0.0.1
Comments=Dev sample
FileName=TWPluginDev.dll
HotIcon=
Type=STATUSBAR
ModuleType=COM
CLSID={43B6A73E-C284-4F98-8328-ABE004C1EF7A}
StartAfterPageDone=[/code][/font]


最后,享受COM插件给你带来的乐趣吧。

[[i] 本帖最后由 needed 于 2008-9-1 13:45 编辑 [/i]]

495127903 发表于 2008-8-31 15:00

板凳做好!:ding:

haokeyy 发表于 2008-8-31 15:20

这个一定要学习下了[twfaceM045]

westone 发表于 2008-8-31 15:23

坐下来认真学习领会。

needed 发表于 2008-8-31 16:55

高亮 & 置顶 & 精华..

  希望插件区 多点这样的帖子 . 少些弄exe的快捷方式. ..

插件区缺少这样的帖子..

东暴 发表于 2008-8-31 17:14

精华中的精华![twfaceM045]

ad6543210 发表于 2008-8-31 17:48

很少用到不是exe的 看看com怎麼做的..

fghxy 发表于 2008-8-31 18:26

没功底恐怕十五天都学不会

kc1143 发表于 2008-8-31 19:44

头有点大。。。
技术文章要支持

glq10107 发表于 2008-9-1 01:34

在学C#中。。。不过看了还是有点晕 呵呵 :cold:

sbyguli 发表于 2008-9-1 09:23

强帖~呵呵,不会这个东东,来支持下会的,顶了

yjwgi 发表于 2008-9-1 09:44

精华啊。
可是没时间去学

sky5 发表于 2008-9-1 10:17

好贴啊,支持支持!

mulao 发表于 2008-9-1 12:59

不怎么看得懂,不过还是支持

Coptis 发表于 2008-9-1 13:23

支持原创....

mutalisker 发表于 2008-9-1 17:28

强帖一定要顶

starsoft 发表于 2008-9-1 18:29

好贴子呀,改天也试试

lixupeng 发表于 2008-9-4 12:16

不错!!!:ding:

baifengs 发表于 2008-9-5 09:26

不错,虽然没时间研究,但很欣赏这样的帖子!

cyberholic 发表于 2008-9-5 15:51

用了五分钟发现学不会,只能坐享其成了:lol:

vlolv 发表于 2008-9-9 14:20

收藏
有空玩一下

412268499 发表于 2008-9-13 18:16

有空再看看 太长了...

kidsuntown 发表于 2008-9-16 10:21

真是无私呀,知识贡献出来,就是非常值得称赞的!佩服个,大家顶顶,人家也花了力气的

页: [1] 2 3 4

Powered by Discuz! Archiver 7.2  © 2001-2009 Comsenz Inc.