上一讲我们完成对Word的介绍,文本开始,我将着重介绍Outlook。Outlook是微软Office中一个非常实用的工具,尤其在一个拥有Windows Domain的公司局域网中,Outlook是员工最常用的通讯工具,所以对Outlook实行进一步的定制开发的需求量是很大的。本文中,我先以联系人的扩展为开始,讲解如何开发一个强大的Outlook插件。

 

故事的开始

首先我们假设一个场景。有一天,市场部的同事来找你帮个小忙(有经验的人都知道,一般这都是无尽痛苦的开始失望)。他们希望在Outlook的联系人信息上加入对方父母生日信息,方便他们做市场营销(嘿嘿,怎么营销大家自己想咯眨眼)。同时,最好能够和Outlook原来的联系人界面保持一致便于查阅。为了不辜负同事的期望,你打开了Visual Studio……

简单思索过后,你在脑海中浮现了一下两个功能点:

  1. Outlook Form Region:用于制作界面,可以和原本的Outlook界面整合在一起。
  2. Contact User Define Fields:用于保存新加入的属性。

而新的联系人界面,你打算画成这样:

01 Contact Panel with Form Region

 

业务逻辑:每次Contact界面展示的时候,我们制作的Form Region会读联系人中用户自定义属性里面的值(我们分别为父母的名字和生日创建4个用户属性),并展示在界面上。当Contact关闭的时候,如果用户改变了这四个控件的值,我们将其写回到自定义属性中去。关于用户自定义的属性,如下图:

08 Customer Defined fields

 

Outlook Object Model介绍

正式开始之前,我们先了解一下Outlook的对象模型。

Microsoft.Office.Interop.Outlook.Application

Outlook中的Application对象就像我在之前在Word中介绍过的Application对象一样,是所有对象的最顶层。即便你同时开多个Outlook,你也只创建了一个Application(进程也只有一个)。Application对象中,我们需要重点关注三个地方:

  1. CreateItem方法:通过这个方法,我们创建新的Outlook对象,例如Email、Task、Contact等。
  2. Explorers属性: 获得当前的Outlook.Explorer对象集合。
  3. Inspectors属性: 获得当前的Outlook.Inspector对象集合。

有很多方法可以获得Application对象,比较常用的是直接在Addin工程中,通过Globals.ThisAddIn.Application获得。如果你看过我之前写的Word插件,你会发现这里是和Word VSTO,其实这是微软为我们设定的模式。包括在之后的Excel和PowerPoint中也都是这样的。我这里进一步介绍一下Globals,它是一个在ThisAddIn.Designer.cs中定义的一个类,对于Outlook来说,它定义了三个静态属性,ThisAddin、Ribbons和Inspectors对象。Globals作用是,通过它我们可以在Addin项目的任何位置获得我们所需要的上下文。

 

Microsoft.Office.Interop.Outlook.Explorer

Explorer类,即当前Outlook的主窗口。示显示包含项(如电子邮件、任务或约会)的文件夹内容的窗口。Explorer 类包括可用来修改窗口的方法和属性,以及窗口更改时所引发的事件。需要注意的是,Outlook是可以开多个Explorer,但是只有一个Application。这点和Word类似,而和我们以后会讲到的Excel不同。一般我们通过Globals.ThisAddIn.Application.ActiveExplorer()方法来获得当前(焦点所在)的Explorer。

 

Microsoft.Office.Interop.Outlook.Inspector

Inspector类,即一个Outlook Item的窗口,比如你写邮件时弹出的窗口就是一个Inspector,你创建一个新的联系人也是一个Inspector。Inspector类是在开发Outlook插件中很常用的。尤其是以下几个地方:

  1. 获得当前的Inspector:Globals.ThisAddIn.Application.ActiveInspector(),很多时候我们都会通过这个方法获得现在的Inspector,但是这个不一定靠的住,我在今天的例子中就会讲到一种特殊情况。
  2. 新Inspector创建事件:Globals.ThisAddIn.Application.Inspectors.NewInspector,如果你需要为每个Email加一个Task Pane,就需要使用到这个事件。
  3. 获得Inspector对应的Outlook对象:Inspector.CurrentItem,Inspector只是一个窗口,而这个窗口背后的Outlook对象,则需要通过这个方法取得。

 

Microsoft.Office.Interop.Outlook.MAPIFolder

MAPIFolder即Outlook中的目录,Outlook中提供了16种内建的目录类型,由枚举Microsoft.Office.Interop.Outlook.OlDefaultFolders定义。

 

Microsoft.Office.Interop.Outlook.MailItem

Microsoft.Office.Interop.Outlook.AppointmentItem

Microsoft.Office.Interop.Outlook.TaskItem

Microsoft.Office.Interop.Outlook.ContactItem

分别对应Outlook中的Email、MeetingRequest、Task和Contact,它们都可以通过Application对象的CreateItem方法来创建。

 

Outlook Form Region

介绍

为了更好地制作界面,我们使用的是Outlook Form Region。它是从Outlook 2007开始,微软进入一项新的技术。它提供一种更加方便的方式扩展Outlook项目的界面。相对之前的Custom Form,Form Region是基于.net,开发更加容易,而且和Outlook本身,结合得更加紧密。

 

创建Form Region

首先,我们添加一个新的Item,选择Office项目类型中的Outlook Form Region

02 Add Outlook Regin Form

 

我们选择设计一个新的Form Region

03 New Form Region

 

Form Region的类型为Adjoining,即增加在页面的底部。

04 Form Region Type Adjoning

 

如果你需要在Region Form上添加比较多的控件,你也可以选择Separate,这样你就会有一个完整的空Form。它成为一个独立的标签页,不合原始的Form冲突。

04 Form Region Type Separate

 

为Form Region 起名字,并且设定在编辑模式和阅读模式中都显示我们的Region Form。

05 Form Region Type Display Mode

 

我们的Form Region是嵌入在Contact中的。

06 Form Region For Contact

 

完成了这一系列的配置之后,VS会帮我们创建一个空的Form Region,如果需要修改刚才的设置,可以它在属性中修改,如下图。

07 Change the setting

 

