• Excel、Exchange 和 C# (摘要)


    ExcelExchange C#
    Eric Gunnerson
    Microsoft Corporation
    2003年4月21日

    摘要: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.Add
    Range("C6").Select
    ActiveCell.FormulaR1C1 = "Hello"
    Range("C6").Select
    With Selection.Interior
        .ColorIndex = 6
        .Pattern = xlSolid
    End 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() 将查找邮件的所有字段,以搜索特定编号的字段。最好将那些常数放入有着明确名称的静态常数中。

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

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

    把代码合在一起

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

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

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

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


    Eric Gunnerson 是 Visual C# 组的程序经理,以前曾是 C# 语言设计组的成员,著有 A Programmer's Introduction to C#, 2nd Edition(英文)。他从事编程工作已经有很长时间,积累了丰富的编程经验,他知道 8 英寸磁盘,而且还曾经用一只手装过磁带。业余时间他一直研究雨燕的飞行速度。

  • 相关阅读:
    三个心态做人做学问 沧海
    成功走职场要找准自己的"快捷键" 沧海
    免费离线下载 拂晓风起
    Hibernate 获取某个表全部记录时 奇怪现象 (重复出现某个记录) 拂晓风起
    无法读取mdb 如果连接不了ACCESS mdb文件,就尝试安装MDAC 拂晓风起
    Netbeans 使用 Hibernate 逆向工程 生成hbm和pojo 拂晓风起
    如何点击单选框 radio 后面的文字,选中单选框 拂晓风起
    Java 连接access 使用access文件 不用配置 拂晓风起
    mysql下如何执行sql脚本 拂晓风起
    Hibernate配置access Hibernate 连接 access 拂晓风起
  • 原文地址:https://www.cnblogs.com/jhabb/p/jhabb.html
Copyright © 2020-2023  润新知