65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (28投票s)

2004 年 8 月 17 日

15分钟阅读

viewsIcon

358036

downloadIcon

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 结构设置**参数**。设置我们的接收器接口并调用 DispEventAdviseDispEventUnAdvise 来启动和终止与源接口的连接。

代码如下所示

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 对象:SessionMessagesMessageFolderFieldsField。为了使这些对象可供您的应用程序使用,您需要在应用程序中导入 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
  • 获取每个 AttachmentFields
  • 遍历 Fields 以查找 Field (H370E001E)(这是 Attachment 的 MIME 类型)
  • 将字段值与“application/x-pkcs7-mime”进行测试

现在,向您的类添加一个新的成员函数,名为 IsCDOEncrypted。该函数接受一个 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(); 
     } 
   }

自定义项目的分组和排序

Sample screenshot

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::ViewXML 属性返回当前视图的**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。

好了,现在是时候编译了,编译您的项目,如果一切顺利,您将看到您的电子邮件按两个组排序。

将项目添加到右键菜单

Sample screenshot

好了,我认为这是本教程中最受期待的部分,而且你们中的大多数人都会关心这一部分,而不是深挖加密逻辑 :D。

**_Amit Dey_** 已经详细解释了如何将项目添加到菜单和工具栏。如果您还没有阅读那篇文章,现在是时候阅读了,因为我不会解释 **CommandBars** 等的细节。

为了将项目添加到 **Outlook** 的右键菜单,我们将不得不映射 **Command Bars** 的 OnUpdate 事件。我们可以通过 ExplorerCommandBars 属性获取 **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 控件可以包含子 CommandBarControlCommandBarButtons。我们将向菜单添加一个 CommandBarControl。我们可以使用 CommandBar 控件的 Add 方法将项目插入 CommandBar,因此我们的任务是

  • 枚举 CommandBars 以查找名为“**Context Menu**”的 CommandBar
  • 获取 CommandBar 控件。
  • 在控件上调用 Add 方法以将项目插入 CommandBar

好的,最重要的事情之一是 CommandBar 对象被 Outlook **锁定**;为了调用 Add 方法,我们必须移除 CommandBarControl 对象的**保护**,否则调用 Add 方法将失败并出现**断言**。我们可以使用 CommandBarProtection 属性来移除保护。

让我们更改 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);

好了,现在编译您的项目,然后玩玩您的菜单。如果您单击一次,邮件项会自动排序,如果您再次单击,邮件项将不会再次排序。

如何将菜单的状态更改为*已选中*?

Sample screenshot

好吧,还有更多要写的 :S,以及更多要读的。

好的,之前我在开发一个解决方案时,一位**Microsoft MVP**(我不点名你;))回复我说,无法将**图标添加到右键**菜单项或将其状态更改为*已选中*,我不得不没有这些功能就将项目交付给了我的客户。后来,我通过 Outlook Spy 和 Amit Dey 的文章发现,这有点棘手,但并非不可能。

好的,为了将菜单项的**状态**更改为**已选中**,您只需将新插入的 CommandBarControlState 属性更改为 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。

其他

© . All rights reserved.