您的位置:首页 > 编程语言 > C语言/C++

【outlook plugin】利用VC++/ATL开发Office 2003 COM插件

2012-03-22 17:10 627 查看
利用VC++/ATL开发Office 2003 COM插件

最近,我为一个客户写了一个Outlook2003的COM插件。当我为这个工程写代码的时候,我遇到了很多用C++无法解决的问题。对于一个初学者来说,用ATL编写插件是非常棘手的。网上大多数Office开发的例子都是VB/VBA相关的,几乎没有用ATL开发的。所以,我整理了一些知识,希望能够对大家有所帮助。

在这篇文章里的代码并没有进行优化,并且附带的例子可能有一些内存泄露,也会有一些COM实现上的不足。但为了使读者便于理解,我尽量使实现过程简单化。为了写这篇文章我花了很多时间,万一还存在什么错误,请给我发个邮件。

概况:

如果你是个COM/ATL的初学者,推荐你阅读Amit Dey’s article for building an Outlook 2000 add-in

这篇文章将要讲述下面的技术:

基本的Outlook 2003 插件
接收Explorer事件
结合CDO和Outlook对象模型
利用CDO查看消息的安全性
利用CDO为Outlook项目添加自定义区域
以编程方式定制条目的分组和分类
向右键菜单添加新项
以编程的方式利用MSI加载CDO

Office COM插件必须实现IDTExtensibility2接口。IDTExtensibility2接口定义于MSADDIN Designer typelibrary (MSADDNDR.dll/MSADDNDR.tlb)文件中,所有继承于IDTExtensibility2接口的COM插件必须实现5个方法:

OnConnection
OnDisconnection
OnAddinUpdate
OnBeginShutDown
OnStartupComplete

注册:

所有Office插件都是注册到以下注册表条目的(Outlook指代应用程序名字)

HKEY_CURRENT_USER/Software/Microsoft/Office/Outlook/Addins/<ProgID>

除此之外,插件还可以通过其他注册表条目来识别Outlook。

开始:

我们先来编写一个最基本的COM插件。然后,我们从头开始一步步地生成一个插件,这个插件将把简单邮件和加密邮件分到不同的组中。

这篇文章假定一你是个VC++ COM程序员,并且也有一些基于ATL的组件开发和OLE/自动化方面的经验,尽管这也不是必须的。插件是为Outlook 2003设计的,所以你机器上必须安装带有CDO的Office 2003。程序代码使用VC++ 6.0 sp3+/ATL3.0创建,在安装有Office 2003的WinXP Pro SP1上通过测试。

启动VC++开发环境,新建一个工程,选择ATL COM AppWizard,为工程命名为Outlook Addin,确定。选择Dynamic Link Library,完成。

然后,点击菜单“插入”->“新建ATL对象”,选择“Simple Object”,命名为OAddin,选择Attributes标签,选中Support ISupportErrorInfo,其他选项默认。

现在,我们可以来实现IDTExtensibility2接口了。在COAddin类上点右键,选择Implement Interface,弹出Browse Type library向导对话框。选择Microsoft Add-in Designer(1.0),OK。如果没有列出来,要到文件夹<drive>/Program Files/Common Files/Designer里面去找。

向导实现了我们所选择的接口,并为IDTExtensibility2接口的5个方法提供了默认实现。现在,一个基本的自动化COM对象就准备好了。通过向rgs文件添加注册条目,我们就可以用Outlook来注册这个插件。打开文件OAddin.rgs,在文件末尾插入以下代码:

HKCU
{
Software
{
Microsoft
{
Office
{
Outlook
{
Addins
{
'OAddin.OAddin'
{
val FriendlyName = s 'SMIME Addin'
val Description = s 'ATLCOM Outlook Addin'
val LoadBehavior = d '00000003'
val CommandLineSafe = d '00000000'
}
}
}
}
}
}
}


注册条目看起来是很简单的。LoadBehaviour表明了Outlook装载COM插件的时机。我们的插件要在启动时装载,所以它的值设为3。现在,Build这个工程。然后在outlook里,点击工具->选项,在其他页点击高级选项->COM 加载项,就可以看见我们的Addin了.

下一步,我们的任务是接收Explorer事件(Sink Explorer Events).

Sinking Explorer Events:

Outlook对象模型提供了Explorer对象,它封装了Outlook的函数。这些Explorer对象可以用来进行Outlook编程。Explorer对象封装了CommandBars、Panes、Veiws和Folders。在这个教程中,我们用ExplorerEvents接口接收Active Exploerer的FolderSwich事件。

在Outlook对象模型中,Application对象位于模型层次的最顶层,它表示整个应用程序。通过它的ActiveExplorer方法,我们可以得到代表Outlook当前窗口的Explorer对象。