我们开始往这个Form上添加控件,一共两个textbox、两个datetimepicker:

09 Form Region Design

 

业务逻辑实现

流程图

这是根据我们的业务逻辑代码制作的流程图。

流程图

 

源代码分析

成员变量

 1:  // 自定义的属性名字
 2:  private const string PROPERTY_NAME_MOTHER_NAME = "PROPERTY NAME MOTHER NAME";
 3:  private const string PROPERTY_NAME_MOTHER_BIRTHDAY = "PROPERTY NAME MOTHER BIRTHDAY";
 4:  private const string PROPERTY_NAME_FATHER_NAME = "PROPERTY NAME FATHER NAME";
 5:  private const string PROPERTY_NAME_FATHER_BIRTHDAY = "PROPERTY NAME FATHER BIRTHDAY";
 6:  
 7:  // 自定义属性对象
 8:  private Outlook.UserProperty _MotherNameProperty = null;
 9:  private Outlook.UserProperty _MotherBirthdayProperty = null;
 10:  private Outlook.UserProperty _FatherNameProperty = null;
 11:  private Outlook.UserProperty _FatherBirthdayProperty = null;
 12:  
 13:  // 对应的Contact对象
 14:  public Outlook.ContactItem _Contact = null;
 15:  
 16:  // 标记是否内容修改
 17:  private bool _Changed = false;

 

FormRegionShowing事件,这里我们从Contact的用户自定义属性中取得值,并赋值给控件。如果这些属性不存在,我们则创建它们。同时我们所有的控件都绑定了Changed事件,便于我们判断值是否改变。这里有一个需要注意的地方,这里我需要获得Contact对象才能读取Property,但是如果你的Form Region是Adjoining模式的,在FormRegionShowing事件中,你会发现你通过Globals.ThisAddIn.Application.ActiveInspector()是不能取到Inspector的,进一步你也不能通过Inspector获得CurrentItem。这个问题在微软的论坛上都很少被提及。我这里所采用的方式是((EmployeeFamilyForm)sender).OutlookItem。可以看如下的代码:

 1:  private void EmployeeFamilyForm_FormRegionShowing(object sender, System.EventArgs e)
 2:  {
 3:  // 获得FormRegion所对应的Contact对象
 4:  _Contact = ((EmployeeFamilyForm)sender).OutlookItem as Outlook.ContactItem;
 5:  
 6:  // 从联系人的自定义属性中,获得母亲姓名属性
 7:  _MotherNameProperty = _Contact.UserProperties.Find(PROPERTY_NAME_MOTHER_NAME, Type.Missing);
 8:  if (_MotherNameProperty != null)
 9:  {
 10:  // 如果存在这个属性,则取出Value为控件赋值
 11:  tbMotherName.Text = _MotherNameProperty.Value as String;
 12:  }
 13:  else
 14:  {
 15:  // 不存在则创建这个属性
 16:  _MotherNameProperty = _Contact.UserProperties.Add(PROPERTY_NAME_MOTHER_NAME, Outlook.OlUserPropertyType.olText, Type.Missing, Type.Missing);
 17:  }
 18:  
 19:  // 母亲生日,原理相同
 20:  _MotherBirthdayProperty = _Contact.UserProperties.Find(PROPERTY_NAME_MOTHER_BIRTHDAY, Type.Missing);
 21:  if (_MotherBirthdayProperty != null)
 22:  {
 23:  dtpMotherBirthday.Value = (DateTime)_MotherBirthdayProperty.Value;
 24:  }
 25:  else
 26:  {
 27:  _MotherBirthdayProperty = _Contact.UserProperties.Add(PROPERTY_NAME_MOTHER_BIRTHDAY, Outlook.OlUserPropertyType.olDateTime, Type.Missing, Type.Missing);
 28:  }
 29:  
 30:  // 父亲姓名
 31:  _FatherNameProperty = _Contact.UserProperties.Find(PROPERTY_NAME_FATHER_NAME, Type.Missing);
 32:  if (_FatherNameProperty != null)
 33:  {
 34:  tbFatherName.Text = _FatherNameProperty.Value as String;
 35:  }
 36:  else
 37:  {
 38:  _FatherNameProperty = _Contact.UserProperties.Add(PROPERTY_NAME_FATHER_NAME, Outlook.OlUserPropertyType.olText, Type.Missing, Type.Missing);
 39:  }
 40:  
 41:  // 父亲生日
 42:  _FatherBirthdayProperty = _Contact.UserProperties.Find(PROPERTY_NAME_FATHER_BIRTHDAY, Type.Missing);
 43:  if (_FatherBirthdayProperty != null)
 44:  {
 45:  dtpFatherBirthday.Value = (DateTime)_FatherBirthdayProperty.Value;
 46:  }
 47:  else
 48:  {
 49:  _FatherBirthdayProperty = _Contact.UserProperties.Add(PROPERTY_NAME_FATHER_BIRTHDAY, Outlook.OlUserPropertyType.olDateTime, Type.Missing, Type.Missing);
 50:  }
 51:  
 52:  // 将这四个控件绑定change事件,只有在修改之后,我们才会将值回写到Contact对应的属性中去
 53:  tbMotherName.TextChanged += new EventHandler(content_Changed);
 54:  dtpMotherBirthday.ValueChanged += new EventHandler(content_Changed);
 55:  tbFatherName.TextChanged += new EventHandler(content_Changed);
 56:  dtpFatherBirthday.ValueChanged += new EventHandler(content_Changed);
 57:  
 58:  // 在Write事件中,把修改的值保存到属性中去
 59:  _Contact.Write += new Microsoft.Office.Interop.Outlook.ItemEvents_10_WriteEventHandler(contact_Write);
 60:  }

 

content_Changed和contact_Write事件

 1:  void content_Changed(object sender, EventArgs e)
 2:  {
 3:  // 有修改时,将_Change置为true
 4:  _Changed = true;
 5:  }
 6:  
 7:  void contact_Write(ref bool Cancel)
 8:  {
 9:  if (_Changed)
 10:  {
 11:  // 保存值到属性中去
 12:  _MotherNameProperty.Value = tbMotherName.Text.Trim();
 13:  _MotherBirthdayProperty.Value = dtpMotherBirthday.Value;
 14:  _FatherNameProperty.Value = tbFatherName.Text.Trim();
 15:  _FatherBirthdayProperty.Value = dtpFatherBirthday.Value;
 16:  }
 17:  
 18:  }

