Excel、Exchange和C#

本文介绍了如何使用C#结合Outlook和Excel创建一个自定义的日历应用,包括如何通过COM互操作访问Outlook日历数据及在Excel中进行布局与打印。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要:EricGunnerson将向您介绍如何使用Outlook、Excel和C#创建自定义的日历,该日历可以提供适用于短期项目和长期项目的清晰明了的版式。

下载csharp05152003_sample.exe示例文件(英文)。

虽然一月份已经过去了,我还是决定为您介绍这个迟到的新年解决方案。我决定不再谈论我的下一个专栏要说什么,因为每提到一个主题似乎就预示着我将来不会说到它。所以,这个月我将不谈论DirectX。(如果下次谈到它,就是违背诺言了。)

在开始之前,先简短回顾一下上月专栏的内容。虽然我使用了NUnit来完成我的单元测试,但您也可以使用csunit或.NETUnit来完成您的单元测试。有关详细信息,请参阅C#工具页面(英文)。

c#程序管理组最近开始使用MicrosoftOutlook®的日历,来安排我们各项活动的日程,所以我们全都知道下一次讨论安排在什么时间、小组成员何时休假以及要召开的会议安排在什么时间。查看短期日程安排时,这个日历非常好用,但要查看未来几个月的日程安排时,它就不那么管用了。我查找了适用于查看长期日程安排的实用程序,但是没有找到。

看来,应该好好利用MSDN了。我写了一些用于访问电子邮件的代码,这些代码看起来相当简单,但是我需要一种方法来创建并打印日程安排网格。网格很容易画,但是要实现跨多个页面打印并不容易,所以我开始寻找可以打印矩形网格的程序,并了解如何跨页打印。看起来MicrosoftExcel是比较理想的选择。

要从C#访问Outlook和Excel,需要使用COM互操作。要使用COM互操作,需要具备互操作程序集以从C#端进行引用。您可以从C#项目中引用适当的COM组件来生成程序集,也可以下载适用于所有MicrosoftOffice组件的互操作程序集。

如果需要将程序集安装到GAC中,则不能引用任何未签名的程序集,因此如果您的程序集需要使用Office,就需要下载已签名的程序集(英文)。下载程序集之后,需要向已签名的程序集添加引用。我将从在Excel中创建工作表并设置单元格开始。

使用Excel

创建项目后,我找到项目中的引用节点,浏览到PIA所在的目录,然后添加对Excel的引用。

现在我已准备好使用Excel开始工作,但要这样做,还需要了解Excel对象模型。遗憾的是,很难找到正确的信息,所以我尝试了两种方法。

第一种方法是使用对象浏览器,浏览互操作程序集中可用的对象。要了解可以使用哪些方法和属性,这是一个不错的方法。

第二个方法是在Excel中录制宏,让宏完成我需要的操作,然后将VBA代码作为要编写的C#代码的参考。通常这很容易完成,但是C#中的代码与VBA中的有些不同,所以我想简单介绍一下这种方法。

Excel宏

我打算写一个“探测”应用程序,以了解如何完成我要在Excel中进行的操作。首先,启动Excel并使其可见,创建一个新的工作表,在其中一个单元格中放入值,然后设置单元格的背景颜色。

但在操作之前,我想简单介绍一下Excel对象模型。Excel是最早提供对象模型的Microsoft应用程序之一,当时提供的几种选项现在仍在使用。这意味着有时用起来会不太方便。遇到这些情况时,我会指出来。

启动Excel很容易,使其可见也是如此:

usingMicrosoft.Office.Interop.Excel;
usingExcelApplication=Microsoft.Office.Interop.Excel.Application;

ExcelApplicationexcel=newExcelApplication();
excel.Visible=true;

第一个using语句引用Excel对象和方法。但在Windows窗体应用程序中使用这个语句时,我发现Excel和Windows窗体都有application对象。我为Excel的Application定义了别名,而没有使用完全限定名称。在第二个using语句中,我将ExcelApplication作为ExcelApplication对象,然后我就可以使用它而不必使用完全限定名称。

我将需要的操作录制为Excel宏,如下所示:

Workbooks.Add
Range("C6").Select
ActiveCell.FormulaR1C1="Hello"
Range("C6").Select
WithSelection.Interior
.ColorIndex=6
.Pattern=xlSolid
EndWith

这看起来不太象C#代码。在Excel宏中,有一些特定的假设值和结果,因此我们必须进行一些转换。例如:

Workbooks.Add

转换为:

Workbookworkbook=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来获取区域。遗憾的是,我们的尝试失败了。

实际上,在ExcelVBA中编程时,根据您编写的内容,会有多个假设的前缀。如果您使用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传递。以下是最终的代码:

ExcelApplicationexcel=newExcelApplication();
excel.Visible=true;

excel.Workbooks.Add(Missing.Value);
Worksheetworksheet=(Worksheet)excel.ActiveSheet;

Ranger=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对象MicrosoftCDO1.21Library的引用。然后编写以下代码,以获取收件箱中邮件的数量:

usingMAPI;
usingSystem.Reflection;

Sessionsession=newSession();
session.Logon("DefaultOutlookProfile",
Missing.Value,
Missing.Value,
Missing.Value,
Missing.Value,
Missing.Value,
Missing.Value
);

Folderfolder=(Folder)session.Inbox;

Messagesmessages=(Messages)folder.Messages;

intmessageCount=(int)messages.Count;

