[原创]十五分钟教你写出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]] [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]] 板凳做好!:ding: 这个一定要学习下了[twfaceM045] 坐下来认真学习领会。 高亮 & 置顶 & 精华..
希望插件区 多点这样的帖子 . 少些弄exe的快捷方式. ..
插件区缺少这样的帖子.. 精华中的精华![twfaceM045] 很少用到不是exe的 看看com怎麼做的.. 没功底恐怕十五天都学不会 头有点大。。。
技术文章要支持 在学C#中。。。不过看了还是有点晕 呵呵 :cold: 强帖~呵呵,不会这个东东,来支持下会的,顶了 精华啊。
可是没时间去学 好贴啊,支持支持! 不怎么看得懂,不过还是支持 支持原创.... 强帖一定要顶 好贴子呀,改天也试试 不错!!!:ding: 不错,虽然没时间研究,但很欣赏这样的帖子! 用了五分钟发现学不会,只能坐享其成了:lol: 收藏
有空玩一下 有空再看看 太长了... 真是无私呀,知识贡献出来,就是非常值得称赞的!佩服个,大家顶顶,人家也花了力气的