前面花了三篇文章讲解了Excel中的UDF函数RTD函数异步UDF函数,这些都是Excel开发中的重中之重。本文现在开始接着第二篇文章的菜单系统开始讲解Excel中可供开发的界面元素,本文要讲解的是Excel中的自定义任务面板(Custome Task Panel,CTP) 。

    自定义任务面板在Office 2003中就引入了,相信大家都用过Word中的字典和插入剪贴画功能,左侧的边栏就是自定义面板。如下图:

CTPIn2003

    但是Office 2003的自定义面板并没有给我们开发人员提供开发接口,也就是说,我们不能创建我们的自定义的任务面板。

    从Office 2007版本开始,Office的一个显著变化是添加了更多的快速预览以及自定义面板,这在Office的各个产品中均有体现。如下图:

CTPIn2010

    更重要的是,从Office 2007开始,CTP接口开放使得开发人员可以将我们自己的业务逻辑集成到自定义面板中。

一 自定义任务面板的优点


    自定义参数面板有一些优点:

    首先:和传统的模态和非模态弹出框相比,CTP是嵌入到Office里面的,使得我们在CTP上的交互和工作区的内容不相互打扰,不会分散人的注意力:

    其次:用户的学习成本比较低,用户自定义的任务面板和内置的面板行为相似,这些面板也可以在程序内部上下左右停靠或者悬浮,我们可以创建多个任务面板,并且对每个面板进行单独的控制。

    再次,在开发方面,CTP 在Visual Studio中其实实际与窗体控件的,我们可以直接创建一个用户自定义控件,然后,在设计面板上像设计Windows Form程序一样进行设置。

二 CTP的工作原理


    通过Office的extensibility COM类库暴露出来的接口能够创建CTP窗体。CTP其实是一个窗体,窗体内包含了一个ActiveX控件,Excel负责管理CTP窗体,包括如何创建,销毁,以及处理窗体间的消息传递等,而ActiveX控件则负责提供我们的业务逻辑功能。CTP就是一个容器,他能够很好的和Excel进行结合。CTP窗体可以在Excel界面中停靠或者悬浮,用户可以改变大小,移动,或者关闭,这些都是由Excel来处理的。

    一般地,我们可以在VSTO和SharedAddin中创建CTP应用程序。使用VSTO创建CTP应用程序比较简单,只需要创建一个自定义窗体即可。在SharedAddin中创建CTP应用程序稍微有点儿复杂,Excel在加载Addin时需要创建CTP窗体,整个大致流程如下:

CTP

    首先,Excel启动的时候,通过注册表项加载特定的Addin程序,在加载Addin的时候查询Addin上是否实现由ICustomTaskPane接口,如果有,调用CTPFactoryAvailable接口,并将该方法的参数ICTPFactory对象保存下来,在后面调用该对象的Create方法创建对象,Create方法需要指定该CTP窗体内的ActiveX控件的ProgID值,然后将生成的CustomTaskPane对象保存,在根据菜单项显示或者隐藏该对象。

    接下来暂时如何在VSTO以及SharedAddin中实现CTP的功能。

三 CTP的实现


    我们可以在VSTO和SharedAddin中创建CTP应用程序,CTP内部的ActiveX控件其实是一个窗体,当在VSTO中创建CTP的时候,我们只需要实例化这个自定义窗体就可以了;但是在SharedAddin中要创建CTP必须要把自定义控件注册为Com组件才能使用。

    以天气预报功能为例子,在前面讲解RTD函数和异步UDF函数的时候,我们后面掩饰的时候,是直接在单元格里面敲函数表达式以及参数的,这样对用户不友好,所以我们将这部分功能可以放置在一个CTP中,然后用户进行选择城市以及指标,点击确定,我们再往Excel通过代码写入函数表达式。这样就可以防止用户的错误输入了。

    我们将在第二篇文章中的菜单系统作为例子,将相应项目加载到VS中来。首先为了达到代码的重用性,我们将该部分要显示的CTP里面的ActiveX控件放置到一个类库YYWeatherCTP里面,然后在里面仅添加一个名为YYWeatherWizard的自定义窗体,该窗体设计如下:

