MMC相关的编程接口
MMC 实现了下面的接口:
IColumnData (new in MMC 1.2)
IConsole2
IConsoleNameSpace2
IConsoleVerb
IContextMenuCallback
IContextMenuProvider
IControlbar
IDisplayHelp
IHeaderCtrl2 (new in MMC 1.2)
IImageList
IMenuButton
IPropertySheetCallback
IPropertySheetProvider
IRequiredExtensions
IResultData
IToolbar
Snap-ins 实现了下面的接口:
IComponent
IComponentData
IEnumTASK
IExtendContextMenu
IExtendControlbar
IExtendPropertySheet2
IExtendTaskPad
IRequiredExtensions
IResultDataCompare
IResultDataCompareEx (new in MMC 1.2)
IResultOwnerData
ISnapinAbout
ISnapinHelp2
IComponent接口表示了右边的结果栏(Result pane)里的对象,MMC需要和结果栏里的对象通信时使用这个接口;
IComponentData接口表示了左边的目录栏(scopet pane)里的对象(这些对象可以称之为节点),MMC需要和目录栏里的对象通信时使用这个接口;
IExtendContextMenu接口,无论用户用鼠标右键点击了左边的节点(scope pane item)还是右边的项(result pane item),MMC都会弹出一个默认的关联菜单,这时MMC会使用这个接口询问该对象(Item),是否需要添加自己的菜单项。
IExtendControlbar和IExtendPropertySheet2也是类似的道理。
ISnapinAbout实现了一个关于对话框的接口,MMC通过这个接口了解Snapin的版本、描述信息、提供者信息、对话框图标,并且特别重要的是,MMC通过这个接口得到了Snapin的所有结点、项的默认图标,要修改根结点的图标也可以通过这个函数GetStaticFolderImage来向MMC提供。
详尽的描述请参考Platform SDK。
Snapin不需要实现全部接口,Sample仅实现了其中的:
IComponent
IComponentData
IExtendContextMenu
IExtendControlbar
IExtendPropertySheet2
ISnapinAbout
MMC的ATL实现支持类简介
Sample的模型图
ATL实现了几乎全部的接口,提供了默认的处理代码。ATL Wizzard生成的代码提供了基本的编程框架,Snapin的特性实现依赖于对CmySnapinData的某些函数的重载来修改默认行为。
上图中,CMySnapinComponent的基类IExtendContextMenuImpl,IExtendContextPropertySheet,也同CMySnapin的一样,依赖于对CMySnapinData的几个同名函数的调用,只是因为线条太乱才没有画出。
Snapin DLL Sample的代码生成Step by Step
1, 新建ATL工程,如果需要使用MFC,可以使用Wizzard选项支持MFC工程命名test6;
2, “New ATL Object”,选择“MMC Snapin”;
3, 命名“MySnapin”,并在第二个属性页“MMC Snapin”上,选择IextendContextMenu,IextendControlbar,IextendPropertySheet,IsnapinAbout;并且去掉对持久性的支持;
4, 确定后,ATL Wizzard为我们生成了CMySnapin, CCMySnapinComponent, CMySnapinData,CMySnapinAbout, CMySnapinPage;
5, 编译;然后运行mmc.exe,添加一个管理单元“MySnapin”,另存为MySnapin.msc;
代码简介
CMySnapin实现了IComponentData,IExtendContextMenu,,IExtendPropertySheet三个接口。CMySnapinComponent实现了IComponent,IExtendContextMenu,IExtendControlbar,IExtendPropertySheet 四个接口,CMySnapinAbout则实现了ISnapinAbout接口。
DLL向外部暴露了CMySnapin和CMySnapinAbout两个对象。这样MMC就可以找到ISnapinAbout和IComponentData,IExtendContextMenu,,IExtendPropertySheet三个接口,然后MMC通过IComponentData的CreateComponent方法创建了CMySnapinComponent对象,从而又提供了CMySnapinComponent的IComponent,IExtendContextMenu,IExtendControlbar,IExtendPropertySheet 四个接口。
从图中可以看到,ATL的接口实现类实现了部分方法的默认行为,然后把几个重要方法如Notify等传递到CMySnapinData对象的同名函数,让其继续处理。
CMySnapinData从CSnapinItemImpl继承,CSnapinItemImpl实现了CSnapinItem接口,该接口定义了Item对象(左边的节点(scope pane item)还有右边的项(result pane item))的所有行为。
CMySnapin在构造函数里创建了CMySnapinData作为根结点。一个Item的所有行为都可以通过重载某些方法来修改默认行为。比如在根结点下面添加几个子节点,可以在重载的Notify方法中处理MMCN_EXPAND消息等等。
MSDN中的MMC Snap-In Wizard
MMC Snap-In Wizard讲了MMC Snap-In FAQ:
1. How Do I Add a Toolbar Resource to the Snap-In Object?
2. How Do I Add Menu Items, Control Bar Buttons, and a Property Sheet to the Snap-In Object?
3. How Do I Enumerate the Child Items of the Snap-In Object?
4. How Do I Add Custom Item Types to the Snap-In Object?
以上内容请自行参考msdn,按照以上的方法,可以做出一个完整功能的Snapin了。但是,非常遗憾的是,ATL Wizzard提供的无论是菜单、工具栏还是属性页都是所有的Item公用的,无论是Scope Item还是Result Item。特别是在有多个不同种类的节点的时候,这样几乎是不合适宜的。
按需定制的Item实现QA
如何生成子节点(Scope Item)?
在按照以上步骤生成代码框架以后,运行MySnapin.msc可以看到只有一个根结点,名字是MySnapin。修改这个根结点的名字,只需要修改MySnapin.cpp文件中
const OLECHAR* CMySnapinData::m_SZDISPLAY_NAME = OLESTR("MySnapin");
这一行即可。
前面讲过ATL把Item抽象成CSnapinItem接口,CSnapinItemImpl类实现了这个接口的大部分默认行为,因此,所有的Item都应该从这个类继承。考虑到CMySnapinData作为我们的根结点,ATL已经为我们生成了一些代码,从CMySnapinData继承是更好的选择。(能够重用的代码其实只有CMySnapinData构造函数中的初始化代码,不过从CMySnapinData继承的最大好处是可以将公共的实现放在CMySnapinData类中,而不需要每个Item类都实现一份相同的代码。),本文将从CMySnapinData类继承,也由于CMySnapinData将作为基类实现,因此,我们需要另外做一个根结点。
接下来我们将生成三个一级子节点,分别为类似“服务”、“事件查看器”、“帐户管理”,然后在“事件查看器”和“帐户管理”中再生成二级子节点,基本上包含了定制菜单、属性页、工具栏等。
分别从CMySnapinData继承,生成CItemRoot, CServicesContainer, CEventsContainer, CAccountsContainer。注意,在MySnapin.h文件中加入一行 #include "test6.h"。
在CMySnapin的构造函数里,我们把根结点改为CItemRoot。
在CItemRoot类中,重载Notify方法,增加代码处理MMCN_EXPAND,这个消息给了我们添加子节点(Scope Item)的机会。
case MMCN_EXPAND:
{
CComQIPtr<IConsoleNameSpace, &IID_IConsoleNameSpace>
spConsoleNameSpace(spConsole);
m_pServicesContainer->m_scopeDataItem.mask |= SDI_PARENT;
m_pServicesContainer->m_scopeDataItem.relativeID = param;
spConsoleNameSpace->InsertItem(&m_pServicesContainer->m_scopeDataItem);
m_pEventsContainer->m_scopeDataItem.mask |= SDI_PARENT;
m_pEventsContainer->m_scopeDataItem.relativeID = param;
spConsoleNameSpace->InsertItem(&m_pEventsContainer->m_scopeDataItem);
m_pAccountsContainer->m_scopeDataItem.mask |= SDI_PARENT;
m_pAccountsContainer->m_scopeDataItem.relativeID = param;
spConsoleNameSpace->InsertItem(&m_pAccountsContainer->m_scopeDataItem);
hr = S_OK;
}
break;
如何生成结果栏的项(Result Item)
从CMySnapinData继承,生成CEventEntry类,表示一个事件内容。在CEvent类中修改Notify方法的MMCN_SHOW消息,这个消息给了我们添加Result Item的机会。
如何修改子节点的名字?
每一个结点都从CMySnapinData继承,其父类CSnapinItemImpl有一个成员变量CComBSTR m_bstrDisplayName。
MMC调用IComponentData或者IComponent 的GetDisplayInfo来获得显示信息,包括结点名字,GetDisplayInfo把调用传递到GetScopePaneInfo或者GetResultPaneInfo函数,GetScopePaneInfo和GetResultPaneInfo函数默认返回m_bstrDisplayName。
因此只要在构造函数里修改m_bstrDisplayName的值即可达到目的。
如何修改子节点的图标?
在CMySnapinData中实现Notify方法的MMCN_ADD_IMAGES消息,这这里我们把所有的图标全部加载。这样其他的Item类就不需要再实现这部分代码了,只要把Notify中没有处理的消息让CMySnapinData的Notify方法处理一下即可。
对于每个Snapin都有一个IImageList接口可供使用,MMC利用这个图标列表来保存所有需要用到的图标,以后每个Item只需要指出它的图标在图标列表的索引即可:
m_scopeDataItem.nImage = 2;
m_scopeDataItem.nOpenImage = 2;
但是Scope栏中的图标却不是通过MMCN_ADD_IMAGES消息来加载的,而必须在CMySnapin类的Initialize方法中首先加载所有必要的图标。
还有,以上的方法对根结点的图标都没有作用。根结点的图标的修改必须通过修改CMySnapinAbout类GetStaticFolderImage方法才可以。也可以顺便为About对话框添加一个ICON。
如何修改子节点在结果栏中的默认显示?
Scope Item在Result pane中默认仅仅显示一个名称。比如,在选中“MySnapin”时,默认行为是在Result pane中显示一级子节点。下面我们让“事件查看器”显示更详尽的信息。
在CEventsContainer的Notify方法中修改MMCN_SHOW消息:
case MMCN_SHOW:
{
CComQIPtr<IResultData, &IID_IResultData> spResultData(spConsole);
spHeader->InsertColumn(0, L"日志", LVCFMT_LEFT, 100);
spHeader->InsertColumn(1, L"描述", LVCFMT_LEFT, 200);
hr = S_OK;
}
break;
然后在CEvent中重载GetResultPaneColInfo方法,提供每一列的显示字符串。
由于GetResultPaneColInfo是非虚函数,因此我们还得在CEvent重载GetResultPaneInfo函数。或者修改CMySnapinData的GetResultPaneColInfo函数为虚函数,那么就可以不用再重载GetResultPaneInfo了。
如何给某一个子节点添加自己独特的菜单?
由于从CSnapinItemImpl以模版方式实现,因此从CMySnapinData继承的Item类,将需要重载相应的处理函数,而不能最大限度地重用CSnapinItemImpl的实现。
所以,我们让CService等几个需要更多功能的类从CSnapinItemImpl<CService>继承。
给“服务”项目添加“启动”、“停止”菜单。
1, 在菜单资源中添加菜单IDR_MYSNAPIN_MENU1,添加菜单项“启动”、“停止”到第一个下拉菜单中,如果需要在其他菜单项上,也可以添加到其他的下拉菜单中;
2, 使用宏SNAPINMENUID(IDR_MYSNAPIN_MENU1)来告诉ATL菜单资源的ID;
3, 使用宏
BEGIN_SNAPINCOMMAND_MAP(CService, FALSE)
SNAPINCOMMAND_ENTRY(IDM_START, OnStart)
SNAPINCOMMAND_ENTRY(IDM_STOP, OnStop)
END_SNAPINCOMMAND_MAP()
来接收命令消息。
4, 并使用
HRESULT OnStart(bool& bHandled, CSnapInObjectRootBase* pObj);
HRESULT OnStop(bool& bHandled, CSnapInObjectRootBase* pObj);
来处理消息。
5, 如有必要,重载UpdateMenuState函数修改菜单状态;
如何给某一个子节点添加自己独特的工具栏?
给“服务”项目添加“启动”、“停止”工具栏按钮。
1, 资源中添加工具栏,添加按钮“启动”、“停止”;
2, 使用宏
BEGIN_SNAPINTOOLBARID_MAP(CService)
SNAPINTOOLBARID_ENTRY(IDR_TOOLBAR1)
END_SNAPINTOOLBARID_MAP()
来接收命令消息。
3, 如有必要,重载UpdateToolbarButton函数修改按钮状态;
4, 如有必要,在处理消息后,可以通过IToolbar接口修改工具栏按钮状态;
如何给某一个子节点添加自己独特的属性页?
给“帐户”添加属性页。
1, 在CAccount类中重载Notify方法。
2, 在Notify方法中处理MMCN_SELECT消息:
CComPtr<IConsoleVerb> spConsoleVerb;
if(spConsole->QueryConsoleVerb(&spConsoleVerb) != S_OK)
{
ATLTRACE("==>Query ConsoleVerb faild.\n");
return S_FALSE;
}
else
{
spConsoleVerb->SetVerbState(MMC_VERB_PROPERTIES, ENABLED,
TRUE);
hr = S_OK;
}
3, 重载CreatePropertyPages、QueryPagesFor方法,创建属性页,可以使用ATL Wizzard为我们生成的CMySnapinPage,也可以自行定制一个类似的类。
4, 如有必要,可以在属性页的OnApply函数中发出属性改变通知CAccout;
5, CAccout相应地更新自己的显示。
注意点:
SCOPEDATAITEM/RESULTDATAITEM 结构的lParam,ATL特殊用法
lParam在ATL中有特殊意义,IComponentImpl和IComponentDataImpl的GetDisplayInfo函数都假设SCOPEDATAITEM/RESULTDATAITEM 结构的lParam就是CSnapInItem对象,因此,CMySnapinData的构造函数中有m_scopeDataItem.lParam = (LPARAM) this;和m_resultDataItem.lParam = (LPARAM) this;
因此,如果我们的Item类不从CMySnapinData继承的话,那么也要同样做这些工作。
QueryPagesFor方法
如果Snapin支持属性页,那么必须在Root结点上提供一个QureyPagesFor的方法来支持,否则无法通过MMC控制台“增加”这个管理结点。