EmployeeFamilyForm_FormRegionClosed事件

 1:  private void EmployeeFamilyForm_FormRegionClosed(object sender, System.EventArgs e)
 2:  {
 3:  // 关闭事件绑定
 4:  _Contact.Write -= new Microsoft.Office.Interop.Outlook.ItemEvents_10_WriteEventHandler(contact_Write);
 5:  
 6:  // 释放对象
 7:  System.Runtime.InteropServices.Marshal.ReleaseComObject(_Contact);
 8:  _Contact = null;
 9:  }

 

后记

自此我们已经完成了对Outlook联系人的扩展,在本文中我们介绍了Outlook Form Region和UserProperties的使用,基本圆满完成了市场部的需求。话说IT帮他们实现这个需求之后,过了几天市场部的同事又来他们了。因为客户的人数比较多,最好能做一个查询工具希望可以做一个查询工具,来方便他们检索内容。至于如何检索Outlook内部对象的方法,且听下回分解。

 

最后,本文欢迎转载,但请保留出处,大家如果有问题,可以联系我 justin.tyrael@gmail.com或者到VSTO之路小组中提问。本文所涉及的源代码可以在这里下载

posted @ 2011-06-12 20:59 Justin Zhang 阅读(1647) 评论(9) 编辑

上一篇文章中,我介绍了Word的对象模型和一些基本开发技巧。为了更好的介绍Word插件开发,我为本文制作了一个Word书签的增强版,具体功能是让用户在Word中选择一段文本,为它添加书签并其标志为高亮,同时用户可以为这段书签写注释,以后当用户点击这个书签时,我就会显示注释。以下是我录制的视频介绍:

 

这个插件将包括以下几个技术点:

  1. 添加右键菜单
      • 添加右键菜单、控制右键菜单显示
      • WindowBeforeRightClick 事件
      • 删除右键菜单
  2. 修改正文内容、样式
    1. 修改选定的内容
    2. 修改选定的样式
  3. 添加控件
    1. 添加书签
    2. 添加超链接
    3. 添加内容控件(Content Control)
  4. 基于用户选中内容,执行程序
    1. WindowSelectionChange 事件
    2. 根据当前光标的位置,显示悬浮框

以下是我对这些功能点的具体介绍

 

右键菜单

添加右键菜单

右键菜单是Word中相当常用的一个功能,我们在大部分的VSTO开发中也会通过修改这个菜单来扩展Word的功能。最通常地添加右键菜单的方法如下:

   1:      // 添加右键按钮
   2:      Office.CommandBarButton addBtn = (Office.CommandBarButton)Application.CommandBars["Text"].Controls.Add(Office.MsoControlType.msoControlButton, missing, missing, missing, false);
   3:      
   4:      // 开始一个新Group,即在我们添加的Menu前加一条分割线   
   5:      addBtn.BeginGroup = true;
   6:      
   7:      // 为按钮设置Tag
   8:      addBtn.Tag = "BookMarkAddin";
   9:      
  10:      // 添加按钮上的文字
  11:      addBtn.Caption = "Add Bookmark";
  12:      
  13:      // 将按钮初始设为不激活状态
  14:      addBtn.Enabled = false;

显示的效果为

01 右键菜单效果

 

控制右键菜单显示

在很多情况下,我们希望根据用户选择内容来控制右键菜单的显示,那么我们就需要用到WindowBeforeRightClick事件。以下是我在范例中写的代码,只有当用户选择两个以上字符的时候,我才会把我刚才添加的右键菜单激活。请注意代码里面的一些注释,VSTO与Office的COM交互时,并不是很稳定,有很多需要注意的地方。

   1:      void Application_WindowBeforeRightClick(Word.Selection Sel, ref bool Cancel)
   2:      {
   3:          // 根据之前添加的Tag来找到我们添加的右键菜单
   4:          // 注意:我这里没有通过全局变量来控制右键菜单,而是通过findcontrol来取得按钮,因为这里的VSTO和COM对象处理有问题,使用全局变量来控制右键按钮不稳定
   5:          Office.CommandBarButton addBtn = (Office.CommandBarButton)Application.CommandBars.FindControl(Office.MsoControlType.msoControlButton, missing, "BookMarkAddin", false);
   6:          addBtn.Enabled = false;
   7:          addBtn.Click -= new Office._CommandBarButtonEvents_ClickEventHandler(_RightBtn_Click);
   8:      
   9:          if (!string.IsNullOrWhiteSpace(Sel.Range.Text) && Sel.Range.Text.Length > 2)
  10:          {
  11:              addBtn.Enabled = true;
  12:              
  13:              // 这里是另外一个注意点,每次Click事件都需要重新绑定,你需要在之前先取消绑定。
  14:              addBtn.Click += new Office._CommandBarButtonEvents_ClickEventHandler(_RightBtn_Click);
  15:          }
  16:      }

 

删除右键菜单

我建议在Addin启动和关闭时候(ThisAddIn_Startup与ThisAddIn_Shutdown中),每次都清除由我们添加的右键菜单,虽然按照微软的提示,如果在创建的时候把Temporary属性设为true,系统会在程序退出时自动帮你删除,但是根据我的经验,微软这个许诺没有兑现。

   1:      private void RemoveRightBtns()
   2:      {
   3:          Office.CommandBarControls siteBtns = Application.CommandBars.FindControls(Office.MsoControlType.msoControlButton, missing, "BookMarkAddin", false);
   4:          // 这里我写了一个循环,目标是清理所有由我创建的右键按钮,尤其是由于Addin Crash时所遗留的按钮
   5:          if (siteBtns != null)
   6:          {
   7:              foreach (Office.CommandBarControl btn in siteBtns)
   8:              {
   9:                  btn.Delete(true);
  10:              }
  11:          }
  12:      }

 