YYWeatherWizard

    该窗体主要用来帮助用户往Excel中插入天气函数。首先选择要看的城市,然后勾选该城市的各个气象指标。界面很简单,每个CheckBox的tag都保存有该指标的函数名称。需要注意的是,我们在该用户窗体中公开的一个可以供外部注册的事件,该事件在点击插入按钮时触发。具体代码为:

public event EventHandler<WeatherFunctionEventArgs> InsertFunctionEventHandler;

private void btnInsert_Click(object sender, EventArgs e)
{
    if (InsertFunctionEventHandler != null)
    {
        String cityName = this.cmbCity.SelectedText;
        List<String> functionNames = new List<string>();
        foreach (Control control in this.Controls)
        {
            GroupBox gb=control as GroupBox;
            if (gb != null)
            {
                foreach (Control ccontrol in gb.Controls)
                {
                    CheckBox cb = ccontrol as CheckBox;
                    if (cb != null)
                    {
                        if (cb.Checked)
                        {
                            functionNames.Add(cb.Tag.ToString());
                        }
                    }
                }
            }
        }
        WeatherFunctionEventArgs args = new WeatherFunctionEventArgs(cityName, functionNames);
        EventHandler<WeatherFunctionEventArgs> temp = null ;
        if (Interlocked.Exchange(ref temp, InsertFunctionEventHandler)==null)
        {
            temp(sender, args);
        }
    }
}

    上面的代码公开了InsertFunctionEventHandler事件,该事件的参数是一个WeatherFunctionEventArgs类型的 EventArgs类,具体定义如下:

public class WeatherFunctionEventArgs : EventArgs
{
    private readonly List<String> functionNames = new List<string>();
    private readonly String cityName;
    public List<String> FunctionNames { get { return functionNames; } }
    public String CtiyName { get { return cityName; } }

    public WeatherFunctionEventArgs(String cityName, List<String> weatherFunctions)
    {
        this.cityName = cityName;
        this.functionNames = weatherFunctions;
    }
}

    注册了插入事件之后,在回调方法中,可以返回用户选择的城市,以及选中的指标,我们将来可以在该回调方法中,利用这些信息往Excel中插入函数。

    该自定义控件编写完成之后,我们来演示如何在VSTO以及SharedAddin中将该用户控件作为CTP加载进来。先看看在VSTO中如何使用该控件。

3.1 在VSTO中创建CTP

    在第二篇文章中,我们介绍了Excel中的菜单系统,包括如何在VSTO以及SharedAddin中创建Excel菜单,本文在此基础上进行。首先打开我们之前在VSTO中创建的Ribbon菜单的后台代码。首先我们定义了一个名为TaskPanels的Dictionary对象,其中key为CTP的英文名称,Value表示一个CTP的CustomTaskPane对象。这里一般我们在创建完CTP之后,将其保存起来,后面如果不用的话,就将其隐藏,这样避免了来回创建CustomTaskPane对象造成的性能开销。

[ComVisible(true)]
public class Ribbon : Office.IRibbonExtensibility
{
    private Office.IRibbonUI ribbon;

    private Dictionary<String, CustomTaskPane> TaskPanels = new Dictionary<string, CustomTaskPane>();

    public static CustomTaskPaneCollection TaskPanelCollection;

    void yyWeatherCtp_InsertFunctionEventHandler(object sender, WeatherFunctionEventArgs e)
    {
        MessageBox.Show("Your insert a bunch of weather functions");
    }
}

    然后我们定义了一个公共的静态的TaskPanelCollection对象,用来保存,系统调用时的Addin对象。这个就和RTD函数中我们要通过局部变量保存IRTDUpdateEvent对象一边后来调研UpdateNotify一样,我们通过TaskPanelCollection对象保存全局的表示TaskPanel集合的对象,如下,在StartUp中给该对象赋值:

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    Ribbon.TaskPanelCollection = this.CustomTaskPanes;
}

    完成之后,我们在Ribbon菜单项中处理菜单点击事件。当用户点击天气函数的时候, 我们首先在TaskPanel中根据key查找有没有之前创建好了的CustomTaskPane,如果有,直接取出来,并将其Visible属性改为true。

