Excel、Exchange和C#

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://linuxstyle.blog.csdn.net/article/details/1536958

摘要: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 英寸磁盘,而且还曾经用一只手装过磁带。业余时间他一直研究雨燕的飞行速度。  
展开阅读全文

Exchange

01-19

DescriptionnnYou are taking part in a large project to automate operations for Northeastern Exchange of Resources and Commodities (NEERC). Different resources and commodities are traded on this exchange via public auction. Each resource or commodity is traded independently of the others and your task is to write a core engine for this exchange — its order book. There is a separate instance of an order book for each traded resource or commodity and it is not your problem to get the correct orders into order books. The order book instance you will be writing is going to receive the appropriate orders from the rest of exchange system.nnOrder book receives a stream of messages. Messages are orders and requests to cancel previously issued orders. Orders that were not cancelled are called active. There are orders to buy and orders to sell. Each order to buy or to sell has a positive size and a positive price. Order book maintains a list of active orders and generates quotes and trades. Active order to buy at the highest price is the best buy order and its price is called bid price. Active order to sell at the lowest price is the best sell order and its price is called ask price. Ask price is always lower than bid price, that is, buyers are willing to pay less than sellers want to receive in return.nnA current quote from the order book contains current bid size, bid price, ask size, and ask price. Here bid and ask sizes are sums of the the sizes of all active orders with the current bid price and the current ask price correspondingly.nnA trade records information about transaction between buyer and seller.nnEach trade has size and price. If an order to buy arrives to the order book at a price greater or equal to the current ask price, then the corresponding orders are matched and trade happens — buyer and seller reached agreement on a price. Vice versa, if an order to sell arrives to the order book at a price less or equal to the current bid price, then trade happens, too. For the purpose of order matching, order book works like a FIFO queue for orders with the same price (read further for details).nnWhen an order to buy arrives to the order book at a price greater or equal to the current ask price it is not immediately entered into the order book. First, a number of trades is generated, possibly reducing the size of incoming order. Trade is generated between incoming buy order and the best order to sell. If there are multiple best orders (at the ask price), then the order that entered the order book first is chosen. Trade is generated at the current ask price with the size of the trade being equal to the smaller of the sizes of two matching orders. Sizes of both matching orders are reduced by the size of the trade. If that reduces the size of sell order to zero, then it becomes inactive and is removed from the order book. If the size of incoming buy order becomes zero, then the process is over — incoming order becomes inactive. If the size of incoming buy order is still positive and there is another sell order to match with, then the process continues generating further trades at the new ask price (ask price can increase as sell orders are traded against and become inactive). If there is no sell order to match with (current ask price became greater than incoming buy order price), then incoming buy order is added to the order book with its remaining size.nnFor incoming sell order everything works similarly – it is matched with buy orders from the order book and trades are generated on bid price.nnOn incoming cancel request the corresponding order is simply removed from the order book and becomes inactive. Note, that by the time of the cancel request the quantity of the corresponding order might have been already partially reduced or the order might have become inactive. Requests to cancel inactive order do not change anything in the order book.nnOn every incoming message the order book has to generate all trades it causes and the current quote (bid size, bid price, ask size, ask price) after processing of the corresponding message, even when nothing has changed in the order book as a result of this message. Thus, the number of quotes the order book generates is always equal to the number of incoming messages.nnInputnnThe first line of the input file contains a single integer number n (1 ≤ n ≤ 10 000) – the number of incoming messages that the order book has to process. The following n lines contain messages. Each line starts with a word describing the message type – BUY, SELL, or CANCEL followed after a space by the message parameters.nnBUY and SELL denote an order to buy or to sell correspondingly, and are followed by two integers q and p (1 ≤ q ≤ 99 999, 1 ≤ p ≤ 99 999) – order size and price. CANCEL denotes a request to cancel previously issued order. It is followed by a single integer i which is the number of the message with some preceding order to buy or to sell (messages are numbered from 1 to n).nnOutputnnWrite to the output file a stream of quotes and trades that the incoming messages generate. For every trade write TRADE followed after space by the trade size and price. For every quote write QUOTE followed after space by the quote bid size, bid price, minus sign (“-”), ask size, ask price (all separated by spaces).nnThere is a special case when there are no active orders to buy or to sell in the order book (bid and/or ask are not defined). This case is treated as follows. If there is no active order to buy, then it is assumed that bid size is zero and bid price is zero. If there is no active order to sell, then it is assumed that ask size is zero and ask price is 99 999. Note, that zero is not a legal price, but 99 999 is a legal price. Recipient of quote messages distinguishes actual 99 999 ask price from the special case of absent orders to sell by looking at its ask size.nnSee example for further clarification.nnSample Inputnn11nBUY 100 35nCANCEL 1nBUY 100 34nSELL 150 36nSELL 300 37nSELL 100 36nBUY 100 38nCANCEL 4nCANCEL 7nBUY 200 32nSELL 500 30nSample OutputnnQUOTE 100 35 - 0 99999nQUOTE 0 0 - 0 99999nQUOTE 100 34 - 0 99999nQUOTE 100 34 - 150 36nQUOTE 100 34 - 150 36nQUOTE 100 34 - 250 36nTRADE 100 36nQUOTE 100 34 - 150 36nQUOTE 100 34 - 100 36nQUOTE 100 34 - 100 36nQUOTE 100 34 - 100 36nTRADE 100 34nTRADE 200 32nQUOTE 0 0 - 200 30 问答

c#创建exchange邮箱问题,有代码?

10-21

c#创建AD用户和exchange邮箱,在asp.net 方式下:有的机器上能够运行,有的不能(能够创建用户,不能创建邮箱)。rn在不能运行的机器上:未安装C#编程环境。:但 同样的程序以c/s win form方式能够运行。rn代码如下:rnDirectoryEntry myADrn…… //创建用户,代码正确rnCDOEXM.IMailboxStore mailbox;rnmailbox = (CDOEXM.IMailboxStore)myAD.NativeObject;rnstring HomeMDB="…"; //邮箱路径rnmailbox.CreateMailbox( HomeMDB ); rnmyAD.CommitChanges();rnrn错误提示:rn参数不正确。 rn说明: 执行当前 Web 请求期间,出现未处理的异常。请检查堆栈跟踪信息,以了解有关该错误以及代码中导致错误的出处的详细信息。 rnrn异常详细信息: System.ArgumentException: 参数不正确。rnrn源错误: rnrn执行当前 Web 请求期间生成了未处理的异常。可以使用下面的异常堆栈跟踪信息确定有关异常原因和发生位置的信息。 rnrn堆栈跟踪: rnrnrn[ArgumentException: 参数不正确。]rn CDOEXM.IMailboxStore.CreateMailbox(String HomeMDBURL) +0rn ADInfo.WebForm1.Button4_Click(Object sender, EventArgs e) in c:\inetpub\wwwroot\ADInfo\webform1.aspx.cs:161rn System.Web.UI.WebControls.Button.OnClick(EventArgs e) +108rn System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument) +57rn System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +18rn System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +33rn System.Web.UI.Page.ProcessRequestMain() +1277rn--------------------------------------------------------------------------------rn版本信息: Microsoft .NET Framework 版本:1.1.4322.573; ASP.NET 版本:1.1.4322.573 rnrnrn高手指教!!! 论坛

没有更多推荐了,返回首页