当用于从一个文件夹切换到另一个文件夹时,可以触发Explorer对象的一个事件。在我们的例子中,我们只考虑邮箱文件夹,不考虑联系人、任务、日历的FolderSwitch事件。在进行实际编码之前,我们需要导入Office和Outlook类型库。打开工程的stdafx.h文件,加入以下#import指令:

#import "C:/Program Files/Microsoft Office/Office/mso9.dll" /
rename_namespace( "Office" ) named_guids using namespace Office;
#import "C:/Program Files/Microsoft Office/Office/MSOUTL9.olb"
rename_namespace( "Outlook" ), raw_interfaces_only, /
named_guids using namespace Outlook;


这些路径需要根据你安装的操作系统和Office目录做出改变。

编译工程导入类型库。

现在,我们需要通过连接点实现由事件源唤起的事件接收接口。

ATL为ATL COM对象提供了IDispEventImpl<>和IDispEventSimpleImpl<>。我用IDispEventSimpleImpl<>建立了一个接收器映射(sink map)。我们要从IDispEventSimpleImpl<>派生我们的COAddin类并用_ATL_SINK_INFO结构建立params。然后建立接收器接口并用接口源分别调用DispEventAdvise和DispEventUnAdvise来初始化和终止连接。

代码看起来是这样的:

从IDispEventSimpleImpl派生你自己的类:

//
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
// ExplorerEvents class fires the OnFolderSwitch event of Explorer
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>


_ATL_SINK_INFO结构用来描述回调参数。打开add-in对象的头文件OAdin.h,在最顶部加入下面一行代码:

extern _ATL_FUNC_INFO OnSimpleEventInfo;

然后打开cpp文件,在上部加入以下代码:

_ATL_FUNC_INFO OnSimpleEventInfo ={CC_STDCALL,VT_EMPTY,0};

创建一个回调函数:

void __stdcall OnFolderChange(); (OAddin.h文件中,译者注)

void __stdcall COAddin::OnFolderChange() (OAddin.cpp文件中,译者注)

{

MessageBoxW(NULL,L"Hello folder Change Event",L"Outlook Addin",MB_OK);

}

现在,为了利用接收器映射,我们将要用到ATL BEGIN_SINK_MAP() 和END_SINK_MAP()。每个条目都由SINK_ENTRY_INFO来描述。dispid代表事件的DISPID。你可以在类型库中找到这些ID,也可以用Outlook Spy来查看它们。

BEGIN_SINK_MAP(COAddin)
// sink the event for Explorer
// the first parameter is the same as given above while driving the class
// the third parameter is the event identifier to sink i.e FolderChange
// rest of event id's can also be located using OutlookSpy or type libraries
SINK_ENTRY_INFO(1,__uuidof(Outlook::ExplorerEvents),/*dispinterface*/0xf002
,OnFolderChange,&OnSimpleEventInfo)
END_SINK_MAP()


(以上代码位于OAdin.h文件中,译者注)

现在,来到OnConnection函数(我们用向导实现接口的时候生成的),修改代码如下:

//
STDMETHOD(OnConnection)(IDispatch * Application, ext_ConnectMode ConnectMode,
IDispatch * AddInInst, SAFEARRAY * * custom)
{
CComQIPtr <Outlook::_Application> spApp(Application);
ATLASSERT(spApp);
m_spApp = spApp; //store the application object
CComPtr<Outlook::_Explorer> spExplorer;
spApp->ActiveExplorer(&spExplorer);
m_spExplorer = spExplorer; // store the explorer object
HRESULT hr = NULL;
//Sink Explorer Events to be notified for FolderChange Event
DispEventAdvise((IDispatch*)spExplorer);
hr = ExpEvents::DispEventAdvise((IDispatch*)m_spExplorer,
&__uuidof(Outlook::ExplorerEvents));
}


到此为止,我们建立了Explorer对象的FolderSwitch事件映射。编译这个工程。如果一切OK,当你在Outlook中从收件箱向发件箱或其他文件夹切换时,对话框就会弹出来。如果你切换到其他的文件夹,然后折叠邮件区域,同样会接受到这个对话框。稍后我们将用程序逻辑来控制它。

结合CDO和Outlook对象模型

CDO(Collaboration Data Objects,协作数据对象)是一项建立消息通知或协作应用程序的技术。CDO可以单独地使用,也可以用在Outlook对象模型的连接中来获取更多的对Outlook的访问途径。

我们的例子处理“自定义邮件分组”,按照邮件是SMIME(加密的),还是简单邮件(Simple emails)。Outlook对象模型对标记的加密邮件不提供任何交互接口。事实上,如果你试图研究代表一封加密邮件的MailItem对象,你会发现大约80%的属性和方法是无法访问的。现在,我们将用CDO来实现消息安全的可用性。

