我們知道,Excel中有很多內(nèi)置的函數(shù),比如求和,求平均,字符串操作函數(shù),金融函數(shù)等等。在有些時候,結(jié)合業(yè)務(wù)要求,這些函數(shù)可能不能滿足我們的需求,比如我想要一個函數(shù)能夠從WebService上獲取某只股票的最新價;我想要一個函數(shù)能夠獲取當(dāng)前的天氣情況,這些需求我們可以通過編寫Excel自定義函數(shù)(User Define Function ,UDF )來實現(xiàn),這樣,在Excel中直接調(diào)用我們的自定義函數(shù)即可滿足特定的業(yè)務(wù)需求,一般地,因為這種自定義函數(shù)的粒度相對較小,所以我們可以根據(jù)業(yè)務(wù)需求編寫很多基礎(chǔ)的自定義函數(shù),然后以這些自定義函數(shù)為基礎(chǔ),編寫各種復(fù)雜的分析報表。 編寫UDF的方式有很多種,比如直接在VBA種編寫自定義函數(shù);如果您熟悉C++,可以將自定義函數(shù)編寫到XLL中,不熟悉也可以使用ExcelDNA這個開源的庫使用.NET技術(shù)也可以將您的代碼編譯為XLL;如果熟悉.NET,使用C#編寫自定義函數(shù)類庫,然后將類庫注冊成Com組件也可在Excel中調(diào)用。下面就這幾種方式簡要介紹,并給出其優(yōu)缺點。
1. 使用C# 類庫注冊的方式實現(xiàn)Excel自定義函數(shù)我自己對.NET 較熟悉,所以首先介紹這種在.NET中即可進(jìn)行Excel自定義函數(shù)開發(fā)的模式,這種方法相對簡單。在開始之前,還是回到我們之前對YY插件的規(guī)劃,我們的YY插件有天氣,財經(jīng),地圖等功能,現(xiàn)在我們假設(shè)需要一個天氣自定義函數(shù),通過該函數(shù)能夠獲取某個城市某一天的天氣情況,比如說氣溫。 首先我們需要創(chuàng)建一個簡單的C#類庫,如下圖,其名為YYWeatherUDF。 然后,我們創(chuàng)建一個所有自定義函數(shù)的基類UDFBase.cs,在該類中,我們放一些基本的注冊Com組件所需要的一些操作以及屏蔽一些Object的對象的方法使其不要出現(xiàn)在Excel的UDF函數(shù)中來。有一點需要注意的是,在注冊及取消注冊為Com組件的時候,為避免Excel找不到mscoree.dll,需要往注冊表中寫入其全部路徑,下面的代碼即為實現(xiàn)這一功能。 public abstract class UDFBase { /// <summary> /// 解決在某些機(jī)器的Excel提示找不到mscoree.dll的問題 /// 這里在注冊表中將該dll的路徑注冊進(jìn)去,當(dāng)使用regasm注冊該類庫為com組件 /// 時會調(diào)用該方法 /// </summary> /// <param name="type"></param> [ComRegisterFunctionAttribute] public static void RegisterFunction(Type type) { Registry.ClassesRoot.CreateSubKey( GetSubKeyName(type, "Programmable")); RegistryKey key = Registry.ClassesRoot.OpenSubKey( GetSubKeyName(type, "InprocServer32"), true); key.SetValue("", System.Environment.SystemDirectory + @"\mscoree.dll", RegistryValueKind.String); } [ComUnregisterFunctionAttribute] public static void UnregisterFunction(Type type) { Registry.ClassesRoot.DeleteSubKey( GetSubKeyName(type, "Programmable"), false); } private static string GetSubKeyName(Type type, string subKeyName) { return string.Format("CLSID\\{{{0}}}\\{1}", type.GUID.ToString().ToUpper(), subKeyName); } } 將工程的AssemblyInfo.cs 中的[assembly: ComVisible(true)]。還有一點需要注意的是,Object類還有四個公共方法,如果直接繼承的話,這四個方法還是會出現(xiàn)在我們的UDF函數(shù)中,要避免Object對象的其中三個公共方法出現(xiàn)在UDF列表中,可以重寫對三個方法設(shè)置ComVisible自定義屬性。但是第四個GetType方法不允許重寫,所以還是會出現(xiàn)在UDF中。要解決這一問題,我們不能使用類了,要使用接口,然后將接口暴露為Interface,具體做法可以參見http:///questions/2817942/how-to-hide-gettype-method-from-com 和 http:///questions/1592440/excel-2007-udf-how-to-add-function-description-argument-help 這里為了簡潔和取舍,暫采用抽象類的方式處理。 /// <summary> /// 將Object類的四個公共方法隱藏 /// 否則將會出現(xiàn)在Excel的UDF函數(shù)中 /// </summary> /// <returns></returns> [ComVisible(false)] public override string ToString() { return base.ToString(); } [ComVisible(false)] public override bool Equals(object obj) { return base.Equals(obj); } [ComVisible(false)] public override int GetHashCode() { return base.GetHashCode(); } 以上這些代碼是一個在C# 創(chuàng)建一個UDF類的基本代碼,我們可以將該代碼保存為基類。 現(xiàn)在我們要編寫我們自己的實際的UDF函數(shù)了,新建一個名為WeatherFunc的類,繼承自上面的UDFBase抽象類,并在類的屬性上添加一些自定義屬性,這些屬性在后面注冊為Com組件的時候需要用到。ClassInterface類的構(gòu)造函數(shù)中參數(shù)ClassInterfaceType.AutoDual表示兩邊都傳遞,這就回是的Object類的所有公共方法都會顯示出來,如果采用實現(xiàn)接口的方式,將該熟悉設(shè)置為None即可解決多余方法出現(xiàn)在UDF列表中的問題,這里不細(xì)講,繼續(xù)。 [Guid("5268ABE2-9B09-439d-BE97-2EA60E103EF6")] [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class WeatherFunc : UDFBase { public WeatherFunc() { } } 其中Guid可以使用Visual Studio自帶的工具生成,如果我們不指定,則Visual Studio在將該類注冊為Com組件時,調(diào)用regasm工具的時候會自動生成一個Guid,在開發(fā)過程中,一般我們會手動指定一個Guid,這樣在調(diào)試的時候我們可以根據(jù)Guid在注冊表中去查找該項,看是否正常注冊為了Com組件。 然后我們在WeatherFunc中定義三個public方法,分別為YY_Weather_Condition ,YY_Weather_Temperture和YY_Weather_WindSpeed 這三個函數(shù)分別用來獲取天氣描述,氣溫和風(fēng)速。 這里的天氣情況使用了Yahoo Weather API,您可以參考http://developer.yahoo.com/weather/,API請求的格式中w代表城市編號,u代表單位,以上海為例,其請求的url為http://weather./forecastrss?w=2151849&u=c,他的返回格式是一個xml文件。這里我創(chuàng)建了一個Weather類對該接口進(jìn)行了封裝,由于這里重點講解UDF,故不做展開。 /// <summary> /// 根據(jù)城市和日期,返回當(dāng)天的天氣描述 /// </summary> /// <param name="city">城市</param> /// <param name="day">日期</param> /// <returns></returns> public String YY_Weather_Condition(String city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Condition.Text; } /// <summary> /// 根據(jù)城市和日期,返回當(dāng)天的氣溫狀況 /// </summary> /// <param name="city">城市</param> /// <param name="day">日期</param> /// <returns></returns> public double YY_Weather_Temperature(String city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Condition.Temperature; } /// <summary> /// 根據(jù)城市和日期,返回當(dāng)天的風(fēng)速 /// </summary> /// <param name="city">城市</param> /// <param name="day">城市</param> /// <returns></returns> public double YY_Weather_WindSpeed(String city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Wind.Speed; } 這樣我們的三個UDF函數(shù)已經(jīng)寫好了,然后右擊工程項目,在屬性設(shè)置-〉生成 注冊為Com組件。 這里注冊為Com組件時,Visual Studio其實是去調(diào)用regasm 將類庫注冊為Com組件,并在注冊表里面寫入一些信息。在發(fā)布到客戶端上部署的時候,我們可以直接導(dǎo)入該類庫的相關(guān)注冊表信息,或者直接調(diào)用regasm在客戶的機(jī)器上寫入這些信息,在后面講到Excel插件安裝部署的時候,會說到這些。 然后我們直接進(jìn)行編譯。因為注冊Com組件涉及對注冊表進(jìn)行操作,在XP以上系統(tǒng)中,需要管理員權(quán)限,如果當(dāng)前Visual Studio不是以管理員身份運行。則會出現(xiàn)下面錯誤: 保存退出,然后右鍵選擇以管理員權(quán)限打開VS,然后重新編譯。 完成之后,我們根據(jù)之前的GUID 5268ABE2-9B09-439d-BE97-2EA60E103EF6 到注冊表中去查找,可以看到,編譯并注冊為Com組件后會到注冊表中寫入一些信息,這些情況在安裝Office等軟件的時候,最后一步的時候,一般可以看到正在注冊組件,然后后面是不停閃動GUID,應(yīng)該是在做相同的工作。我機(jī)器上截圖如下。 現(xiàn)在我們打開Excel軟件,在開發(fā)工具-〉加載項中,將我們的自定義函數(shù)加載進(jìn)來。 現(xiàn)在,在公式->插入函數(shù)中可以看到我們編寫的自定義函數(shù)。 可以看到我們之前編寫的兩個UDF函數(shù),那個GetType是因為我們繼承自抽象類的緣故,其中三個已經(jīng)被重寫對Com不可見,GetType方法不能被重寫,沒有被移除,前面介紹了采用暴露接口的方式可以解決這一問題,需要將我們所有的UDF函數(shù)以接口的形式定義,然后在類中實現(xiàn)。這里就不多講了。 點擊相應(yīng)的函數(shù),即可彈出參數(shù)選擇框: 自此,采用.NET編寫自定義函數(shù)介紹完了,他的好處是編寫方便,如果您熟悉.NET的話,相當(dāng)于就是編寫了一個特殊的類庫。當(dāng)然也有一些缺點,比如說,在Excel中輸入函數(shù)的時候,沒有AutoComplete支持,并且在界面上不能顯示參數(shù)的解釋和函數(shù)的解釋,Com的注冊和注銷都需要對注冊表進(jìn)行讀寫操作,在安裝部署的時候可能需要一些注冊表讀寫權(quán)限以及需要某些殺毒軟件的放行,一般地,因為我們的插件再安裝的時候是一定需要道注冊表中注冊的,所以一般的UDF函數(shù)的注冊對注冊表的讀寫時是和插件安裝一同進(jìn)行的;UDF函數(shù)使用.NET 編寫容易反編譯;僅支持Excel 2003及以上版本。不過由于這種方式簡單,所以仍然是很多開發(fā)UDF函數(shù)的可選方案。 2. VBA 的方式編寫自定義函數(shù)相信使用Excel比較多的人對VBA比較熟悉,VBA其實就是Excel里面的腳本,通過VBA也可以編寫自定義函數(shù),存放到某個Sheet頁或者作為一個獨立的Excel腳本文件比如xla 文件存放。一般的,我們會將我們的自定義函數(shù)存在單獨的Excel腳本.xla文件中,然后在系統(tǒng)加載的時候,將該腳本文件加載進(jìn)來。然后就可以直接使用里面編寫的UDF函數(shù)了。 這里我們打開一個Excel文件,然后按Alt+F11打開VBA編輯器。 然后可以看到VBA 的IDE界面了,這時候,我們對著工程文件點擊,新添加一個模塊: 在該模塊中寫兩個簡單的函數(shù)。這一回,我們要編寫查找天氣最高溫度,最低溫度的兩個函數(shù)。命名為YY_Weather_TemperatureHigh,YY_Weather_ TemperatureLow。這里先簡單寫個假數(shù)字。 完成后,保存為.xla文件。然后回到Excel界面,現(xiàn)在我們在單元格中輸入我們的自定義函數(shù)的時候,當(dāng)我們輸入YY的時候,就有AutoComplete提示了,Excel 2003以上版本會為我們列出所有匹配以YY開頭的內(nèi)置函數(shù)和自定義函數(shù)了。 Excel函數(shù)只能提示界面上,按上,下鍵可以選擇,按Tab鍵確認(rèn)。輸入YY_Weather_TemperatureHigh的時候,單元格會返回30,輸入YY_Weather_TemperatureLow,單元格返回20,這兩個數(shù)字是我們硬編碼進(jìn)去的。這就是在VBA中編寫UDF函數(shù)的方式,簡單吧。如果邏輯比較簡單的話,您完全可以使用VBA語言來實現(xiàn)您的邏輯,比如說在VBA里面去查詢數(shù)據(jù)庫,去訪問WebService等等。 如果您對VBA不熟悉的話,也沒關(guān)系,VBA中也可以調(diào)用C# 類庫中編寫的方法。我們可以使用VBA來編寫UDF函數(shù)的簽名,然后在VBA方法體內(nèi)調(diào)用C# 里面的方法?,F(xiàn)在我們將前面的兩個取最高,最低氣溫的函數(shù)換成我們之前寫好的通過Weather類獲取氣溫的代碼。 要在VBA中調(diào)用C#中的方法,首先我們需要在第二篇文章中的SharedAddin程序的基礎(chǔ)上進(jìn)行,我們添加FunctionHelper類,并讓其繼承自StandardOleMarshalObject對象,然后提供一些對Weather函數(shù)進(jìn)行包裝的方法。代碼如下: public class FunctionHelper:StandardOleMarshalObject { public object GetWeather_TemperatureHigh(string city,DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Forecast.Days[0].High; } public object GetWeather_TemperatureLow(string city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Forecast.Days[0].Low; } } 然后,在Connect類的OnConnection中將ComAddin實例對象的Object的屬性設(shè)置為FunctionHelper實例對象: public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { applicationObject = application as Application; addInInstance = addInInst as COMAddIn; addInInstance.Object = new FunctionHelper(); if (applicationObject.Version == "11.0") { if (menuDesigner == null) { menuDesigner = new MenuDesigner(applicationObject); } menuDesigner.AddMenus(); menuDesigner.AddToolBars(); } } 然后在VBA中,我們根據(jù)ProgId查找我們的插件。 Public Function GetCOMAddIn(Optional addInName As String) As COMAddIn Dim YYAddIn As COMAddIn If addInName = "" Then addInName = "YYSharedAddin" End If Dim addInItem As COMAddIn For Each addInItem In Application.COMAddIns If addInItem.Description = addInName Then Set YYAddIn = addInItem Exit For End If Next addInItem Set GetCOMAddIn = YYAddIn End Function 這里我們之前的插件叫YYSharedAddin,然后改寫我們的之前寫過的兩個函數(shù): '獲取名為city的城市的當(dāng)天的最高氣溫 Function YY_Weather_TemperatureHigh(city As String, day As Date) Dim YYAddIn As COMAddIn Dim dataQuery As Object Set YYAddIn = GetCOMAddIn("YYSharedAddin") Set dataQuery = YYAddIn.Object YY_Weather_TemperatureHigh = dataQuery.GetWeather_TemperatureHigh(city, day) End Function '獲取名為city的城市的當(dāng)天的最低氣溫 Function YY_Weather_TemperatureLow(city As String, day As Date) Dim YYAddIn As COMAddIn Dim dataQuery As Object Set YYAddIn = GetCOMAddIn("YYSharedAddin") Set dataQuery = YYAddIn.Object YY_Weather_TemperatureLow = dataQuery.GetWeather_TemperatureLow(city, day) End Function 可以看到Y(jié)YAddin.Object對象在我們的Connect類的OnConnect的時候已經(jīng)賦值為了FunctionUtility的實例類,所以后面我們在VBA里可以直接調(diào)用里面的C# 方法了。 將上面的VBA保存為Excel .xla格式的宏文件。然后在VBA中和Visual Studio中C#函數(shù)方法體內(nèi)設(shè)置斷點,在Sheet頁中輸入我們的自定義函數(shù),然后可以看到VBA中的斷點被命中,注意在VBA IDE中,逐步運行調(diào)試的快捷鍵是F8,當(dāng)調(diào)試我們的dataQuery.GetWeather_TemperatureHigh這一句時,會跳到Visual Studio中的C#函數(shù)內(nèi)部,在VS中逐步調(diào)試的快捷鍵是F11,有意思吧。 一般地,我們會將自定義UDF的簽名放到Excel 宏文件中以.xla文件保存,然后在方法體內(nèi)調(diào)用C#方法。以YY插件項目為例,當(dāng)程序運行的時候,在Connect類的OnConnect方法中我們可以使用以下方法加載.xla宏腳本文件到Excel中。 string basePath = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; LoadUDFs(basePath + "\\YYFunc.xla", true); public bool LoadUDFs(string progID, bool load) { bool loaded = false; try { if (File.Exists(progID)) { AddIn udfAddIn = applicationObject.AddIns.Add(progID, true); udfAddIn.Installed = load; loaded = udfAddIn.Installed; } } catch { loaded = false; } return loaded; } 其中progID傳入我們腳本文件名加路徑,load為true。 一般地,采用VBA的方式來編寫UDF函數(shù)簡單快捷,而且比較靈活方便調(diào)試,對于一些復(fù)雜的邏輯,可以將方法體的實現(xiàn)邏輯放置到C#代碼中,在Excel 03以上版本,還有Excel智能提示的支持,在注冊的時候,不需要對注冊表進(jìn)行讀寫操作。但是這種方式也有一些缺點:
3. XLL方式編寫自定義函數(shù)XLL是Excel自97版本就支持的一種外接二進(jìn)制插件,其運行速度較前面的兩種方式快,并且有更多強(qiáng)大的功能,但是XLL通常使用C或者C++編寫,對于開發(fā)者的要求較高。關(guān)于XLL開發(fā)在第一篇文章的參考資料中有一本Financial Applications using Excel Add-in Development in C / C++ 講的比較深入,有興趣的同學(xué)可以看看。 幸運的是,有一個名為ExcelDNA的開源庫使得我們使用.NET語言即可編寫XLL程序。使用ExcelDNA很簡單,官網(wǎng)上的幫助文檔也很詳細(xì),這里簡要介紹一下如何使用ExcelDNA來編寫XLL自定義函數(shù)。當(dāng)然一開始我們需要到官網(wǎng)上下載安裝包: 先建一個C# 類庫,然后引用下載文檔中的ExcelDna.Integration.dll,創(chuàng)建兩個公共的靜態(tài)的方法,這里為了完善我們的歪歪天氣函數(shù),我們創(chuàng)建兩個函數(shù)YY_Weather_Condition,YY_Weather_Sunrise分別用來獲取對天氣的描述信息和當(dāng)天的日出時間。 public class YYWeather { [ExcelFunction(Description = "獲取指定城市的最新的天氣信息")] public static string YY_Weather_Condition(string city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Condition.Text; } [ExcelFunction(Description = "獲取指定城市的日出時間")] public static DateTime YY_Weather_Sunrise(string city, DateTime day) { Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius); return weather.Astronomy.Sunrise; } } 然后在項目中添加一個名為YYWeather.dna的文本文件。 <DnaLibrary Name="YY Weather Fucntion" RuntimeVersion="v4.0"> <ExternalLibrary Path="YYWeatherUDFExcelDNA.dll" /> </DnaLibrary> 然后將安裝包內(nèi)的ExcelDna.xll文件拷貝到項目內(nèi),并將其命名為YYWeather.xll,然后將其包含在項目中,設(shè)置其生成屬性,保證其在編譯時會拷貝到生成目錄下。 點擊生成的YYWeather.xll文件,然后先建一個Sheet頁,在打開的Excel Sheet頁中輸入YY,可以看到,我們之前建立的兩個自定義函數(shù)給予了提示。 選擇天氣狀況函數(shù),輸入城市和日期,可以看到返回了正確的天氣狀況信息,多云天氣 另外在插入函數(shù)的時候,可以看到,對函數(shù)和參數(shù)有提示: 我們選擇查詢?nèi)粘龊瘮?shù),輸入城市和時間之后,可以看到能夠返回正確的結(jié)果。 另外,在使用ExcelDNA時,比如上面的例子中,我們可以看到一堆的xll, dna, dll文件,不方便發(fā)布和部署。ExcelNDA提供了一個打包工具,可以將這些文件打包成一個xll文件,使用下載包內(nèi)的默認(rèn)打包程序ExcelDnaPack.exe即可實現(xiàn)這一功能,將ExcelDNAPack.exe拷貝到生成目錄,并在cmd下將當(dāng)前目錄切換到生成目錄下,命令行下運行程序,并將YYWeather.dna作為參數(shù)傳遞進(jìn)行: 程序會生成YYWeather-packed.xll,這個即為生成好的xll文件,他會將dll中的內(nèi)容打包到xll中去,分發(fā)部署的時候,只需要將這個文件發(fā)布到用戶的機(jī)器上即可。 一般地,在實際的開發(fā)環(huán)境中,以歪歪插件為例,和加載xla宏腳本文件一樣,在Connect的Onconnect方法中調(diào)用之前的LoadUDFs傳入該xll的完整路徑和文件名即可使用里面的自定義函數(shù)了。 string basePath = Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; LoadUDFs(basePath + "\\YYWeather-packed.xll", true); ExcelDNA功能強(qiáng)大,這里只是簡要介紹了如何使用.NET語言編寫UDF函數(shù),ExcelDNA支持VBA語言,F(xiàn)#語言,還可以實現(xiàn)Ribbon菜單,RTD函數(shù),異步UDF函數(shù)等,這些后面會介紹。 借助ExcelDNA編寫xll的UDF有很多優(yōu)點,比如說,運行速度快;Excel 2003以上版本支持函數(shù)的AutoComplete;支持函數(shù)和函數(shù)參數(shù)的注釋;不用對注冊表進(jìn)行讀寫;使用.NET 借助ExcelDNA開發(fā)門檻相對較低等優(yōu)點,在大多數(shù)情況下是比較理想的解決方案。 另外,ExcelDNA的函數(shù)提示,僅支持1000個UDF,就是說超過1000個,當(dāng)您在Excel中輸入的時候,下拉提示框中,超出的部分函數(shù)可能不會出現(xiàn)。這可能是ExcelDNA的開發(fā)者考慮到過多的提示項對增加內(nèi)存消耗的原因。解決方法很簡單,您只需要下載ExcelDNA的源代碼,搜索1000,然后改為您想要的數(shù)字,然后在有些地方添加一些代碼。編譯一下即可。 最后,使用上面的方式創(chuàng)建好了UDF之后,我們來創(chuàng)建一個簡單的天氣預(yù)報報表,我錄制了一個小動畫,如下圖,您應(yīng)該可以體會到Excel自定義函數(shù)的強(qiáng)大用處: 4. 結(jié)語Excel 中的自定義函數(shù)極大地擴(kuò)充了Excel的應(yīng)用領(lǐng)域,他也是Excel插件和業(yè)務(wù)邏輯的一個極好的接入點,通過Excel UDF函數(shù),您可以在此基礎(chǔ)上創(chuàng)建各種靈活的分析報表,構(gòu)建各種分析模型。本文簡要介紹了常見的三種編寫Excel UDF函數(shù)的方法,他們是采用.NET 托管代碼注冊為Com組件的方式,純VBA腳本和VBA調(diào)用.NET 類庫方式,以及借助Excel結(jié)合.NET 編寫xll的方式,三種方式各有優(yōu)缺點,現(xiàn)簡單總結(jié)如下:
下文將會介紹Excel中的另外一類比較重要的函數(shù),Real Time Data 即RTD函數(shù),這一類型的函數(shù)可以實現(xiàn)諸多強(qiáng)大的功能,比如在Excel中實現(xiàn)股票行情信息的實時刷新,實現(xiàn)Excel 異步自定義函數(shù)功能等等,敬請期待。 本文所有代碼點擊此處下載,希望本文對您了解Excel自定義函數(shù)有所幫助。 |
|