Excel、Exchange和C#
摘要:Eric?Gunnerson?將向您介紹如何使用?Outlook、Excel?和?C#?創建自定義的日歷,該日歷可以提供適用于短期項目和長期項目的清晰明了的版式。?
下載?csharp05152003_sample.exe?示例文件(英文)。
雖然一月份已經過去了,我還是決定為您介紹這個遲到的新年解決方案。我決定不再談論我的下一個專欄要說什么,因為每提到一個主題似乎就預示著我將來不會說到它。所以,這個月我將不談論?DirectX。(如果下次談到它,就是違背諾言了。)
在開始之前,先簡短回顧一下上月專欄的內容。雖然我使用了?NUnit?來完成我的單元測試,但您也可以使用?csunit?或?.NETUnit?來完成您的單元測試。有關詳細信息,請參閱?C#?工具頁面(英文)。
c#?程序管理組最近開始使用?Microsoft?Outlook??的日歷,來安排我們各項活動的日程,所以我們全都知道下一次討論安排在什么時間、小組成員何時休假以及要召開的會議安排在什么時間。查看短期日程安排時,這個日歷非常好用,但要查看未來幾個月的日程安排時,它就不那么管用了。我查找了適用于查看長期日程安排的實用程序,但是沒有找到。
看來,應該好好利用?MSDN?了。我寫了一些用于訪問電子郵件的代碼,這些代碼看起來相當簡單,但是我需要一種方法來創建并打印日程安排網格。網格很容易畫,但是要實現跨多個頁面打印并不容易,所以我開始尋找可以打印矩形網格的程序,并了解如何跨頁打印。看起來?Microsoft?Excel?是比較理想的選擇。
要從?C#?訪問?Outlook?和?Excel,需要使用?COM?互操作。要使用?COM?互操作,需要具備互操作程序集以從?C#?端進行引用。您可以從?C#?項目中引用適當的?COM?組件來生成程序集,也可以下載適用于所有?Microsoft?Office?組件的互操作程序集。?
如果需要將程序集安裝到?GAC?中,則不能引用任何未簽名的程序集,因此如果您的程序集需要使用?Office,就需要下載已簽名的程序集(英文)。下載程序集之后,需要向已簽名的程序集添加引用。我將從在?Excel?中創建工作表并設置單元格開始。
使用?Excel
創建項目后,我找到項目中的引用節點,瀏覽到?PIA?所在的目錄,然后添加對?Excel?的引用。?
現在我已準備好使用?Excel?開始工作,但要這樣做,還需要了解?Excel?對象模型。遺憾的是,很難找到正確的信息,所以我嘗試了兩種方法。
第一種方法是使用對象瀏覽器,瀏覽互操作程序集中可用的對象。要了解可以使用哪些方法和屬性,這是一個不錯的方法。
第二個方法是在?Excel?中錄制宏,讓宏完成我需要的操作,然后將?VBA?代碼作為要編寫的?C#?代碼的參考。通常這很容易完成,但是?C#?中的代碼與?VBA?中的有些不同,所以我想簡單介紹一下這種方法。
Excel?宏
我打算寫一個“探測”應用程序,以了解如何完成我要在?Excel?中進行的操作。首先,啟動?Excel?并使其可見,創建一個新的工作表,在其中一個單元格中放入值,然后設置單元格的背景顏色。?
但在操作之前,我想簡單介紹一下?Excel?對象模型。Excel?是最早提供對象模型的?Microsoft?應用程序之一,當時提供的幾種選項現在仍在使用。這意味著有時用起來會不太方便。遇到這些情況時,我會指出來。
啟動?Excel?很容易,使其可見也是如此:
using?Microsoft.Office.Interop.Excel; using?ExcelApplication?=?Microsoft.Office.Interop.Excel.Application;ExcelApplication?excel?=?new?ExcelApplication(); excel.Visible?=?true;
第一個?using?語句引用?Excel?對象和方法。但在?Windows?窗體應用程序中使用這個語句時,我發現?Excel?和?Windows?窗體都有?application?對象。我為?Excel?的?Application?定義了別名,而沒有使用完全限定名稱。在第二個?using?語句中,我將?ExcelApplication?作為?Excel?Application?對象,然后我就可以使用它而不必使用完全限定名稱。?
我將需要的操作錄制為?Excel?宏,如下所示:
????Workbooks.AddRange("C6").SelectActiveCell.FormulaR1C1?=?"Hello"Range("C6").SelectWith?Selection.Interior.ColorIndex?=?6.Pattern?=?xlSolidEnd?With
這看起來不太象?C#?代碼。在?Excel?宏中,有一些特定的假設值和結果,因此我們必須進行一些轉換。例如:
????Workbooks.Add
轉換為:
????Workbook?workbook?=?excel.Workbooks.Add(Missing.Value);
我怎么知道要這樣轉換呢?我首先查看?Application?對象,發現它有一個名為?workbooks?的屬性可以返回?workbooks?對象(這并不奇怪)。所以,在?VBA?代碼中有一個假設的“excel.”。我鍵入?Workbooks.Add(?時,IntelliSense??提示我?add?方法接受一個名為?template?的參數。?
但在?VBA?代碼中并沒有參數,顯然,這是一個可選參數。我們使用的包裝類僅定義了函數的一個版本,因此我們必須傳遞一個表示“使用默認值”的值,該值就是?system.reflection?命名空間中的?Missing.Value。
下一步,在單元格?C6?中設置值。由于?VBA?代碼中的?Workbooks?表示?C#?代碼中的?excel.Workbooks,因此我們可以嘗試使用?excel.Range?來獲取區域。遺憾的是,我們的嘗試失敗了。
實際上,在?Excel?VBA?中編程時,根據您編寫的內容,會有多個假設的前綴。如果您使用?Range,那么實際上就是在使用?excel.ActiveSheet.Range。因此,我們編寫以下代碼:
excel.ActiveSheet.Range("C4").Select();
至少我們可以嘗試這樣寫,但是會發現這樣不能編譯。原來,excel.ActiveSheet?是某種類型的對象。我不能確定這是為什么,只能推測,它可能是工作表或其他對象,也可能只是最初設定的類型的對象。
所以,我們嘗試:
((Worksheet)?excel.ActiveSheet).Range("C4").Select();
這樣會好一些,但在?Worksheet?類中沒有?range?函數。range?在?VBA?領域里是一個屬性,但是在?C#?中,它只是一個接受兩個參數的方法。所以,我們得到以下代碼:
((Worksheet)?excel.ActiveSheet).get_Range("C4",?Missing.Value).Select();
excel.ActiveCell.Value2?=?"Hello";
為什么是?Value2?而不是?FormulaR1C1?這也是我尚未查明的問題。
有兩種方法可以使代碼更簡潔一些。第一種方法是將?Worksheet?對象存儲在變量中,這樣就可以避免類型轉換;第二種方法是對?range?對象執行操作,而不是選擇它并使用活動的單元格。
最后一步是保存工作表,可以通過調用?worksheet.saveas()?來完成。此方法接受十個參數,因此可以將其余參數作為?Missing.Value?傳遞。以下是最終的代碼:
????ExcelApplication?excel?=?new?ExcelApplication();excel.Visible?=?true;excel.Workbooks.Add(Missing.Value);Worksheet?worksheet?=?(Worksheet)?excel.ActiveSheet;Range?r?=?worksheet.get_Range("C6",?Missing.Value);r.Value2?=?"Hello";r.Interior.ColorIndex?=?6;worksheet.SaveAs(@"c:/ExcelExample.xls",?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value,?Missing.Value);?excel.Quit();
創建一個工作表,設置一些值,然后保存并退出,共九行代碼。真是好極了。這些代碼保存在?excelexample?項目中。
使用電子郵件
要訪問?Exchange?電子郵件,可以使用?Outlook?對象模型,也可以使用?CDO(協作數據對象,以前稱為?MAPI)模型。因為我不關心圖形的顯示,所以我要使用?CDO。CDO?不是?Office?的一部分,所以沒有?PIA。
我創建一個新項目,并添加對?COM?對象?Microsoft?CDO?1.21?Library?的引用。然后編寫以下代碼,以獲取收件箱中郵件的數量:
?????????using?MAPI;using?System.Reflection;Session?session?=?new?Session();session.Logon("Default?Outlook?Profile",?Missing.Value,Missing.Value,Missing.Value,Missing.Value,Missing.Value,Missing.Value);Folder?folder?=?(Folder)?session.Inbox;Messages?messages?=?(Messages)?folder.Messages;int?messageCount?=?(int)?messages.Count;
與?Excel?一樣,MAPI/CDO?對象模型出現的很早,其中的每項內容都被定義為對象,甚至象文件夾中郵件數量都是如此。通常,我會編寫?MAPI?對象的包裝對象,這樣就可以不進行類型轉換就直接使用它們。我為文件夾和?Messages?集合編寫了兩個包裝程序,您可以使用?foreach?對它們進行遍歷。
上述準備工作完成后,我可以編寫以下代碼來查看收件箱中的所有郵件:
?????????MapiFolder?inbox?=?new?MapiFolder(session.Inbox);int?size?=?0;int?count?=?0;foreach?(MAPI.Message?message?in?inbox.Messages){size?+=?(int)?message.Size;count++;}
當我運行這段代碼時,發現我的?Exchange?收件箱中有?2982?封郵件,占用的空間超過了?33?MB。?
如果我要查看所有文件夾,我可以編寫一個遞歸函數:
??????public?int?TraverseFolder(MapiFolder?folder){int?size?=?0;foreach?(MapiFolder?subFolder?in?folder){size?+=?TraverseFolder(subFolder);}foreach?(MAPI.Message?message?in?folder.Messages){size?+=?(int)?message.Size;}return?size;}
如果我運行這段代碼,大約一分多鐘以后,它就會告訴我,我的整個收件箱樹占用了大約?88?MB?空間。我想我需要做些清理工作。
處理約會
起初,mapi?只是處理郵件。添加了其他類型的項后,它出現了一個問題。如果我的代碼用于取回?message?項,而意外地取回了?appointment?項,代碼將會中斷。所以,如果我打開一個郵箱并找到?calendar?子文件夾,我將取回由郵件而不是由約會組成的文件夾。如果我要查找一個約會的主題,這樣很有效,但是如果我要獲取開始日期和結束日期,就比較困難了。
為解決這個問題,mapi?添加了一個名為?getdefaultfolder()?的新函數,我可以通過調用它來指定我真正需要的?appointmentitems?集合,而不是?messages?集合。因此,我可以編寫以下代碼:
??????public?void?TraverseCalendar(Session?session){Folder?calendar?=(Folder)?session.GetDefaultFolder(
ActMsgDefaultFolderTypes.ActMsgDefaultFolderCalendar);Messages?messages?=?(Messages)calendar.Messages;AppointmentItem?message?=?(AppointmentItem)?messages.GetFirst(Missing.Value);while?(message?!=?null){string?subject?=?(string)?message.Subject;message?=?(AppointmentItem)?messages.GetNext();}}?
我沒有編寫?Appointments?集合的包裝程序,這就是我編寫的沒有包裝程序的代碼。?
這段代碼運行良好,但還有一個缺點。我只能獲取我的郵箱的默認文件夾,而不能獲取其他人的郵箱的文件夾。您可能還記得,我的目標是查看其他人郵箱中的約會,而這個方法沒有解決問題。
所以,我又回到?Google?進行更多的研究。結果是,除了郵件中特定的項外,還有一個包含此類型所有字段的?fields?項,這些字段按編號存儲。因此,如果我知道正確的編號,我就可以獲取特定字段的值。
下面是我最后編寫的代碼:
?????????InfoStore?infoStore?=FindInfoStore(session,?mailbox);MapiFolder?rootFolder?=?new?MapiFolder((Folder)?infoStore.RootFolder);MapiFolder?calendar?=?rootFolder.FindSubFolder("Calendar");DateTime?graphEndDate?=?graphStartDate?+?new?TimeSpan(days,?0,?0,?0);foreach?(MAPI.Message?message?in?calendar.Messages){DateTime?startDate?=?(DateTime)?GetFieldValue(message,?6291520);DateTime?endDate?=?(DateTime)GetFieldValue(message,?6357056);if?(endDate?<?graphStartDate)continue;if?(startDate?>?graphEndDate)continue;if?(startDate?<?graphStartDate){startDate?=?graphStartDate;}if?(endDate?>?graphEndDate){endDate?=?graphEndDate;}int?labelIndex?=?0;try{labelIndex?=?(int)?GetFieldValue(message,?-2093678589);}catch?(Exception?e){string?s?=?e.Message;}Appointment?appointment?=?new?Appointment((string)?message.Subject,?labelIndex,startDate,endDate);appointments.Add(appointment);}
GetFieldValue()?將查找郵件的所有字段,以搜索特定編號的字段。最好將那些常數放入有著明確名稱的靜態常數中。
雖然不太漂亮,但它可以達到預期的目的。遺憾的是,我還不知道如何處理周期性的約會。有兩種可能的選擇:?
- 嘗試我用過的相同辦法,并對存儲周期性事件的對象進行解碼。?
- 不使用?CDO,而用其他方法處理?Exchange,例如?WebDAV。?
把代碼合在一起
處理?Excel?和?Exchange?之后,我開始編寫真正的應用程序。具有挑戰性的任務是解決如何在網格中完成約會的版式,這確實有些復雜,所以我寫了一些單元測試來作為指導。?
要編寫單元測試,我需要針對某些內容進行測試。針對實時日歷進行測試不太順暢,因為各種約會時有時無。因此,我將日歷操作抽象為?icalendar?接口,并創建了兩個實現該接口的類。第一個類是真實的,使用了?CDO;第二個是虛擬的,我只在其中創建了用于測試的對象。
這樣我就可以編寫單元測試,以測試用于版式的代碼,然后在?Excel?中執行排版。
我還為?Excel?對象編寫了類似的接口和虛擬對象,但我選擇了“手動驗證”在?Excel?中創建的正確結果。
?
eric?Gunnerson?是?Visual?C#?組的程序經理,以前曾是?C#?語言設計組的成員,著有? A?Programmer's?Introduction?to?C#,?2nd?Edition(英文)。他從事編程工作已經有很長時間,積累了豐富的編程經驗,他知道?8?英寸磁盤,而且還曾經用一只手裝過磁帶。業余時間他一直研究雨燕的飛行速度。??
總結
以上是生活随笔為你收集整理的Excel、Exchange和C#的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 多少钱一克银啊?
- 下一篇: 用C#实现在PowerPoint文档中搜