为了定制邮件分组,唯一的办法就是向MailItem增加自定义属性字段X,然后让Outlook按照属性X对邮件进行分组。然而不幸的是,一封标记过的加密邮件的字段属性是无效的,并且我们无法利用Outlook对象模型增加任何字段。

另一方面,CDO不是Outlook对象模型的一部分,它不提供任何基于某一功能的事件,我们也不能利用CDO操作Outlook对象。所以,为了用CDO访问当前选中的文件夹,我们需要先用Outlook对象模型得到当前选中的文件夹,然后得到它的唯一标识符,传给CDO,使CDO返回文件夹。

准备CDO编码

首先,在Office XP和随后版本中,默认安装是没有安装CDO的。所以你必须首先确定你的客户安装了CDO。此外,这个教程也要求你在机器上安装CDO。CDO不能从应用程序直接配置,必须用Microsoft Office MSI从Office光盘安装。

教程的结尾有一个例子,展示了如何编程调用MSI自动安装CDO。

我们将要用到下面CDO对象:Session, Messages, Message, Folder, Fields 和Field。为使这些对象在应用程序中可用,必须导入CDO。打开stdafx.h,加入下面代码:

#import "F://Program Files//Common Files//System//MSMAPI//1033//cdo.dll" /

rename_namespace("CDO")

我们可以通过Active Explorer的GetCurrentFolder用Outlook对象模型来访问Outlook的当前文件夹。在这里,如果返回的文件夹的DefaultItemType属性不是olMailItem,我们可以终止程序的执行。返回文件夹的EntryID属性把它和其他文件夹区分开来,我们将向CDO传递这个属性以得到当前文件夹。

首先,转到OnFolderChange添加下面代码:

//
void __stdcall COAddin::OnFolderChange()
{
if(!m_bEnableSorting) // its a boolean variable to identify
//weither to sort or not
return;
CDO::_SessionPtr Session("MAPI.Session");
//logon to CDO
// the first parameter is the Profile name you want to use.
// the rest of two false tell CDO not to display
// any user interface if this profile is not found.
Session->Logon("Outlook","",VARIANT_FALSE,VARIANT_FALSE);
//its the OutlookObject Model MAPIFolder object. it is used to findout
//currently selected folder of outlook as CDO doesn't
// provide any direct interface
//to get the currently selected folder of outlook
CComPtr<Outlook::MAPIFolder> MFolder;
m_spExplorer->get_CurrentFolder(&MFolder);
//this example only deals with outlook Mail Items
OlItemType o;
MFolder->get_DefaultItemType(&o);
if(o != olMailItem)
return;
BSTR entryID;
MFolder->get_EntryID(&entryID);
CDO::MessagesPtr pcdoMessages;
// get the selected folder in CDO using the EntryID of Outlook::MapiFolder
CDO::FolderPtr  pFolder= Session->GetFolder(entryID);
if(pFolder) //making sure
{
//play with the folder messages here
}
}


OK,到此为止,我们得到了CDO Folder,然后就可以获取Messages集合了。

利用CDO查看一个消息是否具有安全性

现在,让我们来看看如何判断一封邮件是“encrypted”还是“signed”。下面的KB表明了全部原理:"KB 194623",但是我发现对我的客户来说它并不正常,因为他们有很多邮件客户端,不是每个客户端都和这个KB描述的一致。事实上,它也说明了,“使用这些属性编程决定一条消息是否具有安全性是不可靠的”。为了达到结果,我所能够找到的唯一的办法是,每封具有安全性的邮件都有一个特殊的附件。这个附件包含了Outlook
“encrypted/signed”的内容。这个附件的扩展名是“p7m”,MIME类型是application/x-pkcs7-mime。在我们对解决方案中,我们的方法是:

1. 得到文件夹的Messages集合。

2. 枚举集合得到Message。

3. 枚举Attachments。

4. 得到每个Attachments的Fields。

5. 枚举Fields,找到Field(H370E001E)(这是Attachment的MIME类型)。

6. 用"application/x-pkcs7-mime"测试Field值。

现在,为你的类添加一个新的成员函数IsCDOEncrypted。这个函数接收一个单独的CDO Message对象,返回一个布尔类型的值指示这个message的状态。下面就是上述理论的代码片断:

