使用 VC++/ATL 开发 Office 2003 COM 插件






4.80/5 (28投票s)
2004 年 8 月 17 日
15分钟阅读

358036

2800
本文介绍如何开发一个完整的 Office 2003 COM 插件,并介绍一些编程 Outlook 的实用技巧。
引言
最近,我为一位客户编写了一个 Outlook 2003 COM 插件。在编码过程中,我遇到了许多 C++ 方面无法找到解决方案的问题。我发现对于初学者来说,使用 ATL 编写插件相当棘手,因此为了帮助他人,我决定记录下一些信息,因为我在网上找到的大多数与 Office 相关的示例都是 VB/VBA 相关的,而使用 ATL 的几乎没有。
本文中的代码并不十分优化,并且附带的示例可能存在一些内存泄漏或糟糕的 COM 实现,但总体方法力求简单易懂。尽管我花了相当长的时间来编写,但如果发现任何错误或 bug,请随时给我发邮件。
概述
如果您是 COM/ATL 初学者,我建议您阅读 Amit Dey 关于构建 Outlook 2000 插件的文章。
我的文章将介绍以下技巧
- 基本的 Outlook 2003 插件
- 事件接收器 Explorer 事件
- 混合使用 CDO 和 Outlook 对象模型
- 使用 CDO 检测消息是否具有安全属性
- 使用 CDO 为 Outlook 项目添加自定义字段
- 以编程方式自定义项目的分组和排序
- 将项目添加到右键菜单
- 如何以编程方式使用 MSI 安装 CDO
Office COM 插件必须实现 IDTExtensibility2
接口。IDTExtensibility2
dispinterface 定义在 **MSADDIN Designer typelibrary** (MSADDNDR.dll/MSADDNDR.tlb) 中。所有继承自 IDTExtensibility2
接口的 COM 插件都必须实现五个方法
OnConnection
OnDisconnection
OnAddinUpdate
OnBeginShutDown
OnStartupComplete
注册
所有 Office 插件都注册到以下注册表项,其中 _Outlook_ 被替换为应用程序名称。
HKEY_CURRENT_USER\Software\Microsoft\Office\Outlook\Addins\<ProgID>
还有一些其他条目用于 Outlook 识别插件。
入门
让我们开始编写一个基本的、功能性的 COM 插件。然后,我们将逐步进行,创建一个插件,该插件将简单邮件和加密邮件分到不同的组。
本文假定您是 VC++ COM 程序员,并且具有 ATL 组件开发和 OLE/Automation 的经验,尽管这不是必需的。该插件是为 Outlook 2003 设计的,因此您必须在计算机上安装带有 CDO 的 Office 2003。项目代码使用 VC++ 6.0 SP3+/ATL3.0 构建,并在安装了 Office 2003 的 WinXP Pro SP1 上进行了测试。
启动您的 VC IDE。创建一个新项目,选择 **ATL COM AppWizard** 作为项目类型,将其命名为 **Outlook Addin**,然后单击 **OK**。选择 **Dynamic Link Library** 作为您的服务器类型,然后单击 **Finish**。
现在,使用 **Insert** 菜单 **Add a New ATL Object**。选择 **Simple Object** 并将其命名为 **OAddin**,转到 **Attributes** 并选中 **Support ISupportErrorInfo** 复选框,其余选项保持默认。
现在,是时候实际实现 IDTExtensibility2
接口了。右键单击 COAddin
类,然后选择 **Implement Interface**。这将打开 **Browse Type library wizard**。选择 **Microsoft Add-in Designer(1.0)**,然后单击 **OK**。如果列表中没有,您可能需要浏览到 _<drive>/Program Files/Common Files/Designer_ 文件夹。
此向导实现所选接口,并为 IDTExtensibility2
接口的五个方法添加了默认实现。此时,一个基本的自动化兼容 COM 对象已准备就绪。通过向 _rgs_ 文件添加注册表项,我们可以将此 COM 插件注册到 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。因此,您现在可以构建项目,并在 Outlook 中看到以下条目:(**Tools -> Options -> Other -> Advanced Options -> COM Addins**)
那么,下一步是接收 Explorer 事件。
事件接收器 Explorer 事件
Outlook 对象模型提供了 Explorer 对象,这些对象封装了 Outlook 的功能。这些 Explorer 对象可用于编程控制 Outlook。Explorer 对象封装了 **CommandBars、Panes、Views 和 Folders**。在本教程中,我们将使用 ExplorerEvents
接口来接收 **Active Explorer** 的 FolderSwitch
事件。
在 Outlook 对象模型中,Application
对象位于对象层次结构的顶部,代表整个应用程序。通过其 ActiveExplorer
方法,我们可以获取 Explorer
对象,该对象代表 Outlook 的**当前窗口**。
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
结构设置**参数**。设置我们的接收器接口并调用 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
结构用于描述回调参数。打开插件对象的头文件 (_OAddin.h_),并在最上方添加以下行extern _ATL_FUNC_INFO OnSimpleEventInfo;
打开类的 _cpp_ 文件,并在顶部添加以下行
_ATL_FUNC_INFO OnSimpleEventInfo ={CC_STDCALL,VT_EMPTY,0};
创建一个回调函数
void __stdcall OnFolderChange(); void __stdcall COAddin::OnFolderChange() { MessageBoxW(NULL,L"Hello folder Change Event",L"Outlook Addin",MB_OK); }
现在,要设置接收器映射,我们将使用 ATL BEGIN_SINK_MAP()
和 END_SINK_MAP()
。每个条目将由 SINK_ENTRY_INFO
表示,_dispid_ 是事件的 DISPID。您可以在类型库中或使用 Outlook Spy 找到这些 ID。
// 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()
现在,转到我们在实现接口时通过向导添加的 OnConnection
函数,并按如下方式修改代码
// STDMETHOD(OnConnection)(IDispatch * Application, ext_ConnectMode ConnectMode, IDispatch * AddInInst, SAFEARRAY * * custom) { omQIPtr <Outlook::_Application> spApp(Application); TLASSERT(spApp); _spApp = spApp; //store the application object ComPtr<Outlook::_Explorer> spExplorer; pApp->ActiveExplorer(&spExplorer); _spExplorer = spExplorer; // store the explorer object RESULT hr = NULL /Sink Explorer Events to be notified for FolderChange Event /DispEventAdvise((IDispatch*)spExplorer); r = ExpEvents::DispEventAdvise((IDispatch*)m_spExplorer, &__uuidof(Outlook::ExplorerEvents)); }
到这里,我们已经映射了 Explorer
对象的 FolderSwitch
事件。如果一切顺利,请编译项目。当您将文件夹从收件箱切换到已发送或其他文件夹时,消息框将弹出。如果您切换到其他文件夹,然后再切换到邮件文件夹,您仍然会收到此消息框。我们稍后将通过我们的编程逻辑来控制这一点。
混合使用 CDO 和 Outlook 对象模型
CDO (Collaboration Data Objects) 是一种用于构建消息传递或协作应用程序的技术。CDO 可以单独使用,也可以与 Outlook 对象模型结合使用,以获得对 Outlook 的更多访问权限。
本示例处理“自定义邮件分组”,根据其性质 SMIME(加密)或简单邮件。Outlook 对象模型不提供与签名和加密邮件交互的接口。实际上,如果您查看代表加密邮件的 MailItem
对象,其大约 80% 的属性和方法都不可用。我们将使用 CDO 来确定消息是否具有安全属性。
为了自定义邮件的分组,唯一的方法是将自定义字段“X”添加到 MailItem
并强制 Outlook 根据“X”进行分组。但不幸的是,签名/加密邮件的“Fields
”属性无效,并且我们无法使用 Outlook 对象模型向其添加任何字段。
另一方面,CDO 不是 Outlook 对象模型的一部分,它不提供任何基于事件的功能,您也不能使用 CDO 操作 Outlook 对象。因此,要访问 CDO 中当前选定的文件夹,您必须使用 Outlook 对象模型来检索当前选定的文件夹,获取其唯一标识符,然后将其传递给 CDO 以返回文件夹。
准备您的代码以使用 CDO
关于 CDO 的第一条说明是:“CDO 在 Office XP 及更高版本的默认安装中并未安装”,因此您需要确保您的客户已安装。此外,继续阅读本教程还需要您在计算机上安装 CDO。CDO 不可作为可再发行组件提供,也不能将 CDO.dll 与您的应用程序一起部署。CDO 仅在 Office CD 中可用,并且只能通过 Microsoft Office MSI 安装程序进行安装。
在本教程的结尾有一个示例,演示如何以编程方式调用 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")
我们可以使用 Outlook 对象模型通过 **Active Explorer** 的 GetCurrentFolder
来访问 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 } }
好的,到目前为止,我们已经获得了 **CDO Folder**,我们现在可以获取 Messages
集合。
使用 CDO 检测消息是否具有安全属性
现在,让我们看看如何检测消息是否已加密/签名。以下 KB 文章介绍了完整的理论:“KB 194623”,但我未能使其在我的客户那里生效,因为存在许多电子邮件客户端,并非所有客户端都设置了该 KB 文章描述的位。实际上,它指出“**使用这些属性以编程方式确定消息是否具有安全属性是不可靠的**”。为了实现结果,我能找到的唯一方法是,每封带有安全属性的电子邮件都有一个特殊的附件。附件包含 Outlook 解密/验证并显示的加密/签名内容。此附件具有“p7m”扩展名,MIME 类型为“**application/x-pkcs7-mime**”。我们的解决方案将如下所示
- 获取文件夹的
Messages
集合 - 遍历集合并获取
Message
- 遍历
Attachments
- 获取每个
Attachment
的Fields
- 遍历
Fields
以查找Field
(H370E001E)(这是Attachment
的 MIME 类型) - 将字段值与“application/x-pkcs7-mime”进行测试
现在,向您的类添加一个新的成员函数,名为 IsCDOEn
crypted。该函数接受一个 CDO 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
属性,您可以向这些项目添加**自定义字段**。我们将使用 CDO 向邮件项添加自定义字段。
好的,现在让我们在 OnFolderChange
函数的**枚举**消息时使用 IsCDOEncrypted
函数。回到 OnFolderChange
代码。
以下代码片段向每个项目添加自定义字段。
// ..... // 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>
那么,让我们回到代码并添加一个名为 ChangeView(Outlook::ViewPtr pView)
的新成员函数。该函数接收一个 **Outlook View**,检索其 **XML**,并进行相应修改。Outlook MAPIFolder
对象的 CurrentView
属性返回**当前视图**。Outlook::View
的 XML
属性返回当前视图的**XML**。这里,我使用了 **MSXML 解析器**来修改 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
函数中添加以下代码以启用 SMIME 分组。
// 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。
好了,现在是时候编译了,编译您的项目,如果一切顺利,您将看到您的电子邮件按两个组排序。
将项目添加到右键菜单
好了,我认为这是本教程中最受期待的部分,而且你们中的大多数人都会关心这一部分,而不是深挖加密逻辑 :D。
**_Amit Dey_** 已经详细解释了如何将项目添加到菜单和工具栏。如果您还没有阅读那篇文章,现在是时候阅读了,因为我不会解释 **CommandBars** 等的细节。
为了将项目添加到 **Outlook** 的右键菜单,我们将不得不映射 **Command Bars** 的 OnUpdate
事件。我们可以通过 Explorer
的 CommandBars
属性获取 **Command Bars** 对象,并接收其 OnUpdate
事件。
首先,添加私有成员以保存 CommandBars
对象。转到 _OAddin.h_ 头文件并添加以下行
CComPtr<Office::_CommandBars> m_spCommandbars; //commandbars CComPtr<Office::CommandBarControl> m_pSortButton; // Sort Button
让我们首先对 COAddin
类进行必要的更改,以便能够接收 OnUpdate
事件。转到 _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)> now create a call back function declartion in your OAddin.h file as follows 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));
最后,您就完成了。每当 CommandBar
需要更新时,就会触发 OnUpdate
事件,因此我们需要找到右键菜单并向其添加项目。右键菜单有一个固定的名称“**Context Menu**”。在这里,我们将不得不**枚举** CommandBars
来查找 Context Menu。
CommandBars
对象包含子 CommandBar
控件,每个 CommandBar
控件可以包含子 CommandBarControl
和 CommandBarButtons
。我们将向菜单添加一个 CommandBarControl
。我们可以使用 CommandBar
控件的 Add
方法将项目插入 CommandBar
,因此我们的任务是
- 枚举
CommandBars
以查找名为“**Context Menu**”的CommandBar
。 - 获取
CommandBar
控件。 - 在控件上调用
Add
方法以将项目插入CommandBar
。
好的,最重要的事情之一是 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
结构来描述回调参数。打开插件对象的头文件 (_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 的文章发现,这有点棘手,但并非不可能。
好的,为了将菜单项的**状态**更改为**已选中**,您只需将新插入的 CommandBarControl
的 State
属性更改为 Office::msoButtonDown
,就完成了。State
属性对新插入的 CommandBarControl
不可用,我们将不得不将其转换为 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); }
注意:为了给菜单项添加图标,您可以使用 PasteFace
方法将图像放到按钮上。示例源代码就是这么做的。以下是将图标放在按钮上的代码。
// 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")
MSI 需要功能名称和产品代码才能安装 CDO。产品代码可以从传递给 OnConnection
函数的 Outlook 的 Application
对象中检索。以下是 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); }
好了,就是这样 :D。
我已尽力在教程中提供尽可能多的描述。
提供的示例代码是我的 VB 代码的 C++ 示例。它并未优化到提供最佳结果,而且您可能会发现代码中存在一些糟糕的 COM 实现,但欢迎评论 :)。谢谢。祝您好运 :)。
~被她的爱击中了~
参考和致谢
MSKB 文章
- Q220600 - 如何使用 VC++ 自动化 Outlook。
- Q238228 - 使用 VB 构建 Office2000 COM 插件。
- Q259298 - 如何通过 #import 使用 Outlook 对象模型。
- Q173604 - 如何在 Outlook 解决方案中使用 CommandBars。
- Q182394 - 如何在 Outlook 解决方案中使用 CommandBars。
MSDN 文章
- 为 Outlook 2000 构建 COM 插件 - Thomas Rizzo。
- 开发 MS Office 2000 的 COM 插件。
书籍
- WROX Beginning ATL 3.0 COM Programming - Richard Grimes。