修改正文内容、样式

修改选定的内容
Word文档内容的修改,主要是通过Range对象来实现的,比较容易。例如,你可以先通过 Application.ActiveDocument.Range(object start ,object end)方法来获得一个你需要的Range,然后通过Range.Text来修改正文的内容,例如:

   1:      Word.Range range = Application.ActiveDocument.Range(0, 10);
   2:      if (range != null)
   3:      {
   4:          range.Text = "Justin";
   5:      }

这里需要指出的是,获得Range的方式很多,你也可以通过用户选择的Selection对象来获得Range,详细内容可以参考我在上一篇随笔中的Word对象模型部分

 

修改选定的样式

修改样式也是通过Range对象来实现的,这里我就写两个范例,一个是修改字体,一个是修改背景色(VSTO中称为高亮色),大家可以在这里进一步扩展出去很多东西。

   1:      // 设置字体
   2:      range.Font.Name = "宋体";
   3:      
   4:      // 添加下划线(点)
   5:      range.Font.Underline = Word.WdUnderline.wdUnderlineDotted;
   6:      
   7:      // 将背景色设为黄
   8:      range.HighlightColorIndex = Word.WdColorIndex.wdYellow;

 

添加控件

Word在正文中提供了非常丰富的控件,例如书签、超链接、注释等,这些控件可以方便用户编辑和阅读文档。所以也是我们开发人员需要注意的一个重点。添加书签(以及其他的控件),在Word中实现的方法很多,本质都是通过获得书签集合(或其他对象的集合),然后通过这个集合的Add方法来添加数据。一般在添加的同时,我们会指定这个控件所对应的Range,即这个空间所包含的范围。因为这种控件对象很多,我这里列举几个范例:

添加书签

对Word中一段文字添加书签,我们需要先去的这段文字的Range,然后通过以下方法来实现。

    // "VSTOBookMark"是书签的名字
    Word.Bookmark mark = _Range.Bookmarks.Add("VSTOBookMark", _Range);


添加超链接

添加超链接和添加书签类似,区别在于超链接需要指定url,且没有名字。

    Word.Hyperlink link = range.Hyperlinks.Add(range, url);


添加内容控件(Content Control)

内容控件是Word 2007开始引如的新功能,是一批独立的控件,用于增强用户体验,这里我介绍如何如在Word文档中添加一个下拉框,这是一段Ribbon的代码,为了方便我讲解,我全部贴出来:

 

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using Microsoft.Office.Tools.Ribbon;
   6:   
   7:  using Office = Microsoft.Office.Core;
   8:  using Word = Microsoft.Office.Interop.Word;
   9:  using ToolsWord = Microsoft.Office.Tools.Word;
  10:   
  11:  namespace OfficeContentControlsDemo
  12:  {
  13:      public partial class Rb
  14:      {
  15:          private void Rb_Load(object sender, RibbonUIEventArgs e)
  16:          {
  17:   
  18:          }
  19:   
  20:          private void button1_Click(object sender, RibbonControlEventArgs e)
  21:          {
  22:              Word.Document currentDocument = Globals.ThisAddIn.Application.ActiveDocument;
  23:   
  24:              if (currentDocument.Paragraphs != null &&
  25:                  currentDocument.Paragraphs.Count != 0)
  26:              {
  27:                  // 在第一段文字前添加一个段落
  28:                  currentDocument.Paragraphs[1].Range.InsertParagraphBefore();
  29:                  currentDocument.Paragraphs[1].Range.Select();
  30:   
  31:                  // 将Interop的Document对象转化为VSTO中的Document对象
  32:                  ToolsWord.Document document = Globals.Factory.GetVstoObject(currentDocument);
  33:   
  34:                  // 添加DropDownList
  35:                  ToolsWord.DropDownListContentControl dropdown = document.Controls.AddDropDownListContentControl(currentDocument.Paragraphs[1].Range, "MyContentControl");
  36:                  dropdown.PlaceholderText = "My DropdownList Test";
  37:                  dropdown.DropDownListEntries.Add("Test01", "01", 1);
  38:                  dropdown.DropDownListEntries.Add("Test02", "02", 2);
  39:                  dropdown.DropDownListEntries.Add("Test03", "03", 3);
  40:              }
  41:          }
  42:      }
  43:  }

运行效果

02 Content Control

这段代码有两个特殊的地方需要注意的:

  1. 首先,可能有人注意到了,我获得第一个段落对象时,使用的是Paragraphs[1]而不是Paragraphs[0],这是因为VSTO中的很多集合的下标不是从0开始的,这可能是延续VB的风格。
  2. 其实,我在这里把Introp的Doucment对象转化为了VSTO的Document对象。我们在前文中已经介绍了过了Introp的Doucment对象,它代表着一个Word文档,即便你刚打开你的Word,是一个空的新文档,也会有一个Document。而Microsoft.Office.Tools.Word下的Document。它们大体相同,区别在于
    1. Controls 属性: 使用此属性可在运行时在 Word 文档中添加托管控件或者移除控件。
    2. VstoSmartTags 属性: 使用此属性可在文档中添加Smart Tag(2010中 Smart Tag基本被废了哭泣的脸)。
    3. InnerObject 属性: 使用此属性获取 Microsoft.Office.Tools.Word.Document 的基础 Microsoft.Office.Interop.Word.Document 对象。
    4. 文档级事件: 仅在 Word 对象模型的应用程序级别提供的文档级事件,例如 BeforeClose 和 BeforeSave。 也就是说,在 Word 对象模型中,这些事件在 Microsoft.Office.Interop.Word.Application 对象上可用。(而不是 Microsoft.Office.Interop.Word.Document 对象)

 

基于用户选中内容,执行程序

在编程中,经常有客户向我提问,能否根据用户选择的内容显示相应的内容,这个功能看似复杂,其实实现起来很简单。

WindowSelectionChange 事件

WindowsSelectionChange事件是这个功能的核心,每次当用户移动光标或者点击Word正文内容时都会触发这个事件。事件参数为Selection,即当前选中的位置。接下来,我们来看一个实际的例子

 
根据当前光标的位置,显示悬浮框