//
BOOL COAddin::IsCDOEncrypteD(CDO::MessagePtr pMessageRemote)
{
BOOL bEncrypted = false;
CDO::MessagePtr pMessage;
pMessage = pMessageRemote;
//get the attachments of the CDO message
CDO::AttachmentsPtr pAttachments;
pAttachments = pMessage->Attachments;
_variant_t nCount =pAttachments->Count;
long nTotal = nCount.operator long();
//enumerate the attachments
for(int i = 0; i < nTotal; i++)
{
// get the attachment from the
//attachments collection
CDO::AttachmentPtr pAttachment;
CComVariant nItem(i+1);//1 based index
pAttachment = pAttachments->Item[nItem];
//get the Fields collection of the Attachment object
CDO::FieldsPtr pFields;
pFields = pAttachment->Fields;
_variant_t nVFields = pFields->Count;
for(int z = 0; z < nVFields.operator long() ; z++)
{
// get the field from fields collection.
CComVariant nFieldItem(z+1);
CDO::FieldPtr pField;
pField = pFields->Item[nFieldItem]; //1 based index
//check if this field is what we need
//the mime type of the
//attachment is stored as Field in the CDO message
//the field that contains the mime type of the CDO Message
// has an ID of 923664414 (more such ID's can be
//found in CDO in HTTP transport Header section)
BSTR bstrFieldID;
bstrFieldID =  pField->GetID().operator _bstr_t();
if(wcscmp(bstrFieldID,L"923664414")==0)
// get the mime type of the attachment
{
// check the mime type of the mail item now.
// compare the field value.
if(wcscmp(pField->Value.operator _bstr_t(),
L"application/x-pkcs7-mime")==0)
{
bEncrypted = true;
break;
}
}

}
}
pAttachments->Release();
pAttachments = NULL;
pMessage->Update(); //its not a necessary call.
return bEncrypted;
}


利用CDO向Outlook添加自定义字段

Outlook对象模型暴露了Fields属性,可以增加自定义的Fields属性。我们将通过CDO向邮件增加自定义Fields。

在OnFolderChange函数中,遍历messages时,可以使用IsCDOEncrypted函数。回到OnFolderChange函数。

下面的代码片断为每封邮件增加了自定义fields。

//
.....
// previous code of OnFolderChange
.....
CDO::FolderPtr pFolder= Session->GetFolder(entryID);
if(pFolder) //making sure
{
//get the message of the Folder
pcdoMessages = pFolder->Messages;
CDO::MessagePtr pMessage = pcdoMessages->GetFirst();
while(pMessage) // iterate them
{
//check if the message is encrytped
BOOL bEncrypted = IsCDOEncrypteD(pMessage);
if(bEncrypted)
{
//add a custom field to the outlook message
//an encrypted email
CDO::FieldsPtr pMessageFields = pMessage->Fields;
//Add a custom field
//Encrypted of type String(8)
//and set its value to "Encrypted"
pMessageFields->Add(L"Encrypted",
CComVariant(8),L"SMIME Emails");
pMessage->Update();
}
else
{
CDO::FieldsPtr pMessageFields = pMessage->Fields;
//Add a custom field
pMessageFields->Add(L"Encrypted",CComVariant(8),L"Simple Emails");
// you must call Update message to reflect the new field to
// mail item
pMessage->Update();
}
pMessage = pcdoMessages->GetNext();
}
}


定制分组和分类



Outlook现在暴露了新的基于XML的视图系统。你可以用XML创建你自己的视图,也可以改变XML来修改现有的视图。下面是收件箱的标准XML:

//
<?xml version="1.0"?>
<view type="table">
<viewname>Messages</viewname>
<viewstyle>table-layout:fixed;width:100%;font-family:Tahoma;
font-style:normal;font-weight:normal;font-size:8pt;
color:Black;font-charset:0</viewstyle>
<viewtime>0</viewtime>
<linecolor>8421504</linecolor>
<linestyle>3</linestyle>
<usequickflags>1</usequickflags>
<collapsestate></collapsestate>
<rowstyle>background-color:#FFFFFF</rowstyle>
<headerstyle>background-color:#D3D3D3</headerstyle>
<previewstyle>color:Blue</previewstyle>
<arrangement>
<autogroup>1</autogroup>
<collapseclient></collapseclient>
</arrangement>
<column>
<name>HREF</name>
<prop>DAV:href</prop>
<checkbox>1</checkbox>
</column>
<column>
<heading>Importance</heading>
<prop>urn:schemas:httpmail:importance</prop>
<type>i4</type>
<bitmap>1</bitmap>
<width>10</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<heading>Icon</heading>
<prop>http://schemas.microsoft.com/mapi/proptag/0x0fff0102</prop>
<bitmap>1</bitmap>
<width>18</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<heading>Flag Status</heading>
<prop>http://schemas.microsoft.com/mapi/proptag/0x10900003</prop>
<type>i4</type>
<bitmap>1</bitmap>
<width>18</width>
<style>padding-left:3px;;text-align:center</style>
</column>
<column>
<format>boolicon</format>
<heading>Attachment</heading>
<prop>urn:schemas:httpmail:hasattachment</prop>
<type>boolean</type>
<bitmap>1</bitmap>
<width>10</width>
<style>padding-left:3px;;text-align:center</style>
<displayformat>3</displayformat>
</column>
<column>
<heading>From</heading>
<prop>urn:schemas:httpmail:fromname</prop>
<type>string</type>
<width>49</width>
<style>padding-left:3px;;text-align:left</style>
<displayformat>1</displayformat>
</column>
<column>
<heading>Subject</heading>
<prop>urn:schemas:httpmail:subject</prop>
<type>string</type>
<width>236</width>
<style>padding-left:3px;;text-align:left</style>
</column>
<column>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<width>59</width>
<style>padding-left:3px;;text-align:left</style>
<format>M/d/yyyy||h:mm tt</format>
<displayformat>2</displayformat>
</column>
<column>
<heading>Size</heading>
<prop>http://schemas.microsoft.com/mapi/id
/{00020328-0000-0000-C000-000000000046}/8ff00003</prop>
<type>i4</type>
<width>30</width>
<style>padding-left:3px;;text-align:left</style>
<displayformat>3</displayformat>
</column>
<groupby>
<order>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<sort>desc</sort>
</order>
</groupby>
<orderby>
<order>
<heading>Received</heading>
<prop>urn:schemas:httpmail:datereceived</prop>
<type>datetime</type>
<sort>desc</sort>
</order>
</orderby>
<groupbydefault>2</groupbydefault>
<previewpane>
<visible>1</visible>
<markasread>0</markasread>
</previewpane>
</view>


