利用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從OfficeCD光牒安裝。
教程的結尾有一個例子,展示了如何程式設計調用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實作上的不足。歡迎批評指正。謝謝!