这是如何实现的代码:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Xml.Linq;
   6:  using System.Windows.Forms;
   7:  using System.Drawing;
   8:   
   9:  using Word = Microsoft.Office.Interop.Word;
  10:  using Office = Microsoft.Office.Core;
  11:  using Microsoft.Office.Tools.Word;
  12:   
  13:  namespace BookMarkAddin
  14:  {
  15:      public partial class ThisAddIn
  16:      {
  17:          public FloatingPanel _FloatingPanel = null;
  18:   
  19:          private void ThisAddIn_Startup(object sender, System.EventArgs e)
  20:          {
  21:              this.Application.WindowSelectionChange += new Word.ApplicationEvents4_WindowSelectionChangeEventHandler(Application_WindowSelectionChange);
  22:          }
  23:          
  24:          void Application_WindowSelectionChange(Word.Selection Sel)
  25:          {
  26:              Globals.ThisAddIn._FloatingPanel = new FloatingPanel(bookmark);
  27:   
  28:                          // 当前用户选中的屏幕坐标
  29:              Point currentPos = GetPositionForShowing(Sel);
  30:   
  31:              // 显示悬浮框
  32:              Globals.ThisAddIn._FloatingPanel.Location = currentPos;
  33:              Globals.ThisAddIn._FloatingPanel.Show();
  34:          }
  35:   
  36:          private static Point GetPositionForShowing(Word.Selection Sel)
  37:          {
  38:              // get range postion
  39:              int left = 0;
  40:              int top = 0;
  41:              int width = 0;
  42:              int height = 0;
  43:              Globals.ThisAddIn.Application.ActiveDocument.ActiveWindow.GetPoint(out left, out top, out width, out height, Sel.Range);
  44:   
  45:              Point currentPos = new Point(left, top);
  46:              if (Screen.PrimaryScreen.Bounds.Height - top > 340)
  47:              {
  48:                  currentPos.Y += 20;
  49:              }
  50:              else
  51:              {
  52:                  currentPos.Y -= 320;
  53:              }
  54:              return currentPos;
  55:          }
  56:   
  57:          private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
  58:          {
  59:              try
  60:              {
  61:                  this.Application.WindowSelectionChange -= new Word.ApplicationEvents4_WindowSelectionChangeEventHandler(Application_WindowSelectionChange);
  62:              }
  63:              catch { }
  64:          }
  65:   
  66:          #region VSTO generated code
  67:          // .......
  68:          #endregion
  69:      }
  70:  }

最终的效果可以参考本文开始的视频。这个功能可以推而广之,比如可以把用户当前选中的内容与Task Pane交互,这就看各位的需求了。

总结

至此,我已经介绍了Word插件开发的主要技巧,大家可以下载我制作的插件源代码,里面包含了这些功能。Word作为一个经历了13代的产品,包含的功能非常多,我不能在此全部介绍完毕。如果大家有什么问题,可以到VSTO之路小组中提问,我会和大家继续探讨,同时也方便新人查阅。一下篇文章,我将开始介绍Outlook的开发技巧,希望大家继续支持眨眼

 

最后,本文欢迎转载,但请保留出处,大家如果有问题,可以联系我 justin.tyrael@gmail.com或者到VSTO之路小组中提问。

posted @ 2011-05-26 11:45 Justin Zhang 阅读(2482) 评论(7) 编辑

前一篇文章中,我初步介绍了如何如何开发一个VSTO程序,在本文中,我将进一步深入介绍Word的插件开发。Word是一个大家在日常工作中一直接触的文档工具,也是微软最赚钱的产品之一。从最初的Word 1.0到现在的Word 2010历经了13代的演化,已经成为了一个比较复杂的系统。(这里稍微跑题一下,Office 2010的版本代号是version 14,但是我为什么说Word一共演化了13代呢?因为Office并没有Version 13,上一代的Office 2007是Version 12,微软觉得13这个死数字不吉利,所以直接跳过了……Oh my god眨眼)。言归正传,我录制了一段视频来演示本文所要介绍的内容。

这段视频,描述了一个简单的Word搜索插件,包含了以下几个功能点

  1. 自定义Ribbon
  2. 自定义Task Pane
  3. VSTO插件中获取Word内容全文
  4. 修改Word内容和样式

其中关于如何创建Ribbon和Task Pane的内容,我已经在前一篇文章中介绍了,如果你还不熟悉,可以看这里

 

Word Object Model 介绍

首先,要开发出良好的程序,我们需要了解我们的开发平台,而Word本身是一个很复查的平台,我在这里先从Word的对象模型开始介绍。Word Object Model中一共包含有数百个不同类型的对象,其中最关键的也是最常用的是Application、Document、Range、Selection和Bookmark,他们的关系如下图:

Word Object Model Abstract

我来依次介绍这几个对象:

Application 对象

Application代表Word程序,而一个Word程序内可以包含多个Word文档。用通俗的话来说,无论你开几个Word文档,都是在一个Word进程里面管理。这我们以后会讲到的Excel不一样。同时Application又是所有Word对象根,你可以通过Application对象,获得其他对象。在Addin开发过程中,我们可以通过以下方式来获得Application对象:

    Globals.ThisAddIn.Application
Document 对象

Document对象代表着一个Word文档,即便你刚打开你的Word,是一个空的新文档,也会有一个Document。在开发过程中,以下这个属性从Application中获得当前的Document对象:

    Globals.ThisAddIn.Application.ActiveDocument

此外Application对象也维护着一个集合,即Application.Documents,里面包含着现在所有打开的Word文档。

 

Range 对象

Range是一个比较特殊的对象(我其实都不知道如何用中文翻译贴切地翻译这个单词),在你日常使用Word的过程中,你甚至可能不会知道有这样一个对象存在,但如果你想通过程序方式修改Word正文的内容,Range是一个很关键的对象。用微软官方的表述,Range代表着文档中一片连续的区域,微软为它列出了一下几个特性:

  1. Range的组成成分可以是单独的插入点,也可以是一个文本范围或整个文档。
  2. Range包含非打印字符,例如空格、制表符和段落标记。
  3. Range可以是当前所选内容所表示的区域,也可以表示当前所选内容之外的区域。
  4. Range与始终可见的所选内容不同,它在文档中是不可见的。
  5. Range不随文档保存,仅存在于代码运行期间。