在这个教程中,我们关注的是<groupby> 和 <orderby>两个节点。这里我只是自定义分组功能,你可以重用相同的代码来自定义分类功能。

为了定制分组功能,你可以在Outlook的"Customize Current View"选项中设置"User Defined fields"。为了以编程方式实现,由于我们已经增加了自定义字段,我们仅需要像下面意义修改<groupby>元素:

//
<groupby>
<order>
<heading>Encrytped</heading>
<prop>http://schemas.microsoft.com/mapi/string/
{00020329-0000-0000-C000-000000000046}/Encrypted</prop>
<type>string</type>
<sort>asc</sort>
</order>
</groupby>


OK,新增一个成员函数ChangeView(Outlook::ViewPtr pView)。这个函数接收一个Outlook View,返回它的XML,并相应地作出修改。Outlook的MAPIFolder对象的CurrentView属性返回当前视图。Outlook::View的XML属性返回当前视图的XML。这里,我使用MSXML
parser修改XML。你也可以使用任何方便的方法。代码如下:

//
void COAddin::ChangeView(Outlook::ViewPtr pView)
{
HRESULT hr;
IXMLDOMDocument2 * pXMLDoc;
IXMLDOMNode * pXDN;
//...create an instance of IXMLDOMDocument2
hr = CoInitialize(NULL);
hr = CoCreateInstance(CLSID_DOMDocument30, NULL, CLSCTX_INPROC_SERVER,
IID_IXMLDOMDocument2, (void**)&pXMLDoc);
hr = pXMLDoc->QueryInterface(IID_IXMLDOMNode, (void **)&pXDN);
//get the view's XML
BSTR XML;
pView->get_XML(&XML);
//loaod the XML
VARIANT_BOOL bSuccess=false;
pXMLDoc->loadXML(XML,&bSuccess);

CComPtr<IXMLDOMNodeList> pNodes;
//check groupby element exists
pXMLDoc->getElementsByTagName(L"groupby",&pNodes);
long length = 0;
pNodes->get_length(&length);
if(length> 0)
{
// groupby element already exists.
// get the first occourance of groupby element
/*<groupby>
<order>
<heading>Encrypted</heading>
<prop>http://schemas.microsoft.com/mapi/string
/{00020329-0000-0000-C000-000000000046}
/Encrypted</prop>
<type>string</type>
<sort>asc</sort>
</order>
</groupby>*/
HRESULT hr = pNodes->get_item(0,&pXDN);
IXMLDOMNode *pXDNTemp,*pXDNTemp2;
pXDN->get_firstChild(&pXDNTemp);
pXDNTemp->get_firstChild(&pXDNTemp2);
_variant_t vtHeading("Encrypted"),vtType("string"),
vtProp("http://schemas.microsoft.com/mapi/string/ /
{00020329-0000-0000-C000-000000000046}/Encrypted");

// get the heading element
//the first element is the name of the field.
pXDNTemp2->put_nodeTypedValue(vtHeading);
// get the prop element
pXDNTemp2->get_nextSibling(&pXDNTemp2);
pXDNTemp2->put_nodeTypedValue(vtProp);
pXDNTemp2->get_nextSibling(&pXDNTemp2);
// get the type elment. it tell what sort of sorting goin to be
pXDNTemp2->put_nodeTypedValue(vtType);
}else
{
//groupby element doesn't exists
IXMLDOMElement *pGroupByElement;
//create the element
pXMLDoc->createElement(L"groupby",&pGroupByElement);
IXMLDOMElement *pOrderElement;
IXMLDOMNode *pOrderNode;
//create the Order element in side groupby element
pXMLDoc->createElement(L"order",&pOrderElement);
pGroupByElement->appendChild(pOrderElement,&pOrderNode);
IXMLDOMElement *pHeadingElement,*pPropElement,*pTypeElement,
*pSortElement;

IXMLDOMNode *pHeadingNode,*pPropNode,*pTypeNode, *pSortNode;
_variant_t vtHeading("Encrypted"),vtSort("asc"),vtType("string"),
vtProp("http://schemas.microsoft.com/mapi/string//
{00020329-0000-0000-C000-000000000046}/Encrypted");

//create the heading element and populate it with value
pXMLDoc->createElement(L"heading",&pHeadingElement);
pOrderNode->appendChild(pHeadingElement,&pHeadingNode);
pHeadingNode->put_nodeTypedValue(vtHeading);

//create the prop element and populate it with value
pXMLDoc->createElement(L"prop",&pPropElement);
pOrderNode->appendChild(pPropElement,&pPropNode);
pPropNode->put_nodeTypedValue(vtProp);
//create the type element and populate it with value
pXMLDoc->createElement(L"type",&pTypeElement);
pOrderNode->appendChild(pTypeElement,&pTypeNode);
pTypeNode->put_nodeTypedValue(vtType);
pXMLDoc->createElement(L"sort",&pSortElement);
pOrderNode->appendChild(pSortElement,&pSortNode);
pSortNode->put_nodeTypedValue(vtSort);

HRESULT hr;//= pXMLDoc->insertBefore(pOrderNode,NULL,NULL);
IXMLDOMElement *pXMLRootElement;
if(!FAILED(pXMLDoc->get_documentElement(&pXMLRootElement)))
{
_variant_t _vt;
hr= pXMLRootElement->insertBefore(pGroupByElement,_vt,NULL);
}
}
// get the xml out of the MSXML document object
pXMLDoc->get_xml(&XML);
// put the xml to View
pView->put_XML(XML);
// Save method is a must to reflect change to the view
pView->Save();
}


下面添加在OnFolderChange函数中的代码实现了分组功能:

// OnFolderChange
// ....
// .. Old Code goes here

pMessageFields->Add(L"Encrypted",CComVariant(8),L"Simple Emails");
// you must call Update message to reflect the new field to
// mail item
pMessage->Update();
}
pMessage = pcdoMessages->GetNext();
}  // end of while
CComPtr<Outlook::View> pV;
HRESULT hr = MFolder->get_CurrentView(&pV);
//now change its view state.
ChangeView(pV);
}