与Excel一样,MAPI/CDO对象模型出现的很早,其中的每项内容都被定义为对象,甚至象文件夹中邮件数量都是如此。通常,我会编写MAPI对象的包装对象,这样就可以不进行类型转换就直接使用它们。我为文件夹和Messages集合编写了两个包装程序,您可以使用foreach对它们进行遍历。

上述准备工作完成后,我可以编写以下代码来查看收件箱中的所有邮件:

MapiFolderinbox=newMapiFolder(session.Inbox);

intsize=0;
intcount=0;
foreach(MAPI.Messagemessageininbox.Messages)
{
size+=(int)message.Size;
count++;
}

当我运行这段代码时,发现我的Exchange收件箱中有2982封邮件,占用的空间超过了33MB。

如果我要查看所有文件夹,我可以编写一个递归函数:

publicintTraverseFolder(MapiFolderfolder)
{
intsize=0;

foreach(MapiFoldersubFolderinfolder)
{
size+=TraverseFolder(subFolder);
}

foreach(MAPI.Messagemessageinfolder.Messages)
{
size+=(int)message.Size;
}
returnsize;
}

如果我运行这段代码,大约一分多钟以后,它就会告诉我,我的整个收件箱树占用了大约88MB空间。我想我需要做些清理工作。

处理约会

起初,mapi只是处理邮件。添加了其他类型的项后,它出现了一个问题。如果我的代码用于取回message项,而意外地取回了appointment项,代码将会中断。所以,如果我打开一个邮箱并找到calendar子文件夹,我将取回由邮件而不是由约会组成的文件夹。如果我要查找一个约会的主题,这样很有效,但是如果我要获取开始日期和结束日期,就比较困难了。

为解决这个问题,mapi添加了一个名为getdefaultfolder()的新函数,我可以通过调用它来指定我真正需要的appointmentitems集合,而不是messages集合。因此,我可以编写以下代码:

publicvoidTraverseCalendar(Sessionsession)
{
Foldercalendar=
(Folder)session.GetDefaultFolder(
ActMsgDefaultFolderTypes.ActMsgDefaultFolderCalendar);

Messagesmessages=(Messages)
calendar.Messages;

AppointmentItemmessage=
(AppointmentItem)messages.GetFirst(Missing.Value);
while(message!=null)
{
stringsubject=(string)message.Subject;
message=(AppointmentItem)messages.GetNext();
}
}

我没有编写Appointments集合的包装程序,这就是我编写的没有包装程序的代码。

这段代码运行良好,但还有一个缺点。我只能获取我的邮箱的默认文件夹,而不能获取其他人的邮箱的文件夹。您可能还记得,我的目标是查看其他人邮箱中的约会,而这个方法没有解决问题。

所以,我又回到Google进行更多的研究。结果是,除了邮件中特定的项外,还有一个包含此类型所有字段的fields项,这些字段按编号存储。因此,如果我知道正确的编号,我就可以获取特定字段的值。

下面是我最后编写的代码:

InfoStoreinfoStore=
FindInfoStore(session,mailbox);

MapiFolderrootFolder=
newMapiFolder((Folder)infoStore.RootFolder);
MapiFoldercalendar=rootFolder.FindSubFolder("Calendar");

DateTimegraphEndDate=
graphStartDate+newTimeSpan(days,0,0,0);
foreach(MAPI.Messagemessageincalendar.Messages)
{
DateTimestartDate=(DateTime)
GetFieldValue(message,6291520);
DateTimeendDate=(DateTime)
GetFieldValue(message,6357056);

if(endDate<graphStartDate)
continue;

if(startDate>graphEndDate)
continue;

if(startDate<graphStartDate)
{
startDate=graphStartDate;
}

if(endDate>graphEndDate)
{
endDate=graphEndDate;
}

intlabelIndex=0;
try
{
labelIndex=(int)GetFieldValue(message,-2093678589);
}
catch(Exceptione)
{
strings=e.Message;
}

Appointmentappointment=
newAppointment((string)message.Subject,
labelIndex,
startDate,
endDate);
appointments.Add(appointment);
}

GetFieldValue()将查找邮件的所有字段,以搜索特定编号的字段。最好将那些常数放入有着明确名称的静态常数中。

虽然不太漂亮,但它可以达到预期的目的。遗憾的是,我还不知道如何处理周期性的约会。有两种可能的选择:

  1. 尝试我用过的相同办法,并对存储周期性事件的对象进行解码。
  2. 不使用CDO,而用其他方法处理Exchange,例如WebDAV。

把代码合在一起

处理Excel和Exchange之后,我开始编写真正的应用程序。具有挑战性的任务是解决如何在网格中完成约会的版式,这确实有些复杂,所以我写了一些单元测试来作为指导。

要编写单元测试,我需要针对某些内容进行测试。针对实时日历进行测试不太顺畅,因为各种约会时有时无。因此,我将日历操作抽象为icalendar接口,并创建了两个实现该接口的类。第一个类是真实的,使用了CDO;第二个是虚拟的,我只在其中创建了用于测试的对象。

这样我就可以编写单元测试,以测试用于版式的代码,然后在Excel中执行排版。

我还为Excel对象编写了类似的接口和虚拟对象,但我选择了“手动验证”在Excel中创建的正确结果。


ericGunnerson是VisualC#组的程序经理,以前曾是C#语言设计组的成员,著有 AProgrammer'sIntroductiontoC#,2ndEdition(英文)。他从事编程工作已经有很长时间,积累了丰富的编程经验,他知道8英寸磁盘,而且还曾经用一只手装过磁带。业余时间他一直研究雨燕的飞行速度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值