我再为它加2条

  1. Range有明确的开始和结束,但不同的Range之间是可以有交集的
  2. Range的长度是在变化的,如果你往一个Range里面插入的一个单词,它的长度会自动变长。

获得Range对象的方式很多,我们可以通过Document对象的Range(ref object Start = Type.Missing, ref object End = Type.Missing)方法,创建一个自定义的Range。通过Word中文档相关的对象都有一个Range属性,比如Paragraph.Range、Selection.Range。

 

Selection 对象

Selection代表着当前光标所选中的对象,我们在开发过程中这个对象会和Application.WindowSelectionChange一起使用。

   1:          //
   2:          // Summary:
   3:          //     Occurs when the selection changes in the active document window.
   4:          event ApplicationEvents4_WindowSelectionChangeEventHandler WindowSelectionChange;

Delegate接口

   1:      [TypeLibType(16)]
   2:      [ComVisible(false)]
   3:      public delegate void ApplicationEvents4_WindowSelectionChangeEventHandler(Selection Sel);

 

Bookmark 与 Content Control

Bookmark即书签,在Word文档中做一个标记,方便查阅。开发过程中,我们可以基于Range来创建Bookmark。如:

   1:      // 将第一段文档标记为一个BookMark 
   2:      Word.Range range = Globals.ThisAddIn.Application.ActiveDocument.Paragraphs[0].Range;
   3:      range.Bookmarks.Add("JustinTest");

 

至此我们已经介绍完毕了Word中的主要5个对象,现在我们运用我们学到的东西,来实开头视频中的那个插件

 

插件:自定义Search面板

我先定义一下我们要实现的功能点,获得Word文档的内容,取得与检索关键字相关的上下文并显示在ListView中,当用户点中ListView中的项目时,高亮显示Word文档中对应的内容。以下是如何实现这几个功能的介绍。

 

获得Word的全文
要取得当前Word的全文,我们主要要解决两个问题。

  1. 如何获得当前的Document对象?
  2. 如何通过Document对象获得文档内容?

对于第一个问题,因为搜索功能主要是写在Task Pane中的UserControl中(这一点在上一篇文章中已经有过介绍),所以取得Document对象的主要方法,是通过Application对象获得ActiveDocument的对象,即当前编辑的文档:

    Globals.ThisAddIn.Application.ActiveDocument

对于第二个问题,我们有两个方法:首先,Document对象有Paragraphs集合,这个集合里面包含了每个段落的对象,而每个段落对象,都有Range属性,我们可以通过Paragraph.Range.Text,来获得每个段落的正文。其次Document对象有一个Range方法,通过它我们可以把整个Document作为一个Range。

 

Search按钮代码

   1:          private void btnSearch_Click(object sender, EventArgs e)
   2:          {
   3:              // 清楚文档中的高亮显示
   4:              ClearMark();
   5:   
   6:              lvSearchResult.Items.Clear();
   7:              if (string.IsNullOrWhiteSpace(tbSearchText.Text))
   8:              {
   9:                  return;
  10:              }
  11:   
  12:              // 按段落检索
  13:              Word.Document currentDocument = Globals.ThisAddIn.Application.ActiveDocument;
  14:              if (currentDocument.Paragraphs != null &&
  15:                  currentDocument.Paragraphs.Count != 0)
  16:              {
  17:                  foreach (Word.Paragraph paragraph in currentDocument.Paragraphs)
  18:                  {
  19:                      MatchCollection mc = Regex.Matches(paragraph.Range.Text, tbSearchText.Text.Trim(), RegexOptions.IgnoreCase);
  20:                      if (mc.Count > 0)
  21:                      {
  22:                          foreach (Match m in mc)
  23:                          {
  24:                              try
  25:                              {
  26:                                  int startIndex = paragraph.Range.Start + m.Index;
  27:                                  int endIndex = paragraph.Range.Start + m.Index + m.Length;
  28:   
  29:                                  Word.Range keywordRange = currentDocument.Range(startIndex, endIndex);
  30:                                  
  31:                                  // 获取上下文信息
  32:                                  // 获取前两个单词的位置(如果有)
  33:                                  startIndex = GetStartPositionForView(paragraph, m, startIndex);
  34:   
  35:                                  // 获取后两个单词的位置(如果有)
  36:                                  endIndex = GetEndPositionForView(paragraph, m, endIndex);
  37:   
  38:                                  // 在ListView中展示检索的关键字以及其上下文
  39:                                  Word.Range range = currentDocument.Range(startIndex, endIndex);
  40:                                  ListViewItem item = new ListViewItem(range.Text);
  41:                                  item.Tag = keywordRange;
  42:                                  lvSearchResult.Items.Add(item);
  43:                              }
  44:                              catch (Exception ex)
  45:                              {
  46:                                  MessageBox.Show(ex.Message);
  47:                              }
  48:                          }
  49:                      }
  50:                  }
  51:              }
  52:          }

介绍一下这段代码的几个功能点:

  1. 在Search功能开始时,先清除文档中的高亮显示(ClearMark方法稍后会介绍)。
  2. 分段落,依次查找关键字。
  3. 获得关键字的上下文,并放入ListView中显示。需要注意的时候,我在ListViewItem的tag对象里面,存入了Keyword在文档中的Range,为了ListView点击事件。

 

ListView点击事件

   1:          private void lvSearchResult_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
   2:          {
   3:              ClearMark();
   4:              if (lvSearchResult.SelectedItems.Count > 0)
   5:              {
   6:                  Word.Range range = lvSearchResult.SelectedItems[0].Tag as Word.Range;
   7:   
   8:                  // 为了可以恢复被修改的Range,我先将该Range和原本的Color放入Class的成员
   9:                  _LastRange = range;
  10:                  _LastRangeBackColor = range.HighlightColorIndex;
  11:                  range.HighlightColorIndex = Word.WdColorIndex.wdYellow;
  12:              }
  13:          }