喔,打字打得好累 :O

好了,现在可以编译了,如果一切顺利,你的邮件将会被分成两个组。

向右键菜单添加选项



我想这是才是本教程最值得大家期待的部分。比起钻研密码逻辑来说,大部分人更关心这个。

Amit Dey就“向菜单和工具栏添加新项”做了很多解释。如果你没有读过他的文章,先去读一下,因为我将不再详细解释CommandBars这类东西。

为了向Outlook右键菜单增加新项,我们需要映射Command Bars的OnUpdate事件。我们可以使Command Bars对象通过Explorer的CommandBars属性接收OnUpdate事件。

首先,增加私有成员变量以存储CommandBars对象。打开头文件OAddin.h,添加下面代码:

CComPtr<Office::_CommandBars> m_spCommandbars; //commandbars

CComPtr<Office::CommandBarControl> m_pSortButton; // Sort Button

为了接收OnUpdate事件,COAddin类必须做出一些修改。打开OAddin.h文件,从CommandBarsEvents继承你的类:

//
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
// ExplorerEvents class fires the OnFolderSwitch event of Explorer
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>,
public IDispEventSimpleImpl<2,COAddin,&__uuidof(Office::_CommandBarsEvents)>
然后在OAddin.h中声明一个回调函数:
void __stdcall OnContextMenuUpdate();
打开cpp文件,增加这个函数的定义:
//void __stdcall COAddin::OnContextMenuUpdate()
{
MessageBoxW(NULL,L"Hello Menu Update Event",L"Outlook Addin",MB_OK);
}




建立接收器映射:

//

BEGIN_SINK_MAP(COAddin)
// sink the event for Explorer
// the first parameter is the same as given above while driving the class
// the third parameter is the event identifier to sink i.e FolderChange
// rest of event id's can also be located using OutlookSpy or type libraries
SINK_ENTRY_INFO(1,__uuidof(Outlook::ExplorerEvents),/*dispinterface*/0xf002
,OnFolderChange,&OnSimpleEventInfo)
SINK_ENTRY_INFO(2,__uuidof(Office::_CommandBarsEvents),/*dispinterface*/0x1,
OnContextMenuUpdate,&OnSimpleEventInfo)
END_SINK_MAP()


