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),/*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实现上的不足。欢迎批评指正。谢谢!

 

恋恋蝶舞 今年烟花不寂寞


 


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Add-in Express是一个用于开发Microsoft OfficeMicrosoft .NET应用程序的工具。它提供了一个简单易用的平台,使开发人员能够快速构建自定义的Office插件和扩展。 使用Add-in Express,开发人员可以利用Visual Studio IDE来创建自定义的Ribbon菜单、工具栏、任务窗格和自定义对话框等Office扩展。它提供了丰富的设计时集成,让开发人员能够轻松地在设计阶段实现快速调试和测试。 Add-in Express支持多种Office应用程序,包括Word、Excel、Outlook、PowerPoint和Visio。它还支持连接到外部数据库和Web服务,以实现更强大的功能。 使用Add-in Express,开发人员不需要深入了解Office的内部结构和编程模型,也不需要编写大量的代码。它提供了一系列的可视化设计工具和代码生成器,可以自动生成所需的代码。开发人员只需要专注于业务逻辑的实现,而不必担心底层的技术细节。 Add-in Express还提供了丰富的文档和示例代码,帮助开发人员快速上手和解决问题。它还有强大的社区支持,开发人员可以在官方论坛上与其他开发人员交流经验和解决方案。 总结来说,Add-in Express是一个功能强大且易于使用的工具,可帮助开发人员快速构建自定义的Office插件和扩展。无论是初学者还是有经验的开发人员,都可以从中受益。它可以大大提高开发效率,节省时间和精力。所以,如果你需要开发自定义的Office插件,Add-in Express是一个值得考虑的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值