总体来说这段代码很简单,我稍微介绍一下Range对象的使用,这里我修改了HighlightColorIndex属性,来修改文字的背景色,如果你修改字体相关的样式,可以通过Range.Font属性。(此外,我会在下一篇文中,介绍如何通过Range加超链接、书签或者Content Control)。

 

其他方法

   1:          private void ClearMark()
   2:          {
   3:              if (_LastRange != null)
   4:              {
   5:                  _LastRange.HighlightColorIndex = _LastRangeBackColor;
   6:              }
   7:          }

我在Search事件和ListView点击方法中都会先调用这个方法,它会使用在lvSearchResult_ItemSelectionChanged中保存的Range和Color,来恢复之前的样式。

 

总结

本文中,我介绍了Word对象模型的基本内容和我写的一个Word插件,包含对Application、Document和Range对象的操作。Word程序包含的内容是很多的,本来想把自己了解的Word知识一次写完,但写到这里发现已经写了很多了,怕大家会看得累眨眼,所以先发出来,再下一篇文章,我会进一步深入介绍Word插件开发。下次内容预告:

  1. 修改右键菜单
  2. 往文档中插入内容
  3. 添加超链接、书签
  4. 基于选中内容,显示悬浮框

 

最后,本文范例的代码可以在这里下载。此外,本文欢迎转载,但请保留出处,大家如果有问题,可以联系我 justin.tyrael@gmail.com

posted @ 2011-05-14 09:40 Justin Zhang 阅读(2759) 评论(14) 编辑

今天在debug一个奇怪的问题,一个程序在开发机器上没有任何错误,但是安装在客户机器上之后,只要客户运行它,就立刻停止响应。更加奇怪的是,程序本身没有输出任何日志。开始以为是安装包做的不好,但是几次实验之后,证明问题出在程序本身。经过排除法,发现这个问题只在启用了UAC的Windows 7和Vista上。于是我包了一个很大的Try Catch,并且用MessageBox直接输出Exception的内容。结果是令人惊讶的,问题居然就出在写Log的地方。

网上搜索了一下,便很快找到了原因。当User Access Control被启用之后,程序不能改写%ProgramFiles%目录中的内容。而我调试的程序一开始就在写Log,同时Log的目录又就是在安装目录底下。说白了,这其实是一个很不好的编程习惯所导致的问题,作为一个正规的程序,它的临时文件应该保存在%APPDATA%中。修改了Log保存路径之后,一切OK。

附C#中获得%APPDATA%的代码:

string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
posted @ 2011-05-05 22:13 Justin Zhang 阅读(272) 评论(0) 编辑

开始之前,首先我介绍一下我的开发环境:VS2010 + Office 2010,是基于.Net framework 4.0和VSTO 4.0。以下的范例代码都基于这个,如果你使用的是VS2008,那么问题也不大,基本可以移植过去。需要注意的是.Net framework 4.0中增加了dynamic类型可选参数,而这两点新的特性在VSTO 4.0中被广泛使用,新特性的详细内容我就不介绍了,(其实介绍的地方很多,Google一下就可以了眨眼),移植的时候需要稍微调整一下代码。

创建VSTO项目

环境搭建

要开发VSTO项目,环境搭建比较简单,微软这方面做得都做不错。只要安装了VS2010(需要勾选VSTO 4.0)和Office2010即可。VS2008配合Office2007也可以轻松搭建开发环境。

创建项目

选择创建新项目->C#->Office,然后选择Word 2010 Add-in,如下图:

创建新项目

可以看到图中除了Word 2010 Add-in,还有Word 2010 Document和Word 2010 Template这两种项目类型。它们的区别在于,Add-in属于Application level,即是一个插件,每次Word启动的时候都会加载这个插件。而Document和Template属于Document Level,这种项目会生成一个Word文件(Document项目会生成.docx文件,而Template会生成.dotx),你写的代码只有在运行这个特定的Word文件的时候才会起作用。

添加自定义的Ribbon

右击项目->添加新的Item->Office->Ribbon (Visual Designer),如下图:

添加Ribbon

 

往Ribbon上,添加一个按钮,并取名为Hello

在Ribbon上添加按钮

 

双击Hello按钮,在OnClick中,添加如下代码:

   1:          private void btHello_Click(object sender, RibbonControlEventArgs e)
   2:          {
   3:              System.Windows.Forms.MessageBox.Show("Hello World!");
   4:          }

按F5运行当前程序,VS会帮你自动开始一个新的Word实例,然后在Word中选择Add-ins标签页,点击Hello按钮。如果你顺利看到Hello Word!对话框,就说明你的第一个VSTO程序,顺利完成了。

第一个运行

对于我们的一个VSTO程序,我们做一些小的优化。从前一张图我们可以看到,我们的Ribbon和其他插件的Ribbon都挤在了Add-ins,这个Tabs中。我们现在要把我们的Ribbon独立出来。在VS中,打开Ribbon的设计界面,并选择Ribbon Tab,如下图,把ControlIdType改为Custom,然后把ControlID改为MyFirstAddin,这样我们的Ribbon会独占一个Ribbon Tab。

优化Ribbon

 

* 这里有个小Trick,如果你的客户希望你把自己建的Ribbon插入到Office自己的Ribbon里面去,我们也可以实现。把ControlIdType设定为Office,并把OfficeID设定为TabHome,如下图:

添加到TabHome

 

运行界面

TabHome

其他的OfficeID可以到这里查看:Office 2010 IDsOffice 2007 IDs

 

添加Task Pane

创建Task Pane

Task Pane是一个很有用的控件,它可以驻留在Office程序的窗口上,让开发者更具自己的需求创建界面。首先我们先创建一个User Control,之后这个User Control将嵌入在Task Pane中。而我们所添加的代码,主要也加在这个User Control中。

1 UserControlForTP

 

在UserControl上放一个Label,我们暂时不在这里写进一步的代码:

2 UserControlForTP View

 