现在,从源接口接收事件接口的通道已经打通。接收事件的最佳地方是OnConnection,回到OnConnection函数,增加以下代码。如上所述,我们可以从Explorer对象得到CommandBars。

//... OnConnection function
// .. earlier code goes here
hr = ExpEvents::DispEventAdvise((IDispatch*)m_spExplorer,
&__uuidof(Outlook::ExplorerEvents));
// .....
//.....
CComPtr < Office::_CommandBars> spCmdBars;
hr = spExplorer->get_CommandBars(&spCmdBars);
if(FAILED(hr))
return hr;
m_spCommandbars = spCmdBars;
//Sink the OnUpdate event of command bars
hr = CmdBarsEvents::DispEventAdvise((IDispatch*)m_spCommandbars,
&__uuidof(Office::_CommandBarsEvents));


OK!当一个CommandBar需要更新时,OnUpdate事件就激发了。所以我们现在需要的是找到右键菜单,往里面添加新项。右键菜单具有一个固定的名字:Context Menu。我们可以枚举CommandBars来找到Context Menu。

CommandBars对象包含了子控件CommandBar。每个CommandBar又可以包含CommandBarControl 和CommandBarButtons。我们要向菜单增加一个CommandBarControl。我们可以用CommandBar的Add方法向CommandBar增加一个新项。所以我们的任务是:

 1.枚举CommandBars以找到名字为Context Menu的CommandBar。

 2.得到CommandBar控件。

 3.调用这个控件的Add方法向它插入新项。

好了,现在最重要的一件事是,CommandBar对象是被Outlook锁定的。为了调用它的Add方法,必须取消对CommandBarControl对象的保护,否则对Add的访问将会失败。我们可以用CommandBar的Protection属性来取消这个保护。

下面是修改OnContextMenuUpdate的代码:

//
void __stdcall COAddin::OnContextMenuUpdate()
{
CComPtr<Office::CommandBar> pCmdBar;
BOOL bFound =false;
for(long i = 0; i < m_spCommandbars->Count ; i++)
{
CComPtr<Office::CommandBar> pTempBar;
CComVariant vItem(i+1);   //zero based index
m_spCommandbars->get_Item(vItem,&pTempBar);
if((pTempBar) && (!wcscmp(L"Context Menu",pTempBar->Name)))
{
pCmdBar = pTempBar;
bFound = true;
break;
}
// pCmdBar->Release();
}
if(!bFound)
return;

if(pCmdBar)//make sure a valid CommandBar is found
{
soBarProtection oldProtectionLevel = pCmdBar->Protection ;
// change the commandbar protection to zero
pCmdBar->Protection = msoBarNoProtection;
//set the new item type to ControlButton;
CComVariant vtType(msoControlButton);
//add a new item to command bar
m_pSortButton = pCmdBar->Controls->Add(vtType);
//set a unique Tag that u can be used to find your control in commandbar
m_pSortButton ->Tag = L"SORT_ITEM";
//a caption
m_pSortButton ->Caption = L"Sort By SMIME";
// priority (makes sure outlook includes this item in every time)
m_pSortButton ->Priority =1 ;
// visible the item
m_pSortButton ->Visible = true;
}
}


到此为止,编译这个工程,Outlook鼠标右键菜单就会出现一个新项。但是在向它添加一个句柄之前,它是无效的。所以,为了接收事件,让我们回到OAddin类的头文件,使这个类从_CommandBarButtonEvents继承。

//
class ATL_NO_VTABLE COAddin :
public CComObjectRootEx<CComSingleThreadModel>,
...
...
// ExplorerEvents class fires the OnFolderSwitch event of Explorer
public IDispEventSimpleImpl<1,COAddin,&__uuidof(Outlook::ExplorerEvents)>,
public IDispEventSimpleImpl<2,COAddin,&__uuidof(Office::_CommandBarsEvents)>
,
// Its possible to sink event for a single command bar button
// and you can recognize the control using its face text
// but for this example i've sinked event for each command bar button
public IDispEventSimpleImpl<3,COAddin,
&__uuidof(Office::_CommandBarButtonEvents)>,


然后再次用ATL_SINK_INFO结构描述回调参数。打开add-in对象的头文件OAddin.h,在最顶部加入下面一行代码:

extern _ATL_FUNC_INFO OnClickButtonInfo;

打开这个类的cpp文件,在顶部加入下面代码:

_ATL_FUNC_INFO OnClickButtonInfo =

{CC_STDCALL,VT_EMPTY,2,{VT_DISPATCH,VT_BYREF | VT_BOOL}};