public void GeneralButton_Click(Office.IRibbonControl control)
   {
       try
       {
           if (control.Id.Equals("btnWeatherFunction"))
           {
               CustomTaskPane weatherFunction = null;
               if (TaskPanels.TryGetValue("btnWeatherFunction", out weatherFunction))
               {
                   weatherFunction.Visible = true;
               }
               else
               {
                   var yyWeatherCtp = new YYWeatherWizard();
                   yyWeatherCtp.InsertFunctionEventHandler += new EventHandler<WeatherFunctionEventArgs>(yyWeatherCtp_InsertFunctionEventHandler);
                   var myTaskPane = TaskPanelCollection.Add(yyWeatherCtp, "Weather Report Function");
                   myTaskPane.DockPosition = Office.MsoCTPDockPosition.msoCTPDockPositionLeft;
                   myTaskPane.Width = 300;
                   myTaskPane.Visible = true;

                   TaskPanels.Add("btnWeatherFunction", myTaskPane);
               }

           }
       }
       catch (COMException ex)
       {
           Console.WriteLine("Create CTP encounter errors: " + ex.ToString());
       }
   }

    如果不存在, 首先创建一个之前设计好的YYWeatherWizard自定义窗体,并注册其插入按钮的回调方法。紧接着调用TaskPanelCollection的Add方法,该方法第一个参数接受一个实例化的自定义窗体,也就是我们之前创建好的YYWeatherWizard对象,第二个参数是CTP中在窗体左上角显示的名称,这里我们起名为“Weather Report Function”。然后紧接着设置停靠的位置,宽度,以及是否可见。最后,将我们创建好的CustomTaskPane对象存储到自定中,以用作缓存。

    编译运行,就可以看到效果了。这是在VSTO中创建CTP的方法,接下来将要介绍下如何在SharedAddin中创建CTP。

3.2 在SharedAddin中创建CTP

    在SharedAddin中创建CTP比在VSTO中要复杂一些,在SharedAddin中使用CTP我们需要将创建好的自定义窗体注册为Com组件,这样在SharedAddin中才能直接调用。因为我们之前已经定义了YYWeatherFucntion自定义用户窗体控件,我们需要做的就是让其Com可见,改动也非常简单。

[ComVisible(true)]
public partial class YYWeatherWizard : UserControl

然后类库编译的时候,设置下Com可见:

Compile CTP

    如果您使用的是Win7系统,那么需要以管理员权限运行Visual Studio编译,VS会调研regasm程序对dll进行注册,并往注册表里面写注册表项。

    这一步完成之后,我们打开在第二篇文章中我们创建的SharedAddin应用程序。

/// <summary>
///   The object for implementing an Add-in.
/// </summary>
/// <seealso class='IDTExtensibility2' />
[GuidAttribute("B61D06DE-C254-43B4-A417-BF2A45270D37"), ProgId("YYSharedAddin.Connect")]
public partial class Connect : Object, Extensibility.IDTExtensibility2, ICustomTaskPaneConsumer
{
    static ICTPFactory ctpFactory = null;

    public void CTPFactoryAvailable(ICTPFactory CTPFactoryInst)
    {
       ctpFactory=CTPFactoryInst;
    }

…………..
}

    要在SharedAddin中实现CTP必须让Connect对象实现ICustomTaskPaneConsumer接口,该仅有一个方法,我们定义了一个ICTPFactory对象来讲该方法传进来的CTPFactoryInst对象保存起来。为以后创建CTP对象做准备。

    在Ribbon的事件处理中,我们处理菜单点击事件:

