天天看点

Outlook Add-in

  利用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),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

          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),0xf002

       ,OnFolderChange,&OnSimpleEventInfo)

  SINK_ENTRY_INFO(2,__uuidof(Office::_CommandBarsEvents),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* Ctrl,VARIANT_BOOL * CancelDefault);

void __stdcall COAddin::OnClickButtonSort(IDispatch* 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实现上的不足。欢迎批评指正。谢谢!

继续阅读