创建回调函数:

//

void __stdcall OnClickButtonSort(IDispatch* /*Office::_CommandBarButton*

*/ Ctrl,VARIANT_BOOL * CancelDefault);

void __stdcall COAddin::OnClickButtonSort(IDispatch* /*Office::_CommandBarButton*

*/ Ctrl,VARIANT_BOOL * CancelDefault)

{

// m_bEnableSorting is a member boolean variable

if(!m_bEnableSorting)

{

m_bEnableSorting =true;

OnFolderChange(); // Sort The elements of current view;

}

else

{

m_bEnableSorting = false;

}

}

现在可以接收事件了。当新的CommandBarControl生成时接收事件:

// OnUpdate

// .... Old code goes here.

m_pSortButton ->Visible = true;

hr = CommandButtonEvents::DispEventAdvise((IDispatch*)m_pSortButton);

if(hr != S_OK)

{

MessageBoxW(NULL,L"Menu Event Sinking failed",L"Error",MB_OK);

}

CComQIPtr < Office::_CommandBarButton> spCmdMenuButton(m_pSortButton);

好了,编译工程,看看菜单项是怎么工作的。点击一次,邮件会自动分类,再次点击,邮件就不再分类了。

如何改变菜单状态使它为选中状态呢?



喔,还有很多要写的 :S,并且还有很多要读的。

早些时候当我为一个客户开发解决方案时,一个Microsoft MVP(我指的不是你;))对我说,无论是向右键菜单里添加新项,还是改变菜单项为选中状态,都是不可能的,我必须放弃这些功能把工程交付客户。后来,我用Outlook Spy并参照Amit Dey的文章搞定了,虽然有些棘手,但并不是不可能的。

好了,为了改变菜单项为选中状态,你只需要为Office::msoButtonDown改变新加入CommandBarControl的State的属性。对新插入的CommandBarControl,State是不可用的,所以必须把它转换为CommandButton。

下面是OnUpdate的代码:

// OnUpdate

// .... Old code goes here.

m_pSortButton ->Visible = true;

// ok this example needs to modify the new menu item to be displayed as CHECKED

// as well we get the equivalent commandbarbutton object of this object.

CComQIPtr < Office::_CommandBarButton> spCmdMenuButton(m_pSortButton);

if(m_bEnableSorting)

{

//if sorting is enabled check mark the new menu item

ATLASSERT(spCmdMenuButton);

spCmdMenuButton->State = Office::msoButtonDown;

m_bEnableSorting = true;

}

// .. rest of code goes here

hr = CommandButtonEvents::DispEventAdvise((IDispatch*)m_pSortButton);

if(hr != S_OK)

{

MessageBoxW(NULL,L"Menu Event Sinking failed",L"Error",MB_OK);

}

注意:为了向菜单项添加图标,你可以用方法向菜单项添加一个图像。下面的代码完成了这个工作:

//

HBITMAP hBmp =(HBITMAP)::LoadImage(_Module.GetResourceInstance(),

MAKEINTRESOURCE(IDB_BITMAP1),IMAGE_BITMAP,0,0,LR_LOADMAP3DCOLORS);

// put bitmap into Clipboard

::OpenClipboard(NULL);

::EmptyClipboard();

::SetClipboardData(CF_BITMAP, (HANDLE)hBmp);

::CloseClipboard();

::DeleteObject(hBmp);

// change the button layout and paste the face

spCmdMenuButton->PutStyle(Office::msoButtonIconAndCaption);

HRESULT hr = spCmdMenuButton->PasteFace();

if (FAILED(hr))

return ;

编译这个工程,一个完整的插件就完成了。它可以按照安全性将邮件分类。

如何编程利用MSI安装CDO

可以在程序中利用MSI安装CDO。为了在C++工程中使用MSI,必须向工程导入MSI.dll:

#import "msi.dll" rename_namespace("MSI")

为了安装CDO,MSI需要功能名字和产品代码。产品代码可以从传给OnConnection函数的Outlook的Applicaton对象得到。下面是MSI的代码:

//

BSTR bstrCode;

spApp->get_ProductCode(&bstrCode);

MSI::InstallerPtr pInstaller(__uuidof(MSI::Installer));

MSI::MsiInstallState o = pInstaller->GetFeatureState(bstrCode,L"OutlookCDO");

if(o != MSI::msiInstallStateLocal)

{

pInstaller->ConfigureFeature(bstrCode,L"OutlookCDO",MSI::msiInstallStateLocal);

}

OK,搞定!

在这个教程中,我尽力给出了最多的解释。

提供的例子是用C++写的,我曾经用VB实现。结果并不是最优的,而且你可能会发现一些COM实现上的不足。欢迎批评指正。谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: