文章出處 http://support.microsoft.com/?kbid=302901
毫無疑問,微軟公司的Excel是一個非常成功的應用軟件。它不僅本身功能強,而且還允許用戶根據自己需要進行再開發以擴展其功能。傳統上,我們可以用C/C++語言或者VBA對Excel進行擴展性開發。C/C++功能強大,但是開發工作比較復雜;VBA使用起來非常方便,但是功能有限。.NET的推出為我們提供了一種能夠結合這兩者長處的再開發手段。基於.NET的解決方案可以利用所有.NET開發平台提供的強大功能,例如多線程編程、分布式計算等。這都是VBA難以做到的,事實上即使使用C/C++,我們也很難實現這些功能。不僅如此,我們還可以充分利用Visual Studio的整體開發環境,例如版本控制之類的功能。這些功能對於團隊開發非常重要,但是在VBA開發環境下則非常困難。
本文將以C#為例介紹對Excel進行再開發的方法和技巧。我們通常有三種方法來對Excel進行再開發:
一是擴展插件(AddIn)。這是開發Excel用戶自定義函數的首選。
二是VSTO(Visual Studio Tool for Office)。這是開發Excel內嵌功能的首選(例如按鈕、菜單等等)。
三是RTD(Real Time Data)。這是開發Excel實時數據應用程序的首選。
盡管它們針對的用途不同,但是開發的基本手段和過程是非常類似的。在技術細節上,這三種方法主要的不同是需要實施不同的接口。在本文中,我們將主要介紹利用C#開發Excel擴展插件的方法。讀者可以依次類推在需要的時候開發其它兩類的程序。
首先,創建一個C#庫程序(library)並指定這個類程序庫“控件可見”(Com Visible)。這通常是通過項目屬性、Assembly屬性來指定的,如下圖所示。(注意不同版本的Visual Studio可能對話框的樣子不同)。
為了方便起見,我們還可以讓程序自動注冊成控件。這可以通過項目屬性、編譯屬性來指定,如下圖所示。如果我們不指定這個屬性的話,或者我們需要在其它計算機上使用這個程序的話,我們可以使用.NET平台自帶的RegAsm.exe來手工進行注冊。
其次,我們的項目需要添加以下兩個引用:
Ø (.NET)Extensibility
Ø (COM)Microsoft Office Excel Object Library(選用最新版本)
然后,輸入以下程序。程序中的注釋已經對各部分的作用作了適當說明。注意,這個類程序並不是Excel用戶自定義函數本身,而是包含了所有需要的基本架構函數。有了這個基類以后,我們就可以編寫一個簡單的子類僅僅包含我們需要的Excel用戶自定義函數。
using System;
using System.Runtime.InteropServices;
using Extensibility;
using MyWin32 = Microsoft.Win32;
using MsExcel = Microsoft.Office.Interop.Excel;
namespace MyExcelAddIn
{
///
/// Excel用戶自定義函數的基類,包含了所有必需的基礎架構類函數。它的子類則可以專注於用戶自定義函數的編寫。
///
public class ExcelUDFBase : IDTExtensibility2
{
///
/// 這個函數將在我們的AddIn被注冊時調用。
///
///
[ComRegisterFunctionAttribute]
public static void RegisterFunction(Type type_)
{
MyWin32.Registry.ClassesRoot.CreateSubKey(GetSubKeyName(type_,"Programmable"));
// 以下這兩個函數調用不是必須的。但是省略它們的話,每次我們從EXCEL中引用或取掉這個AddIn時,EXCEL都會提示找不到mscoree.dll,問你
// 是否要刪掉這個AddIn。當然我們應該回答“不”。
MyWin32.RegistryKey key = MyWin32.Registry.ClassesRoot.OpenSubKey(GetSubKeyName(type_, "InprocServer32"), true);
key.SetValue(""
, String.Format("{0}\\mscoree.dll", System.Environment.SystemDirectory)
, MyWin32.RegistryValueKind.String
);
}
///
/// 這個函數將在我們的AddIn被注銷時調用。
///
///
[ComUnregisterFunctionAttribute]
public static void UnregisterFunction(Type type_)
{
MyWin32.Registry.ClassesRoot.DeleteSubKey(GetSubKeyName(type_,"Programmable"));
}
///
/// 工具函數。
///
///
///
///
private static string GetSubKeyName(Type type_, String sub_key_ame_)
{
return String.Format("CLSID\\{{{0}}}\\{1}", type_.GUID.ToString().ToUpper(), sub_key_ame_);
}
#region IDTExtensibility2 Members
///
/// 這個變量將指向Excel應用程序。
///
protected MsExcel.Application MyExcelAppInstance { get; private set; }
///
/// 這個變量將指向我們的AddIn。
///
protected object MyAddInInstance { get; private set; }
///
/// 這個函數和以下四個函數都是為了實施IDTExtensibility2接口。實施IDTExtensibility2接口並不是必須的。但是如果我們不實施這個接口的
/// 話,我們將無法得到,從而無法編寫Volatile函數。
///
///
public void OnAddInsUpdate(ref Array custom)
{
}
///
///
///
public void OnBeginShutdown(ref Array custom)
{
}
///
///
///
///
///
///
public void OnConnection(object Application, ext_ConnectMode ConnectMode, objectAddInInst, ref Array custom)
{
MyExcelAppInstance = (MsExcel.Application)Application;
MyAddInInstance = AddInInst;
}
///
///
///
///
public void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom)
{
}
///
///
///
public void OnStartupComplete(ref Array custom)
{
}
#endregion
}
}
最后我們可以輸入以下程序來創建了我們第一個用戶自定義函數。(當然,另寫一個類不是必須的,我們也可以把這個函數直接寫在ExcelUDFBase類里。)
using System;
using System.Runtime.InteropServices;
using MsExcel = Microsoft.Office.Interop.Excel;
namespace MyExcelAddIn
{
///
/// 這個類包括我們真正需要的用戶自定義函數。它的兩個類屬性是必須的。但是每個這樣的類應該具有唯一的GUID值。GUID可以用Visual Studio自己生成。
/// 我們只要選擇菜單中的工具(Tools),然后選擇GUID選項就可以了。
///
[ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
[Guid("348E9F3A-96E4-42da-A5B9-FAD52E7744BB")]
public class MyFunctions : ExcelUDFBase
{
///
/// 這是個簡單的樣例函數。它的邏輯非常簡單,無需過多解釋。需要指出的是這個函數的返回值是object以及其內部的try . . . catch程序結構。
/// 當程序出錯的時候,這樣的結構可以使我們的用戶自定義函數返回合適的錯誤信息。
///
///
///
///
public object MyDivid(double Value1, double Value2)
{
try
{
return Value1 / Value2;
}
catch (Exception err_)
{
return err_.ToString();
}
}
}
}
編譯好之后,我們就可以啟動Excel。先從菜單上選擇“工具”(Tools)、“插件”(AddIn),然后點擊“自動插件”(Automation)按鈕。然后在跳出的對話框中選擇MyExcelAddIn.MyFunctions。如果一切順利的話,我們就可以使用我們剛才編寫的MyDivid()函數了,如下圖所示:
一個常見的可能問題是Excel和.NET版本不兼容。例如如果你使用的是Office 2002版而程序是用Visual Studio 2005版開發的。這時候,你就需要告訴Excel加載合適的.NET運行環境。這其實很簡單,只要創建一個內容如下的文本文件,命名為excel.exe.config,並把它保存在excel.exe同一個目錄下就可以了。
xml version=”1.0”?>
<configuration>
<startup>
<supportedRuntime version=”v2.0.50727”/>
startup>
configuration>
至此,我們可以看到用C#開發Excel控件基本上和開發其它應用程序一樣簡單。我們唯一需要添加的就是在文章一開頭ExcelUDFBase類里定義的幾個函數,而這幾個函數完全可以照抄。事實上,如果我們只要編寫類似於MyDivid()這樣的函數的話,我們甚至無需實施IDTExtensibility2接口,即只需要RegisterFunction()和UnregisterFunction()這兩個函數就可以了。實施IDTExtensibility2接口的主要目的是為了獲得指向Excel和AddIn的兩個變量。有了這兩個變量,我們就可以做一些比較高級開發工作。其中最簡單但是最重要的大概是編寫所謂的易變函數(Volatile)。熟悉Excel用戶自定義函數的讀者應該知道非易變函數只要輸入沒有變化,Excel就不會重新調用該函數。而易變函數則沒有這個限制。用C#編寫易變函數其實很容易,只要在相應的函數里首先調用MyExcelAppInstance.Volatile(Type.Missing)就可以了,如下所示:
///
/// 這個函數是一個易變(Volatile)函數的例子。我們只要在相應函數的開頭加上MyExcelAppInstance.Volatile(Type.Missing)就可以
///
///
public double MyRand()
{
MyExcelAppInstance.Volatile(Type.Missing);
return m_random.NextDouble();
}
///
///
///
private Random m_random = new Random();
用C/C++開發過Excel用戶自定義函數的讀者一定知道內存管理是個大問題,甚至傳遞一個字符串也需要考慮怎么釋放內存。但是用C#則沒有這個問題,甚至C#會為我們自動進行類型轉換。請見下面這個例子:
///
///
///
///
///
///
///
public object ShowInfo(String Name, DateTime When, MsExcel.Range Dummy)
{
try
{
return String.Format("{0} selected {2} cells at {1}.", Name, When, Dummy.Cells.Count);
}
catch (Exception err_)
{
return err_.ToString();
}
}
}
一個例子如下圖所示:
另外一個常用技巧是函數參數的缺省值。我們知道C#一般不支持缺省參數,但是在這里我們卻可以使用缺省參數。
///
/// 使用缺省參數只要使用Optional這個屬性就可以了。
///
///
///
public object MyEcho([Optional, DefaultParameterValue("Buddy")]String Name)
{
try
{
return Name;
}
catch (Exception err_)
{
return err_.ToString();
}
}
一個例子如下圖所示:
至此,我們已經介紹了利用C#開發Excel控件的基本方法和一些基本技巧。由此出發,我們就可以利用.NET平台提供的強大功能根據需要來開發更為復雜的用戶自定義函數。