《Visual Studio Tools for Office: Using C# with Excel, Word, Outlook, and InfoPath 》——By Eric Carter, Eric Lippert
第一部分:VSTO介绍
本书的第一部分介绍了Office对象模型和Office主互操作程序集(PIA)。您还将学习如何使用Visual Studio使用Visual Studio 2005 Tools for Office(VSTO)的功能来构建文档后面的自动化可执行文件,加载项和代码。
第一章“办公编程介绍”介绍了Office对象模型,并对其基本结构进行了研究。本章介绍如何使用对象,集合和枚举所有Office对象模型中找到的基本类型。您还将学习如何使用Office对象模型中的对象和集合公开的属性,方法和事件。第1章还介绍了将Office对象模型暴露于.NET代码的PIAs,并介绍了如何在VSTO项目中使用和引用Office PIA。
第2章“Office解决方案简介”介绍了Office应用程序的定制和扩展的主要方式。本章介绍可以使用VSTO创建的各种Office解决方案。
这本书的其他部分
第二部分.NET中的Office编程
本书的第二部分更深入地介绍了Office对象模型。第3章到第5章涵盖了Excel,第6章到第8章封面Word,第9章到第11章封面展示,第12章介绍了InfoPath。这些章节还有一些关于应用特定功能和问题的讨论。例如,第3章讨论如何在.NET for Excel中构建自定义公式。第5章详细讨论了Excel特定的“区域”问题。您可以选择第二部分的哪些章节,只要您只对Excel开发感兴趣,可以阅读第3章至第5章,然后跳到本书第3部分。
第三部分VSTO办公编程
本书第三部分由第13至20章组成,介绍了Visual Studio 2005 Tools for Office为Office开发带来的功能。第三部分描述了VSTO的所有功能,包括在Excel和Word文档中使用Windows窗体控件,使用与Office对象的数据绑定,构建智能标记以及将Windows窗体控件添加到Office的任务窗格。
第四部分高级办公室编程
最后,本书第四部分介绍了高级编程主题。第21章和第22章谈到使用VSTO在Word和Excel中使用XML。第23章介绍如何构建Word和Excel的托管COM加载项。第24章介绍如何在VSTO中开发Outlook加载项。
为什么Office编程?
本书涵盖的Office 2003应用程序系列(Excel 2003,Word 2003,Outlook 2003和InfoPath 2003)是构建解决方案的有吸引力的平台。您可以通过针对对象模型开发解决方案来定制和扩展应用程序。通过使用Office System构建解决方案,您可以重用一些功能最丰富和流行的应用程序。分析或显示数据的解决方案可以利用Excel的格式,图表,计算和分析功能。创建文档的解决方案可以使用Word的功能来生成,格式化和打印文档。处理业务信息的解决方案可以将其显示在Outlook文件夹或InfoPath表单中。重用您已经知道的应用程序比从头开始构建这些功能要好得多。
信息工作者每天都使用Office环境。使用Office构建的解决方案可以成为该环境的无缝部分。用户通常必须访问网页或其他公司应用程序,以获取要剪切的数据,并粘贴到Excel工作簿或Word文档中。许多用户希望使用Outlook作为其商业信息门户。通过将解决方案与Office集成,您可以让用户获取所需的信息,而无需切换到其他应用程序。
办公室编程和专业开发人员
历史上,大多数Office编程已经通过Visual Basic for Applications(VBA)和一些Office应用程序内置的宏记录功能完成。用户将记录一个宏以自动执行Office应用程序中的重复任务。有时,通过录制宏创建的代码将使用VBA进一步修改,并将其转化为更复杂的部门解决方案,由没有被编程人员训练并且其主要工作不是编程的用户使用。这些解决方案有时会让企业的食物链变得越来越重要,被专业开发人员所接管,成为业务解决方案。
不幸的是,VBA及其对宏观录制的关注有时导致Office解决方案对于企业和专业开发人员来说太有限。专业开发人员可能难以将VBA解决方案扩展到整个企业。 VBA解决方案在部署后难以更新。通常,专业开发人员希望使用VBA以外的语言来继续增长解决方案。 VBA的易用性虽然对刚开始编码的用户来说是一种福音,但对于希望拥有更丰富的编程环境的专业开发人员来说,却受到限制。
为什么选择Office for Office?
.NET Framework及其相关的类库,技术和语言解决了专业开发人员对Office开发的许多担忧。今天的办公室开发可以使用Visual Studio 2005,这是专业开发人员丰富的编程环境。开发人员可以使用.NET语言,如Visual Basic .NET或C#。 PIA允许.NET代码调用Office应用程序公开的非托管对象模型。丰富的.NET类库使开发人员能够使用诸如Windows Forms之类的技术构建Office解决方案,以显示用户界面(UI)和Web服务来连接到企业数据服务器。
为什么Visual Studio Tools for Office?
Visual Studio Tools 2005 for Office(VSTO)将Word,Excel,Outlook和InfoPath编程的.NET支持添加到Visual Studio。 VSTO将正在编程的Word或Excel文档转换为.NET类,充满数据绑定支持,可以像Windows Forms控件和其他.NET功能一样编码的控件。它使.NET代码易于集成到Outlook中。它使开发人员将.NET代码放在InfoPath表单后面。开发人员甚至可以针对Office对象编程,而不必遍历整个Office对象模型。
.NET如何?
本书讨论了许多针对Office应用程序的.NET编程方法。然而,使用.NET,Office编程的某些方面仍然很尴尬。大多数这些尴尬的领域是由于Office对象模型被设计为使用称为COM的技术来实现的。虽然.NET代码可以通过PIAs与Office对象模型通信,但是对象模型有时候感觉不到.NET友好。此外,Office对象模型并不总是遵循针对.NET设计的类的命名约定或设计模式。
在将来,许多Office对象模型可能会重新设计为.NET,而对象模型会让.NET开发人员感到友善。现在,开发人员必须生活在一个过渡期,其中Office程序设计的某些方面感觉像是针对.NET而设计的,而其他方面则没有。本书讨论了使用.NET与O时开发人员遇到的一些最困难的问题
Office对象模型
几乎所有Office编程都涉及编写使用Office应用程序的对象模型的代码。对象模型是Office应用程序提供的一组对象,运行代码可用于控制Office应用程序。每个Office应用程序的对象模型是按照层次结构的方式组织的。从Application对象中,可以访问构成Office应用程序对象模型的其他对象。
作为对象模型对象在对象模型层次结构中如何相关的示例,图1-1显示了Word对象模型中的一些最重要的对象。根对象是Application对象。该图还显示了一些其他对象,包括文档,文档,段落和段落。 Application对象和Documents对象是相关的,因为通过Application对象上的属性返回Documents对象。其他对象不能从根应用程序对象直接访问,但可以通过遍历路径访问。例如,通过遍历从应用程序到文档到文档到段落的路径访问Paragraphs对象。图1-2显示了Excel对象模型层次结构中某些主要对象的相似图。
对象
每个Office应用程序的对象模型都包含许多可以用来控制Office应用程序的对象。 Word有248个不同的对象,Excel有196个,Outlook有67.对象倾向于对应于应用程序本身的特征和概念。 例如,Word具有对应于Word的特征的诸如Document,Bookmark和Paragraphall的对象。 Excel具有对应于Excel功能的诸如Workbook,Worksheet,Font,Hyperlink,Chart和Seriesall之类的对象。 如您所想,对象模型中最重要和最常用的对象是与应用程序本身,文档和文档中的关键元素(如Word中的文本范围)对应的对象。 大多数解决方案使用这些关键对象,只有少数对象模型中的其他对象。 表1-1列出了Word,Excel和Outlook中的一些关键对象,并简要介绍了这些对象的作用。
Office对象模型中的对象与典型的.NET类开始不同之处在于绝大多数对象模型对象不可创建或“新建”。在大多数Office对象模型中,可以使用new关键字创建的对象数量大约是一到五个对象。在大多数Office解决方案中,新的永远不会被用于创建一个Office对象,已经创建的Office对象(通常是根应用程序对象)被传递给解决方案。
因为大多数Office对象模型对象不能直接创建,而是通过对象模型层次结构来访问它们。例如,清单1-1显示了如何从Application对象中获取Excel中的Worksheet对象。该代码是一种漫长的方式来导航层次结构,因为它声明一个变量来存储每个对象在遍历层次结构时。该代码假定根Excel应用程序对象已传递给代码并分配给名为app的变量。它还使用C#作为运算符将Worksheets集合返回的对象转换为工作表,这是必要的,因为Worksheet集合是第3章“编程Excel”中描述的原因的对象集合。
清单1-1 从应用程序对象导航到Excel中的工作表
Excel.Workbooks myWorkbooks = app.Workbooks; Excel.Workbook myWorkbook = myWorkbooks.get_Item(1); Excel.Worksheets myWorksheets = myWorkbook.Worksheets; Excel.Worksheet myWorksheet = myWorksheets.get_Item(1) as Excel.Worksheet;
如果代码不需要在变量中缓存每个对象模型对象,但只需要获取一个Worksheet对象,则编写此代码的更有效的方法如下所示:
Excel.Worksheet myWorksheet2 = app.Workbooks.get_Item(1).Worksheets.get_Item(1) as Excel.Worksheet;
集合
段落和文档是称为集合的对象类型的示例。集合是表示一组对象的专用对象。通常,将集合命名为其名称是其包含的对象的类型的复数。例如,Documents集合对象是Document对象的集合。一些集合对象可以是诸如字符串的值类型的集合。
集合通常具有一组标准的属性和方法。集合具有Count属性,它返回集合中的对象数。一个集合也有一个Item方法,它接受一个参数(通常是数字)来指定集合中所需对象的索引。除了这些标准属性和方法之外,集合可能具有其他属性和方法。
清单1-2显示了使用集合的Count属性和集合的Item方法的集合的迭代。虽然这不是迭代集合的首选方法(通常使用foreach代替),但它说明了两个关键点。首先,Office对象模型中的集合几乎总是基于1,这意味着它们以索引1而不是索引为0开始。其次,传递给get_Item方法的参数通常作为对象传递,因此您可以将数字索引指定为int或集合中对象的名称作为字符串。
清单1-2。 使用Count属性和get_Item方法迭代集合,使用int或string Index
Excel.Workbooks myWorkbooks = app.Workbooks; int workbookCount = myWorkbooks.Count; for (int i = 1; i <= workbookCount; i++) { // Get the workbook by its int index Excel.Workbook myWorkbook = myWorkbooks.get_Item(i); // Get the workbook by its string index string workbookName = myWorkbook.Name; Excel.Workbook myWorkbook2 = myWorkbooks.get_Item(workbookName); MessageBox.Show(String.Format("Workbook {0}", myWorkbook2.Name)); }
如果要查看Workbooks集合的get_Item方法的定义,您将看到它需要一个对象参数。 即使get_Item方法接受一个对象参数,我们在清单1-2中传递一个int值和一个字符串值。 这是因为,当您将值类型传递给接受对象的方法时,C#可以自动将值类型(例如int或字符串)转换为对象。 这种自动转换称为拳击。 C#自动创建一个被称为框的对象实例,将值类型传递给方法。
迭代集合的首选方法是使用C#的foreach语法,如清单1-3所示。
清单1-3 使用foreach迭代集合
Excel.Workbooks myWorkbooks = app.Workbooks; foreach (Excel.Workbook workbook in myWorkbooks) { MessageBox.Show(String.Format("Workbook {0}", workbook.Name)); }
有时,您可能希望通过在每个对象上调用Delete方法来遍历集合并从集合中删除对象。这是一个冒险的做法,因为如果您在迭代过程中从其中删除项目时,Office对象模型中的集合行为有时会被定义。相反,当您遍历Office对象模型集合时,将要删除的对象添加到您创建的.NET集合中,例如列表或数组。在迭代Office对象模型集合并将要删除的所有对象添加到集合之后,迭代您的集合并调用每个对象上的Delete方法。
枚举
枚举是在对象模型中定义的类型,表示一组固定值。 Word对象模型包含252个枚举,Excel 195和Outlook 55。
作为枚举的示例,Word的对象模型包含一个名为WdWindowState的枚举。 WdWindowState是一个枚举,它有三个可能的值:wdWindowStateNormal,wdWindowStateMaximize,wdWindowStateMinimize。这些是在测试值时可以直接在代码中使用的常量。每个值对应一个整数值。 (例如,wdWindowStateNormal等效于0.)但是,它被认为是坏的编程风格,以便与整数值进行比较,而不是常量名称本身,因为它使代码更不可读。
属性,方法和事件
Office应用程序对象模型中的对象是具有可由解决方案代码访问的属性,方法和事件的.NET类。 对象模型中的对象必须至少有一个属性,方法或事件。 Office应用程序对象模型中的大多数对象具有几个属性,几个方法,也没有事件。 对象模型中最重要的对象(如应用程序和文档)通常要复杂得多,并且具有更多数量的属性和方法以及事件。 例如,Word的Application对象有大约100个属性,60个方法和20个事件。 表1-2列出了Word Application对象中的一些属性,方法和事件,以便了解对象模型对象提供的功能类型。
在Office对象模型中,属性占优势,其次是方法,并且随着事件的不同而拖累。 图1-3显示了Word,Excel和Outlook对象模型中属性,方法和事件的分布。 可以对Office对象模型进行几个一般性的说明,如图1-3所示。 Excel对象模型是Office对象模型中最大的属性,方法和事件总数,紧随其后的是Word。 Word有很少的事件。 我们也可以说Office对象模型中的属性比方法更多。
属性
属性是简单的方法,允许您读取或写入与对象关联的特定命名值。 例如,Word的Application对象有一个名为CapsLock的属性,它返回一个bool值。 如果Caps Lock键为关,则返回true; 如果Caps Lock键已经启动,它将返回false。 清单1-4显示了一些检查此属性的代码。 该代码假定Word对象模型的根应用程序对象已经被分配给一个名为app的变量。
清单1-4 Word应用程序锁定大写CapsLock键,返回对象属性的bool值
if (app.CapsLock == true) { MessageBox.Show("CapsLock key is down"); } else { MessageBox.Show("CapsLock key is up"); }
关于CapsLock属性的另一件事是它是一个只读属性。也就是说,您无法编写将CapsLock属性设置为false的代码;您只能获取CapsLock属性的值。在Office对象模型中,许多属性都是只读的。如果您尝试将只读属性设置为某个值,则在编译代码时会发生错误。
CapsLock属性返回一个bool值。属性也可以返回枚举。清单1-5显示了一些使用WindowState属性来确定Word的窗口是最大化还是最小化或正常的代码。该代码使用C#的switch语句来评估WindowState属性,并将其值与三个可能的枚举值常量进行比较。请注意,当您在C#中指定枚举值时,必须同时指定枚举类型名称和枚举值,例如,如果您刚刚使用wdWindowStateNormal而不是WdWindowState.wdWindowStateNormal,则代码将无法编译。
清单1-5 返回枚举Word应用程序对象上WindowState属性的属性
switch (app.WindowState) { case Word.WdWindowState.wdWindowStateNormal: MessageBox.Show("Normal"); break; case Word.WdWindowState.wdWindowStateMaximize: MessageBox.Show("Maximized"); break; case Word.WdWindowState.wdWindowStateMinimize: MessageBox.Show("Minimized"); break; default: break; }
属性也可以返回其他对象模型对象。 例如,Word的Application对象具有一个名为ActiveDocument的属性,该属性返回用户正在编辑的当前活动文档。 ActiveDocument属性返回名为Document的Word对象模型中的另一个对象。 文件又具有属性,方法和事件。 清单1-6显示了检查ActiveDocument属性的代码,然后显示Document对象的Name属性。
清单1-6 返回另一个对象模型的属性对象在Word应用程序对象上的ActiveDocument属性
Word.Document myDocument = app.ActiveDocument;
MessageBox.Show(myDocument.Name);
如果没有活动文档,如果Word正在运行但没有打开任何文档,会发生什么? 在ActiveDocument属性的情况下,它会引发异常。 因此,上述代码的更安全的版本将捕获异常并报告没有找到活动文档。 清单1-7显示了这个更安全的版本。 一个更好的方法是检查Application对象的Documents集合的Count属性,以查看在访问ActiveDocument属性之前是否打开任何文档。
清单1-7 一个可能会在Word的应用程序对象上抛出异常ActiveDocument属性的属性
Word.Document myDocument = null; try { myDocument = app.ActiveDocument; MessageBox.Show(myDocument.Name); } catch (Exception ex) { MessageBox.Show( String.Format("No active document: {0}", ex.Message)); }
在您遇到的对象不可用或在特定上下文中无意义的错误情况下,对象模型有时会表现不同。 该属性可以返回一个空值。 确定对象模型属性是否将抛出异常或返回空值的方法是通过查询有关属性的对象模型文档。 Excel的Application对象使用此模式作为其ActiveWorkbook属性。 如果没有Excel工作簿打开,它将返回null而不是抛出异常。 清单1-8显示了如何编写处理这种行为模式的代码。
清单1-8 一个可能返回null的ActiveWorkbook属性在Excel的应用程序对象的属性
Excel.Workbook myWorkbook = app.ActiveWorkbook; if (myWorkbook == null) { MessageBox.Show("No active workbook"); } else { MessageBox.Show(myWorkbook.Name); )
参数化属性
目前检查的属性是无参数的。但是,一些属性需要参数。例如,Word的Application对象有一个名为FileDialog的属性返回FileDialog对象。 FileDialog属性使用类型为MsoFileDialogType的枚举参数,该参数用于选择返回哪个FileDialog。其可能的值是msoFileDialogOpen,msoFileDialogSaveAs,msoFileDialogFilePicker和msoFileDialogFolderPicker。
C#不支持调用参数化属性作为属性。当您使用C#中的Word对象模型,并在Word的Application对象上查找FileDialog属性时,无处可见。 FileDialog属性可以从C#调用,但只能通过一种方法名为get_FileDialog。因此,当您在C#中查找参数化属性时,请确保查找get_Property方法(其中Property是要访问的属性的名称)。要在C#中设置参数化属性(假定它们不是只读属性),有一个单独的方法称为set_Property(其中Property是要设置的属性的名称)。
使用VSTO时会发现此异常。 VSTO扩展了少量的Word和Excel对象模型对象。这些对象已被扩展,为您提供了一种不同的访问参数化属性的途径。索引器使您能够以与访问数组的方式相同的方式访问属性,该属性的名称后跟定界符[和]之间的参数列表。因此,对于由VSTO扩展的对象模型对象(如Worksheet),可以使用索引器语法:Range [parameter1,parameter2]而不是get_Range(parameter1,parameter2)来调用参数化属性(如Range),它具有两个参数。
清单1-9中的代码使用称为方法的FileDialog属性,并将msoFileDialogFilePicker作为参数传递,以指定要返回的FileDialog对象的类型。然后调用返回的FileDialog对象的方法来显示对话框。
清单1-9 一个被称为枚举参数并返回对象模型对象的方法的参数化属性Word的应用程序对象上的FileDialog属性
Office.FileDialog dialog = app.get_FileDialog(Office.MsoFileDialogType.msoFileDialogFilePicker);
dialog.Show();
Office对象模型还具有可选参数的属性。可选参数是可以省略的参数,Office应用程序将填充参数的默认值。可选参数通常是类型对象,因为可选参数传递到底层COM API。在C#中,如果不想指定参数,则必须将特殊值传递给类型为object的可选参数。这个特殊的值被称为Type.Missing,它必须被传递给你不想直接指定的可选参数(不同于可以完全省略参数的Visual Basic)。在VSTO项目中,一个“丢失”变量是为您预先声明的(也就是设置为Type.Missing)。因此,在VSTO代码中,您经常会看到缺少的传递而不是Type.Missing。
偶尔会发现一个可选参数是一些枚举类型,而不是类型对象。对于这种可选参数,您不能传递Type.Missing,而必须通过特定的枚举类型值。您可以通过查看该方法的文档或在Visual Basic项目中使用对象浏览器来找出可选参数的默认枚举类型值,但C#对象浏览器不显示可选枚举类型参数的默认值。
清单1-10显示了调用名为Range的参数化属性的示例,它在Excel的Application对象中找到。通过get_Range方法访问Range属性,因为参数化属性只能通过C#中的方法调用。在Excel的Application对象上调用get_Range方法返回活动工作簿中的Range对象,由传递给该方法的参数指定。 get_Range方法有两个参数。第一个参数是必需的,第二个参数是可选的。如果要指定单个单元格,则只传递第一个参数。如果要指定多个单元格,则必须在第二个参数的第一个参数和右下角的单元格中指定左上角的单元格。
清单1-10 参数化属性被称为可选参数的方法Excel应用程序对象上的范围属性
//使用缺少的可选参数调用参数化属性 Excel.Range myRange = app.get_Range(“A1”,Type.Missing); //调用参数化属性而不丢失参数 Excel.Range myRange2 = app.get_Range(“A1”,“B2”);
在Word中,可选参数的处理方式与其他Office应用程序不同。 Word的对象模型要求可选参数通过引用传递。这意味着您不能直接按照清单1-10中的代码传递Type.Missing。相反,您必须声明一个变量,将其设置为Type.Missing,并通过引用传递该变量。如果参数有多个要忽略的参数,则可以重用已设置为Type.Missing的同一声明变量。在VSTO项目中,您可以通过引用传递为您预先声明的缺失变量。列表1-11显示了如何在Word中指定可选参数。在此示例中,代码使用名为SynonymInfo的Word的Application对象的参数化属性,该对象具有必需的字符串参数,用于指定要同义词的单词和可选参数以指定要使用的语言ID。通过get_SynonymInfo方法访问SynonymInfo属性,因为参数化属性只能通过C#中的方法调用。通过省略可选的语言ID参数,并将引用的变量设置为Type.Missing,Word将默认使用您安装的当前语言。
清单1-11 参数化属性被称为具有可选参数的方法通过参考在Word的应用程序对象上的同义词信息属性
object missing = Type.Missing; // Calling a parameterized property in Word // with a missing optional parameter Word.SynonymInfo synonym = app.get_SynonymInfo( "happy", ref missing);
大多数对象共有的属性
因为所有对象模型对象都有对象作为其基类,所以您将始终在每个对象模型对象上找到GetType,GetHashCode,Equals和ToString方法。您还将经常找到一个名为Application的属性,该属性将返回与对象关联的Application对象。这是提供一个快速的方式来回到对象模型的根。许多对象都有一个名为Creator的属性,它提供一个代码,指示对象被创建的应用程序。最后,您将经常找到一个返回对象模型层次结构中父对象的父属性。
方法
一种方法通常比属性更复杂,并且表示对象上的“动词”,导致某些事情发生。它可能有也可能没有返回值,并且更可能具有属性的参数。
方法的最简单形式没有返回类型,没有参数。列表1-12显示了使用Word的Application对象中的Activate方法。此方法激活Word应用程序,使其窗口成为活动窗口(相当于单击任务栏中的Word窗口以激活它)。
清单1-12 一种没有参数的方法,并且没有返回类型在Word的应用程序对象上的激活方法
MessageBox.Show("Activating the Word window."); app.Activate();
方法也可能具有参数,无返回类型。清单1-13显示了这种方法的一个例子。 ChangeFileOpenDirectory方法接受一个字符串,当显示“打开”对话框时,该字符串是要将Word默认为的目录的名称。对于一个简单的方法,您可能会想知道为什么不使用属性,例如,您可以想象Word具有FileOpenDirectory属性。在这种情况下,ChangeFileOpenDirectory只会在当前Word会话的生命周期内临时更改默认打开目录。当您退出Word然后重新启动Word时,默认将不再是您使用此方法设置的。也许是因为这个原因,这个功能是通过一个方法而不是一个属性暴露的。对象模型有时使用诸如此类而不是属性的简单方法的第二个原因是因为在对象模型中暴露的某些值是“只写”;也就是说,它们只能被设置但不能被读取。创建只读属性是常见的,但创建只写属性不常见。因此,当需要只写属性时,通常使用简单的方法。
清单1-13 具有参数且无返回类型的方法,用于Word应用程序对象上的ChangeFileOpenDirectory方法
app.ChangeFileOpenDirectory(@"c:\temp"); MessageBox.Show("Will open out of temp for this session.");
方法可以没有参数和返回类型。 清单1-14显示了这种方法的一个例子。 DefaultWebOptions方法返回DefaultWebOptions对象,然后用于设置Word的Web功能的选项。 在这种情况下,DefaultWebOptions实际上应该被实现为只读属性而不是方法。
清单1-14 一种无参数和返回类型的Word的应用程序对象的DefaultWebOptions方法
Word.DefaultWebOptions options = app.DefaultWebOptions(); MessageBox.Show(String.Format("Pixels per inch is {0}.", options.PixelsPerInch));
方法可以有参数和返回类型。 清单1-15显示了这种方法的一个例子。 CentimetersToPoints方法需要一厘米的值,并将其转换为点,返回值为方法的返回值。 在文档中指定间距时,点是Word经常使用的单位。
清单1-15 具有参数和返回类型的方法在Word的应用程序对象上使用CentimetersToPoints方法
float centimeters = 15.0; float points = app.CentimetersToPoints(centimeters); MessageBox.Show(String.Format("{0} centimeters is {1} points.", centimeters, points));
方法也可以有可选参数。可选参数不需要直接指定来调用该方法。对于您不想指定的任何参数,您传递一个由.NET定义的名为Type.Missing的特殊值。清单1-16显示了一个名为CheckSpelling的方法,它具有可选参数。清单1-16说明了用于省略不想指定的参数的语法。 CheckSpelling方法需要一个字符串,您想要使用两个可选参数来检查along的拼写。第一个可选参数使您可以选择自定义字典来检查拼写。第二个可选参数使您能够告诉拼写检查器忽略所有大写字母中的单词作为首字母缩略词。在清单1-16中,我们检查一个短语,而不指定任何可选参数,通过将Type.Missing传递给每个可选参数。我们还检查一个具有所有大写字母的缩写的第二个短语,所以我们将Type.Missing传递给第一个可选参数,因为我们不想使用自定义字典,但是我们将第二个可选参数指定为true,以便拼写检查器忽略所有大写字母。
清单1-16 一种可选参数的方法和Excel应用对象上的CheckSpelling方法的返回类型
string phrase1 = "Thes is spelled correctly. "; string phrase2 = "This is spelled correctly AFAIK. "; bool isCorrect1 = app.CheckSpelling(phrase1, Type.Missing, Type.Missing); bool isCorrect2 = app.CheckSpelling(phrase2, Type.Missing, true);
Word中的可选参数
Word中的可选参数可以产生一些奇怪的C#代码,因为传递给可选参数的值必须通过引用传递。 例如,清单1-17显示了如何使用C#中的Word对象模型拼写检查字符串。
清单1-17 通过在Word的应用程序对象上引用CheckSpelling方法传递可选参数的方法
void SpellCheckString() { string phrase1 = "Speling erors here."; object ignoreUpperCase = true; object missing = Type.Missing; bool spellingError = app.CheckSpelling(phrase1, ref missing, ref ignoreUpperCase, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing); if (spellingError) MessageBox.Show("Spelling error found"); else MessageBox.Show("No errors"); }
如果您是Visual Basic程序员,并且您从未看过在C#中针对Word编写的代码,首先要注意的是“为什么这么冗长? Visual Basic在方法中有可选参数时,会为您做一些特殊的操作,因此Visual Basic版本更简单,如清单1-18所示。
清单1-18 使用Visual Basic通过引用传递可选参数的方法Word应用程序对象上的CheckSpelling方法
Public Sub SpellCheckString() Dim phrase1 As String = "Speling erors here." Dim spellingError As Boolean spellingError = app.CheckSpelling(myString, , True) If spellingError Then MsgBox("Spelling error found.") Else MsgBox("No error found.") End If End Sub
在Visual Basic中,您不必担心为每个可选参数传递一个值,这样语言可以为您处理。您甚至可以使用逗号,如清单1-18所示,省略一个您不想指定的特定变量。在这种情况下,我们不想指定自定义字典,但是我们确实想要传递参数IgnoreUpperCase的值,所以我们省略了自定义字典参数,只需将其留在逗号之间。
如果你是一个C#程序员,你可能会想到第一件事,你从来没有看过C#中针对Word编写的代码,如清单1-17所示的代码,是“为什么所有这些东西都通过引用传递”?当您正在与Office对象模型方法,属性和事件通话时,您通过称为COM互操作性的.NET技术(互操作性简称)与对象模型通信。 Office对象模型都以非托管代码(C和C ++)编写,通过COM接口公开。您将在本章后面阅读有关允许托管代码调用COM对象的称为互操作程序集的技术的更多详细信息。
如果要检查由Word的COM类型库定义的清单1-17中使用的CheckSpelling方法的COM定义,您将看到如下:
HRESULT CheckSpelling( [in] BSTR Word, [in, optional] VARIANT* CustomDictionary, [in, optional] VARIANT* IgnoreUppercase, [in, optional] VARIANT* MainDictionary, [in, optional] VARIANT* CustomDictionary2, [in, optional] VARIANT* CustomDictionary3, [in, optional] VARIANT* CustomDictionary4, [in, optional] VARIANT* CustomDictionary5, [in, optional] VARIANT* CustomDictionary6, [in, optional] VARIANT* CustomDictionary7, [in, optional] VARIANT* CustomDictionary8, [in, optional] VARIANT* CustomDictionary9, [in, optional] VARIANT* CustomDictionary10, [out, retval] VARIANT_BOOL* prop);
请注意,标记为可选的任何参数都指定为指向Word(VARIANT *)中的VARIANT的指针。 VARIANT是COM类型,大致相当于.NETit中的对象可以包含许多不同类型的值。 Excel通常不会使用指向VARIANT的可选参数的指针,因此,对于大多数Excel,您没有通过引用问题。 当PIA生成时,C#IntelliSense如下所示:
bool _Application.CheckSpelling(string Word, ref object CustomDictionary, ref object IgnoreUppercase, ref object MainDictionary, ref object CustomDictionary2, ref object CustomDictionary3, ref object CustomDictionary4, ref object CustomDictionary5, ref object CustomDictionary6, ref object CustomDictionary7, ref object CustomDictionary8, ref object CustomDictionary9, ref object CustomDictionary10)
由于Word在COM对象(作为指向VARIANT的指针)中定义了可选参数,并且由于如何转换为.NET代码(通过引用传递的对象),Word中的任何可选参数都必须通过C#和必须被声明为一个对象。即使您想在CheckSpelling示例中强制将IgnoreUppercase参数键入bool,您也不能。您必须将其作为对象键入,否则将收到编译错误。这最终会有点混乱,因为你可以强烈地键入你想要检查的字符串的第一个参数。这是因为在CheckSpelling方法中,Word参数(拼写检查的字符串)不是CheckSpelling的可选参数。因此,它是强类型的,不能通过引用传递。另请注意,可选参数始终列在所有必需的参数之后,您将永远不会找到一个情况,其中argument1是可选的,而且argument2不是。
这使我们回到Type.Missing。在C#中省略可选参数,通过引用设置为Type.Missing传递对象。在我们的例子中,我们刚刚声明了一个名为missing的变量,并将其传递了11次。
当您通过引用传递对象来管理大多数托管函数时,您可以这样做,因为托管函数会告诉您可能会更改传递到函数中的对象的值。所以对你来说看起来可能不好,我们声明一个变量,并将它传递给我们不关心的CheckSpelling的所有参数。毕竟,假设你有一个功能,通过参考获取两个参数。如果将相同的变量设置为Type.Missing到两个参数,如果评估第一个参数的代码将其从Type.Missing更改为某个其他值(如bool值true),该怎么办?这也会影响第一个参数和第二个参数,并且当它查看最初设置为Type.Missing的第二个参数时,函数可能会做不同的事情,因为它现在也被设置为true。
为了避免这种情况,您可能会认为我们必须声明一个missing1 tHRough missing11变量,因为Word可能会改变您的其中一个参数,从而使它不再传递Type.Missing,但有些东西否则可能会导致意外的副作用。
幸运的是,在使用Office对象模型时,不需要这样做。请记住,底层的Word应用程序对象是一个非托管对象,您正在通过COM interop与其通信。 COM互操作层意识到您正在将一个Type.Missing传递给COM对象的可选参数。所以interop强制,而不是以某种方式传递对您的缺失变量的引用,interop层传递一个特殊的COM值,表示该参数丢失。您通过引用传递的缺失变量是安全的,因为它从未真正直接传递到Word中。即使当您查看调用的语法时,Word也可能会混淆您的变量,因为它可能是因为它被引用传递。
因此,清单1-17中的CheckSpelling代码是完全正确的。即使通过引用传递它,您的缺失变量也不会被Word更改。但是请记住,这是一种特殊情况,只适用于通过COM interop与具有可选参数的非托管对象模型进行交谈时。调用Office对象模型之外的对象需要通过引用传递参数的方法时,不要让这种特殊情况让您变得模糊。当与非Office对象模型方法进行通信时,必须通过引用传递参数时要小心,因为managed方法可以更改您通过的变量。
事件event
您现在已经详细介绍了使用属性和方法,这些都是您的代码控制Office应用程序的方式。事件是Office应用程序与您的代码进行交谈的方式,并使您能够运行其他代码以响应Office应用程序中发生的某些情况。
在Office对象模型中,事件数量远少于方法和属性,例如Word中有36个事件,Excel中有36个事件。这些事件中的一些重复在不同的对象上。例如,当用户打开Word文档时,Application对象和新创建的Document对象均会引发Open事件。如果您想处理所有文档上的所有Open事件,您将处理Application对象上的Open事件。如果您具有与特定文档相关联的代码,则可以处理相应Document对象上的Open事件。
在大多数Office对象模型中,事件由少量对象引起。在Word对象模型中引发事件的唯一对象是Application,Document和OLEControl。在Excel对象模型中引发事件的唯一对象是应用程序,工作簿,工作表,图表,OLEObject和QueryTable。 Outlook有一个例外:Outlook对象模型中约一半的对象引发事件。但是,大多数这些对象都会引发相同的事件集,使Outlook中唯一事件的总数小。
表1-3显示了Excel的Application对象引发的所有事件。该表几乎表示Excel引发的所有事件,因为Sheet的前面的事件在Excel的Worksheet对象上重复,并且由Workbook开头的事件在Excel的Workbook对象上重复。这些重复事件的唯一区别是应用程序级工作表和工作簿事件传递Sheet或Workbook类型的参数,以指示哪个工作表或工作簿引发事件。由Workbook对象或Sheet对象引发的事件不必传递Sheet或Workbook参数,因为它是从您处理事件的Workbook或Sheet对象中隐式确定的。
要处理由Office对象模型引发的事件,您必须首先在代码中声明与正在引发的事件预期的签名相匹配的回调方法。 例如,Excel中的Application对象上的Open事件期望回调方法与此委托的签名相匹配:
public delegate void AppEvents_WorkbookOpenEventHandler(Workbook wb);
要处理此事件,您必须声明一个与预期签名相匹配的回调方法。 请注意,我们在回调方法中省略了上述签名中的delegate关键字,因为我们没有定义新的委托类型,只需实现Office对象模型定义的现有代理类型。
public void MyOpenHandler(Excel.Workbook wb) { MessageBox.Show(wb.Name + " was opened. "); }
最后,您必须将回调方法连接到引发此事件的Excel Application对象。 我们创建由称为AppEvents_WorkbookOpenEventHandler的Excel对象模型定义的委托对象的新实例。 我们传递给这个对象的构造函数我们的回调方法。 然后,使用+ =运算符将此委托对象添加到Excel Application WorkbookOpen事件。
app.WorkbookOpen += new AppEvents_WorkbookOpenEventHandler(MyOpenHandler);
虽然这看起来很复杂,但Visual Studio 2005有助于自动生成大部分代码行以及相应的事件处理程序。如果您键入这行代码,键入+ =后,Visual Studio将显示一个弹出工具提示。如果您按Tab键两次,Visual Studio会自动生成其余的代码行和回调方法。
清单1-19显示了一个简单类中回调方法和事件连接的完整实现。回调方法称为MyOpenHandler,是SampleListener类的成员方法。此代码假定客户端创建此类的实例,将Excel Application对象传递给该类的构造函数。 ConnectEvents方法将回调方法MyOpenHandler连接到Excel Application对象的WorkbookOpen事件。 DisconnectEvents方法通过使用委托对象上的 - =操作符从Excel Application对象的WorkbookOpen事件中删除回调方法MyOpenHandler。在删除它时,我们创建一个新的委托对象实例可能会很奇怪,但这是C#支持删除委托的方式。
此代码的结果是,任何时候打开工作簿并调用ConnectEvents,MyOpenHandler将处理Excel的Application对象引发的WorkbookOpen事件,它将显示一个消息框,其中打开了工作簿的名称。可以调用DisconnectEvents来阻止MyOpenHandler处理由Excel的Application对象引发的WorkbookOpen事件。
清单1-19 监听Excel应用程序对象的WorkbookOpen事件的类
using Excel = Microsoft.Office.Interop.Excel; class SampleListener { private Excel.Application app; public SampleListener(Excel.Application application) { app = application; } public void ConnectEvents() { app.WorkbookOpen += new AppEvents_WorkbookOpenEventHandler(this.MyOpenHandler); } public void DisconnectEvents() { app.WorkbookOpen -= new AppEvents_WorkbookOpenEventHandler(this.MyOpenHandler); } public void MyOpenHandler(Excel.Workbook workbook) { MessageBox.Show(String.Format("{0} was opened.", workbook.Name)); } }
“我的按钮停止工作”问题
在.NET中开始针对Office事件编程时常遇到的一个问题称为“我的按钮停止工作”问题。开发人员将编写一些代码来处理Office工具栏对象模型中CommandBarButton引发的Click事件。这段代码有时会暂时工作,然后停止。用户将点击该按钮,但Click事件似乎已停止工作。
这个问题的原因是将事件回调连接到其生命周期与事件的期望生命周期不匹配的对象。这通常发生在连接事件处理程序的对象超出范围或被设置为null以便它被收集垃圾时。清单1-20显示了导致此错误的代码示例。在这种情况下,事件处理程序连接到新创建的名为btn的CommandBarButton。但是,btn被声明为局部变量,所以一旦ConnectEvents函数退出并且垃圾收集发生,btn将被收集到垃圾回收,并且连接到btn的事件不被调用。
这个行为的完整解释与btn与称为运行时可调用包装器(RCW)的内容相关联,这在第24章“使用VSTO创建Outlook加载项”中有描述。不要太深入,btn持有一个RCW,这是事件从非托管Office COM对象传播到托管事件处理程序所必需的。当btn超出范围并且被垃圾回收时,RCW上的引用计数下降,并且RCW通过断开事件连接来处理。
清单1-20 一个无法处理CommandBarButton单击事件的类
using Excel = Microsoft.Office.Interop.Excel; using Office = Microsoft.Office.Core; class SampleListener { private Excel.Application app; public SampleListener(Excel.Application application) { app = application; } // This appears to connect to the Click event but // will fail because btn is not put in a more permanent // variable. public void ConnectEvents() { Office.CommandBar bar = Application.CommandBars["Standard"]; Office.CommandBarButton myBtn = bar.Controls.Add(1, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing) as Office.CommandBarButton; if (myBtn!= null) { myBtn.Caption = "My Button"; myBtn.Tag = "SampleListener.btn"; myBtn.Click += new Office. _CommandBarButtonEvents_ClickEventHandler( myBtn_Click); } } // The Click event will never reach this handler. public void myBtn_Click(Office.CommandBarButton ctrl, ref bool cancelDefault) { MessageBox.Show("Button was clicked"); } }
列表1-21显示了尝试连接到Outlook的“检查器”对象引发的Outlook的NewInspector事件的失败事件侦听器类的第二个示例。 每当检查器窗口打开(您正在查看或编辑Outlook项目的窗口)时,会引发此事件。 此代码也将无法获取任何事件。 在这种情况下,由于事件处理程序连接到以App.Inspectors开头的代码行临时创建的检查器对象,因此更为微妙。 因为app.Inspectors返回的检查器对象不存储在永久变量中,所以暂时创建的检查器对象是垃圾回收的,连接到它的事件永远不会被调用。
清单1-21 一个无法处理Outlook检查器对象的NewInspector事件的类
using Outlook = Microsoft.Office.Interop.Outlook; class SampleListener { private Outlook.Application app; public SampleListener(Outlook.Application application) { app = application; } // This will appear to connect to the NewInspector event, but // will fail because Inspectors is not put in a more permanent // variable. public void ConnectEvents() { app.Inspectors.NewInspector += new Outlook. InspectorsEvents_NewInspectorEventHandler( MyNewInspectorHandler); } // The NewInspector event will never reach this handler. public void MyNewInspectorHandler(Outlook.Inspector inspector) { MessageBox.Show("New inspector."); } }
此问题的解决方案是声明一个变量,其生命周期与事件处理程序的生命周期相匹配,并将其设置为要处理该事件的Office对象。 列表1-22显示了一个成功侦听CommandBarButton Click事件的重写类。 这个类工作,因为不使用方法范围的变量btn,它使用一个名为myBtn的类作用域成员变量。 这样可以确保在ConnectEvents被调用时,事件处理程序将连接到该类的生命周期。
清单1-22 一个类成功处理CommandBarButton单击事件,因为它将CommandBarButton对象存储在一个类成员变量中
using Excel = Microsoft.Office.Interop.Excel; using Office = Microsoft.Office.Core; class SampleListener { private Excel.Application app; private Office.CommandBarButton myBtn; public SampleListener(Excel.Application application) { app = application; } public void ConnectEvents() { Office.CommandBar bar = Application.CommandBars["Standard"]; myBtn = bar.Controls.Add(1, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing) as Office.CommandBarButton; if (myBtn != null) { myBtn.Caption = "My Button"; myBtn.Tag = "SampleListener.btn"; myBtn.Click += new Office. _CommandBarButtonEvents_ClickEventHandler( myBtn_Click); } } public void myBtn_Click(Microsoft.Office.Core.CommandBarButton ctrl, ref bool cancelDefault) { MessageBox.Show("Button was clicked"); } }
列表1-23显示了我们失败的Outlook示例的类似修复。 这里我们声明一个我们分配给app.Inspectors的类级别的变量myInspectors。 这确保在调用ConnectEvents时,我们的事件处理程序将连接到该类的生命周期,因为myInspectors的生命周期现在与该类的生命周期匹配。
清单1-23 一个类成功处理Outlook检查器对象的NewInspector事件,因为它将检查器对象存储在一个类成员变量中
using Outlook = Microsoft.Office.Interop.Outlook; class SampleListener { private Outlook.Application app; private Outlook.Inspectors myInspectors; public SampleListener(Outlook.Application application) { app = application; } public void ConnectEvents() { this.myInspectors = myAplication.Inspectors; myInspectors.NewInspector += new Outlook. InspectorsEvents_NewInspectorEventHandler( MyNewInspectorHandler); } public void MyNewInspectorHandler(Outlook.Inspector inspector) { MessageBox.Show("New inspector."); } }
方法名称和事件名称碰撞时
在Office对象模型中的一些情况下,一个对象有一个事件和一个具有相同名称的方法。例如,Excel的Workbook对象具有Activate事件和Activate方法。 Outlook的Inspector和Explorer对象具有Close事件和Close方法。
当使用具有诸如Workbook等事件的Office对象模型对象时,实际上正在使用实现多个接口的对象。其中一个接口具有Close方法的定义,单独的接口具有Close事件的定义。要处理方法名称冲突的事件,必须将对象转换到包含事件定义的接口。包含事件接口的接口名为ObjectNameEvents_Event,其中ObjectName是对象的名称,例如Workbook或Inspector。
列表1-24在添加事件处理程序时将Workbook对象myWorkbook转换为Excel.WorkbookEvents_Event。通过将myWorkbook转换为WorkbookEvents_Event接口,我们消除了Close方法(在Workbook界面上)和Close事件(位于WorkbookEvents_Event接口上)之间的歧义。
清单1-24 将通过Casting to WorkbookEvents_Event监听Excel工作簿对象的激活事件的类
using Excel = Microsoft.Office.Interop.Excel; class SampleListener { private Excel.Workbook myWorkbook; public SampleListener(Excel.Workbook workbook) { myWorkbook = workbook; } public void ConnectEvents() { ((Excel.WorkbookEvents_Event)myWorkbook).Activate += new Excel.WorkbookEvents_ActivateEventHandler(Activate) } public void Activate() { MessageBox.Show("Workbook Activated"); } }