在ThisAddIn.cs文件中,进行如下修改,这样在这样Addin被加载时,就会显示Task Pane:

   1:      public partial class ThisAddIn
   2:      {
   3:          public CustomTaskPane _MyCustomTaskPane = null;
   4:   
   5:          private void ThisAddIn_Startup(object sender, System.EventArgs e)
   6:          {
   7:              UCForTaskPane taskPane = new UCForTaskPane();
   8:              _MyCustomTaskPane = this.CustomTaskPanes.Add(taskPane, "My Task Pane");
   9:              _MyCustomTaskPane.Width = 200;
  10:              _MyCustomTaskPane.Visible = true;
  11:          }
  12:   
  13:          private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
  14:          {
  15:          }
  16:   
  17:          #region VSTO generated code
  18:          // 内容被省略
  19:          #endregion
  20:      }

 

运行结果:

3 TaskPaneResult

 

将Task Pane与Ribbon关联

我们已经完成了一个Task Pane,接下去要用之前创建的Ribbon控制Task Pane的显示。先对之前的Ribbon做一些修改,增加两个按钮,同时我们对界面做一些美化,具体步骤我这里不详述了,界面如下图:

4 Ribbon

为"Open Task Pane"和"Close Task Pane"添加Onclick事件,代码如下:

   1:          private void btnOpen_Click(object sender, RibbonControlEventArgs e)
   2:          {
   3:              if (Globals.ThisAddIn._MyCustomTaskPane != null)
   4:              {
   5:                  Globals.ThisAddIn._MyCustomTaskPane.Visible = true;
   6:              }
   7:          }
   8:   
   9:          private void btnClose_Click(object sender, RibbonControlEventArgs e)
  10:          {
  11:              if (Globals.ThisAddIn._MyCustomTaskPane != null)
  12:              {
  13:                  Globals.ThisAddIn._MyCustomTaskPane.Visible = false;
  14:              }
  15:          }
 
运行你的程序,便可以看到结果了。

本文总结

到这里,我介绍了VSTO的开发方法,以及Ribbon和Task Pane的基本知识,本文中的范例程序可以在这里下载:MyFirstAddin.zip。 在之后的文章中,我会深入介绍Word、Excel和Outlook的具体开发实例以及开发的细节。详细请看,我之前写的序,http://www.cnblogs.com/izualx/archive/2011/04/30/2033289.html

本文欢迎转载,但请保留出处,大家如果有问题,可以联系我 justin.tyrael@gmail.com

posted @ 2011-05-04 08:41 Justin Zhang 阅读(2900) 评论(15) 编辑
摘要: VSTO是微软提供给.Net开发人员的一个接口,通过他我们可以对Office程序做一些处理。但是这个接口并不尽善尽美,相比微软的很多其他产品,VSTO的稳定性并不好,相关的文档也很少。这种情况下,很多问题需要我们程序员自己去克服。我自己从08年进了现在这家公司,断断续续做VSTO开发已经快2年半了,不敢说对VSTO精通,但至少已经有了深入的解,尤其对开发中的一些Trick和一些Bug。准备写点东西记录一下这两年来的心路历程。同时也感慨于国内的VSTO文档资料实在匮乏,我自己在开发过程中走了不少弯路,希望我写的这些文档能对后来的开发者们有所帮助。我喜欢在写文档之前先准备一个提纲,然后再往里面填充阅读全文
posted @ 2011-04-30 10:11 Justin Zhang 阅读(1397) 评论(7) 编辑
摘要: PHP调试对我来说一直是一个问题,之前一直使用echo和var_dump这种打桩模式,效率很差,这次下定决心研究一下如何debug PHP,顺便把步骤记下来,加强记忆,也供大家参考。 首先说一下,如何使用XDebug来调试PHP程序。 我的开发环境 : 1, Eclipse PDT (Eclipse IDE for PHP Developers 1.3.0.20100617-0520),我比较喜欢Eclipse这个IDE,比较强大,插件也多 2, XAMPP version 1.7.4 (Apache 2.2.17,MySQL 5.5.8 (Community Server),PHP 5.3.阅读全文
posted @ 2011-02-11 15:33 Justin Zhang 阅读(1076) 评论(1) 编辑
摘要: LinqtoSql是一个不错的ORM框架,尤其是集成在VS2008中使用起来很方便。Sql CE 3.5则是VS2008中自带的一个轻量级的单机数据库。比较悲剧的是VS2008居然不支持把SQL CE中的表直接导入到DBML中,很难理解为什么微软这么做。但.Net framework 3.5中的LinqtoSql写明是支持SQL CE 3.5。也就是其中还是有方法可以实现的。 查询了Msdn得知,.Net Framework提供了一个叫做SqlMeta.exe的工具(一般位于系统盘下的Progream Files\Microsoft SDKs\Windows\v6.0A\bin),用这阅读全文
posted @ 2011-02-07 22:37 Justin Zhang 阅读(228) 评论(0) 编辑
摘要: 最近在用WPF做开发,项目进展的还算顺利,WPF总体来说还是比较方便的,其中变化最大的主要是Listview和Treeview控件,而且TreeView似乎在WPF是一个备受指责的控件,很多人说他不好用。其实我觉得是开发人员没有掌握好WPF中所传承的MVC思想。在View方面,WPF中的TreeView给了开发人员更大的灵活性,开发人可以非常简单定制每个Node乃至整棵树的外形。同时新的TreeView可以接受各种Collection作为ItemSource,非常灵活。只要简单地了解这些新加入的概念,开发起来就可以得心应手。 首先一个简单的Demo 如果这实现这个Demo呢?我们从MVC三个方阅读全文
posted @ 2011-02-03 20:56 Justin Zhang 阅读(4400) 评论(6) 编辑
摘要: 最近受朋友委托,帮他写一个读取串口的程序。以前只知道需要调用Windows API来实现,查询了MSDN之后发现,从.Net framework 2.0开始,.Net内部已经封装了SerialPort类,使得读取串口内容变得非常容易。 SerialPort位于System.IO.Ports namspace下面。常用的构造函数为 portName:端口号,如COM1,COM2,COM3……baud...阅读全文
posted @ 2010-12-24 11:13 Justin Zhang 阅读(756) 评论(0) 编辑