public void GeneralButton_Click(Office.IRibbonControl control)
{
    if (control.Id.Equals("btnWeatherFunction"))
    {
        CustomTaskPane weatherFunction = null;
        if (TaskPanels.TryGetValue("btnWeatherFunction", out weatherFunction))
        {
            weatherFunction.Visible = true;
        }
        else
        {
            try
            {
                weatherFunction = ctpFactory.CreateCTP("YYWeatherCTP.YYWeatherWizard", "Weather Report Function", Type.Missing);
                weatherFunction.DockPosition = MsoCTPDockPosition.msoCTPDockPositionLeft;
                weatherFunction.Width = 270;
                weatherFunction.Visible = true;
                TaskPanels.Add("btnWeatherFunction", weatherFunction);

                Control contentControl = weatherFunction.ContentControl as Control;
                contentControl.Dock = System.Windows.Forms.DockStyle.Fill;
                YYWeatherWizard yyWeatherWizard = contentControl as YYWeatherWizard;
                yyWeatherWizard.InsertFunctionEventHandler += new EventHandler<WeatherFunctionEventArgs>(yyWeatherWizard_InsertFunctionEventHandler);
            }
            catch (COMException comEX)
            {
                Console.WriteLine("Create CTP encounter errors: " + comEX.ToString());
            }
        }
    }

}

    首先,我们在TaskPanels中查找该CTP是否已经创建,如果没有找到,则利用之前保存的ctpFactory对象来创建一个CustomTaskPane,注意ctpFactory的CreateCTP方法和VSTO中的Add方法不一样,该方法第一个参数为待创建的CTP中的ActiveX控件,也就是我们之前创建的自定义窗体的类的全称,包括程序集的名称,第二个参数一样。可以看到Excel使用反射技术来生成CTP中的ActiveX控件的。生成完了之后,我们得到了一个类型为CustomTaskPane的weatherFunction对象,现在,我们是无法注册插入事件 。要想获得到我们得自定义窗体控件,首先要将weatherFunction的ContentControl转换为Control控件,然后再将Control控件转换为我们得实际类型YYWeatherWizard,现在我们就可以注册插入的回调方法了。回调方法里面很简单,就是根据选中的城市以及指标,拼出RTD函数,然后插入到Excel中。

void yyWeatherWizard_InsertFunctionEventHandler(object sender, WeatherFunctionEventArgs e)
{
    String city = e.CtiyName;
    String dt = DateTime.Today.ToString("yyyy-MM-dd");

    String[,] formulas = new String[1, e.FunctionNames.Count];
    for (int i = 0; i < e.FunctionNames.Count; i++)
    {
        //            =RTD("YYAsyncRTD.Func",,"YY_Weather_Condition",2, "Shanghai",TODAY())
        formulas[0, i] = String.Format("=RTD(\"YYAsyncRTD.Func\",,\"{0}\",2,\"{1}\",\"{2}\"", e.FunctionNames[i], city, dt);
    }
    Range targetRange = applicationObject.Application.ActiveCell as Range;
    if (targetRange == null)
        targetRange = applicationObject.Application.get_Range("A1", Type.Missing);
    FillRange(targetRange, formulas, true);
}

    现在在SharedAddin中创建CTP已经实现啦。

3.3 运行效果

    下面的动画就是整个CTP的效果:当点击菜单上的天气函数的时候,左侧栏出现天气函数的CTP窗体,当点击插入的时候,即可向Excel中插入天气函数,然后天气函数去获取当前选中城市的选中的天气指标的情况,很酷吧。

CTP

四 总结


    Excel中的CTP面板具有良好的用户体验,是业务逻辑的一中比较好的载体,在插件开发中具有比较重要的位置。本文介绍了Excel 中的Custom Task Panel自定义任务面板功能的运行原理,开发方法,以及如何在VSTO以及SharedAddin中创建和使用CTP,最后以一个天气函数的实例协助用户插入天气函数的CTP面板做了演示。 希望本文对您了解Excel中的自定义任务面板有所帮助。