標籤:

Excel催化劑開源第11波-動態數組函數技術開源及要點講述

在Excel催化劑中,大量的自定義函數使用了動態數組函數效果,雖然不是原生的Excel365版效果(聽說Excel2019版取消了支持動態數組函數,還沒求證到位,Excel365是可以用,但也僅限於部分嘗鮮用戶可以用上,大部分Excel365用戶還沒推送成功),但對於自定義函數這條路線,可以實現類似效果,已經是非常震撼及使用面非常廣大了。

順便插一句,不是每個中國企業都能承擔起高昂的軟體更新費用,OFFICE軟體非常大的一個坑爹之處是,舊版本升級到新版本,沒有補差價升級一說,一律是重新購買,舊的也不能轉讓其他公司使用(這個沒求證過,有錯誤請指出)。

在這樣的OFFICE政策下,讓企業每三年緊追OFFICE新版本給員工配置,這個可是極大的軟體費用負擔和軟體使用浪費。

現在中美關係緊張,中國知識產權問題也是擺上檯面要考慮的事情,不能動不動類似個人一樣可以用盜版軟體,企業用的軟體是需要購買授權使用的,同樣的Excel催化劑也只是對個人用戶免費使用,企業用戶沒有任何承諾過免費使用一說。

在這樣的情形下,如果我們能夠通過自定義函數的方式,擴展一下原有舊版本的功能,使用戶們不必垂簾新OFFICE版本功能而沒法使用的打擊學習熱情,也可以有替代方案完成,間接也幫助企業節省軟體成本,為國家減少盜版問題的貿易摩擦,上升高一層,是一種愛國行為了。

動態數組函數實現手段

藉助ExcelDna框架來開發自定義函數,其作者也為我們準備了動態數組函數的技術實現,具體原理對於筆者這樣的重業務導向的,也不懂其中的高深技術,有興趣的朋友們可以深入研究後再更多分享出來。

在Excel自定義函數中,例如GetFiles函數,通過簡單的.Net的IO類,實現遍歷文件夾里的所有文件的功能,返回一個文件全路徑的數組。

最終的關鍵技術是,如何讓返回的這個數組結果,在用戶在一個單元格中輸入函數時,自動對其返回的多個結果進行單元格區域自動擴張,並以數組函數的方式返回。

GetFiles函數實現效果

具體代碼

在GetFiles函數中,關鍵代碼為最後將files數組返回到工作表結果的方法。

Common.ReturnDataArray(files, optAlignHorL);

[ExcelFunction(Category = "文件文件夾相關", Description = "獲取指定目錄下的文件清單,srcFolder為傳入的頂層目錄,containsText可用作篩選包含containsText內容的文件夾,isSearchAllDirectory為是否查找頂層目錄下的文件夾的所有子文件夾。Excel催化劑出品,必屬精品!")]
public static object GetFiles(
[ExcelArgument(Description = "傳入的頂層目錄,最終返回的結果將是此目錄下的文件夾或子文件夾下的全路徑文件名")] string srcFolder,
[ExcelArgument(Description = "查找的文件名中是否需要包含指定字元串,不傳參數默認為返回所有文件,可傳入複雜的正則表達式匹配。")] string containsText,
[ExcelArgument(Description = "是否查找頂層目錄下的文件夾的所有子文件夾,TRUE和非0的字元或數字為搜索子文件夾,其他為否,不傳參數時默認為否")] object isSearchAllDirectory,
[ExcelArgument(Description = "返回的結果是按按列排列還是按行排列,傳入L按列排列,傳入H按行排列,不傳參數或傳入非L或H則默認按列排列")] string optAlignHorL)
{
string[] files;
if (Common.IsMissOrEmpty(containsText))
{
containsText = string.Empty;
}
//當isSearchAllDirectory為空或false,默認為只搜索頂層文件夾
if (Common.IsMissOrEmpty(isSearchAllDirectory) || Common.TransBoolPara(isSearchAllDirectory) == false)
{
files = Directory.EnumerateFiles(srcFolder).Where(s => isContainsText(Path.GetFileName(s), containsText)).ToArray();
}
else
{

files = Directory.EnumerateFiles(srcFolder, "*", SearchOption.AllDirectories).Where(s => isContainsText(Path.GetFileName(s), containsText)).ToArray();
}

return Common.ReturnDataArray(files, optAlignHorL);
}

拆解此方法可知,其實最關鍵的部分已經出來了

return ArrayResizer.Resize(resultArr);

public static object ReturnDataArray(object[] srcArrData, string optAlignHorL)
{

int resultCount = srcArrData.Count();

if (Common.IsMissOrEmpty(optAlignHorL) || optAlignHorL.Equals("H", StringComparison.CurrentCultureIgnoreCase) == false)
{
optAlignHorL = "L";
}
else
{
optAlignHorL = "H";
}
//直接用從下標為0開始的數組也可以
if (optAlignHorL == "L")
{
object[,] resultArr = new object[resultCount, 1];
for (int i = 0; i < resultCount; i++)
{
resultArr[i, 0] = srcArrData[i];
}
//return resultArr;
return ArrayResizer.Resize(resultArr);
}

else
{
//橫排時,直接用一維數組就可以識別到
object[,] resultArr = new object[1, resultCount];
for (int i = 0; i < resultCount; i++)
{
resultArr[0,i] = srcArrData[i];
}
return ArrayResizer.Resize(resultArr);
}

}

最後貼上這個幫助類的源代碼,是從ExcelDna作者的示例代碼中抄過來的,筆者是看不懂的,但確實是起作用了,用了非同步函數的方法返回結果。

using ExcelDna.Integration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ExcelCuiHuaJi
{
public class ArrayResizer : XlCall
{

// This function will run in the UDF context.
// Needs extra protection to allow multithreaded use.
internal static object Resize(object[,] array)
{
var caller = Excel(xlfCaller) as ExcelReference;
if (caller == null)
return array;

int rows = array.GetLength(0);
int columns = array.GetLength(1);

if (rows == 0 || columns == 0)
return array;

if ((caller.RowLast - caller.RowFirst + 1 == rows) &&
(caller.ColumnLast - caller.ColumnFirst + 1 == columns))
{
// Size is already OK - just return result
return array;
}

var rowLast = caller.RowFirst + rows - 1;
var columnLast = caller.ColumnFirst + columns - 1;

// Check for the sheet limits
if (rowLast > ExcelDnaUtil.ExcelLimits.MaxRows - 1 ||
columnLast > ExcelDnaUtil.ExcelLimits.MaxColumns - 1)
{
// Cant resize - goes beyond the end of the sheet - just return #VALUE
// (Cant give message here, or change cells)
return ExcelError.ExcelErrorValue;
}

// TODO: Add some kind of guard for ever-changing result?
ExcelAsyncUtil.QueueAsMacro(() =>
{
// Create a reference of the right size
var target = new ExcelReference(caller.RowFirst, rowLast, caller.ColumnFirst, columnLast, caller.SheetId);
DoResize(target); // Will trigger a recalc by writing formula
});
// Return what we have - to prevent flashing #N/A
return array;
}

//public static double[,] ResizeDoubles(double[,] array)
//{
// var caller = Excel(xlfCaller) as ExcelReference;
// if (caller == null)
// return array;

// int rows = array.GetLength(0);
// int columns = array.GetLength(1);

// if (rows == 0 || columns == 0)
// return array;

// if ((caller.RowLast - caller.RowFirst + 1 == rows) &&
// (caller.ColumnLast - caller.ColumnFirst + 1 == columns))
// {
// // Size is already OK - just return result
// return array;
// }

// var rowLast = caller.RowFirst + rows - 1;
// var columnLast = caller.ColumnFirst + columns - 1;

// if (rowLast > ExcelDnaUtil.ExcelLimits.MaxRows - 1 ||
// columnLast > ExcelDnaUtil.ExcelLimits.MaxColumns - 1)
// {
// // Cant resize - goes beyond the end of the sheet - just return null (for #NUM!)
// // (Cant give message here, or change cells)
// return null;
// }

// // TODO: Add guard for ever-changing result?
// ExcelAsyncUtil.QueueAsMacro(() =>
// {
// // Create a reference of the right size
// var target = new ExcelReference(caller.RowFirst, rowLast, caller.ColumnFirst, columnLast, caller.SheetId);
// DoResize(target); // Will trigger a recalc by writing formula
// });
// // Return what we have - to prevent flashing #N/A
// return array;
//}

static void DoResize(ExcelReference target)
{
// Get the current state for reset later
using (new ExcelEchoOffHelper())
using (new ExcelCalculationManualHelper())
{
ExcelReference firstCell = new ExcelReference(target.RowFirst, target.RowFirst, target.ColumnFirst, target.ColumnFirst, target.SheetId);

// Get the formula in the first cell of the target
string formula = (string)Excel(xlfGetCell, 41, firstCell);
bool isFormulaArray = (bool)Excel(xlfGetCell, 49, firstCell);
if (isFormulaArray)
{
// Select the sheet and firstCell - needed because we want to use SelectSpecial.
using (new ExcelSelectionHelper(firstCell))
{
// Extend the selection to the whole array and clear
Excel(xlcSelectSpecial, 6);
ExcelReference oldArray = (ExcelReference)Excel(xlfSelection);

oldArray.SetValue(ExcelEmpty.Value);
}
}
// Get the formula and convert to R1C1 mode
bool isR1C1Mode = (bool)Excel(xlfGetWorkspace, 4);
string formulaR1C1 = formula;
if (!isR1C1Mode)
{
object formulaR1C1Obj;
XlReturn formulaR1C1Return = TryExcel(xlfFormulaConvert, out formulaR1C1Obj, formula, true, false, ExcelMissing.Value, firstCell);
if (formulaR1C1Return != XlReturn.XlReturnSuccess || formulaR1C1Obj is ExcelError)
{
string firstCellAddress = (string)Excel(xlfReftext, firstCell, true);
Excel(xlcAlert, "Cannot resize array formula at " + firstCellAddress + " - formula might be too long when converted to R1C1 format.");
firstCell.SetValue("" + formula);
return;
}
formulaR1C1 = (string)formulaR1C1Obj;
}
// Must be R1C1-style references
object ignoredResult;
//Debug.Print("Resizing START: " + target.RowLast);
XlReturn formulaArrayReturn = TryExcel(xlcFormulaArray, out ignoredResult, formulaR1C1, target);
//Debug.Print("Resizing FINISH");

// TODO: Find some dummy macro to clear the undo stack

if (formulaArrayReturn != XlReturn.XlReturnSuccess)
{
string firstCellAddress = (string)Excel(xlfReftext, firstCell, true);
Excel(xlcAlert, "Cannot resize array formula at " + firstCellAddress + " - result might overlap another array.");
// Might have failed due to array in the way.
firstCell.SetValue("" + formula);
}
}
}
}

// RIIA-style helpers to deal with Excel selections
// Dont use if you agree with Eric Lippert here: http://stackoverflow.com/a/1757344/44264
public class ExcelEchoOffHelper : XlCall, IDisposable
{
object oldEcho;

public ExcelEchoOffHelper()
{
oldEcho = Excel(xlfGetWorkspace, 40);
Excel(xlcEcho, false);
}

public void Dispose()
{
Excel(xlcEcho, oldEcho);
}
}

public class ExcelCalculationManualHelper : XlCall, IDisposable
{
object oldCalculationMode;

public ExcelCalculationManualHelper()
{
oldCalculationMode = Excel(xlfGetDocument, 14);
Excel(xlcOptionsCalculation, 3);
}

public void Dispose()
{
Excel(xlcOptionsCalculation, oldCalculationMode);
}
}

// Select an ExcelReference (perhaps on another sheet) allowing changes to be made there.
// On clean-up, resets all the selections and the active sheet.
// Should not be used if the work you are going to do will switch sheets, amke new sheets etc.
public class ExcelSelectionHelper : XlCall, IDisposable
{
object oldSelectionOnActiveSheet;
object oldActiveCellOnActiveSheet;

object oldSelectionOnRefSheet;
object oldActiveCellOnRefSheet;

public ExcelSelectionHelper(ExcelReference refToSelect)
{
// Remember old selection state on the active sheet
oldSelectionOnActiveSheet = Excel(xlfSelection);
oldActiveCellOnActiveSheet = Excel(xlfActiveCell);

// Switch to the sheet we want to select
string refSheet = (string)Excel(xlSheetNm, refToSelect);
Excel(xlcWorkbookSelect, new object[] { refSheet });

// record selection and active cell on the sheet we want to select
oldSelectionOnRefSheet = Excel(xlfSelection);
oldActiveCellOnRefSheet = Excel(xlfActiveCell);

// make the selection
Excel(xlcFormulaGoto, refToSelect);
}

public void Dispose()
{
// Reset the selection on the target sheet
Excel(xlcSelect, oldSelectionOnRefSheet, oldActiveCellOnRefSheet);

// Reset the sheet originally selected
string oldActiveSheet = (string)Excel(xlSheetNm, oldSelectionOnActiveSheet);
Excel(xlcWorkbookSelect, new object[] { oldActiveSheet });

// Reset the selection in the active sheet (some bugs make this change sometimes too)
Excel(xlcSelect, oldSelectionOnActiveSheet, oldActiveCellOnActiveSheet);
}
}
}

結語

以上涉及的所有代碼已經進行開源,並且整個自定義函數項目也已經開源了,甚至不需要重新開項目,重新複製粘貼,直接在源項目上增刪內容,即可完成自定義函數的開發,Excel催化劑開源作貢獻是認真的。

通過動態數組函數技術開發自定義函數,不必再煩惱於用戶不懂數組函數的複雜輸入方式及數組函數返回結果不確定時,不知道該選定多少單元格的煩惱。也不必羨慕Excel365用戶可以用上的動態數組函數,在Excel自定義函數中,比官方提供的函數使用場景更廣,門檻列低,通用性更強,在Excel2007及以後所有版本都可使用,方便作文件的分享。

技術交流QQ群

QQ群名:Excel催化劑開源討論群, QQ群號:788145319

Excel催化劑開源討論群二維碼

關於Excel催化劑

Excel催化劑先是一微信公眾號的名稱,後來順其名稱,正式推出了Excel插件,插件將持續性地更新,更新的周期視本人的時間而定爭取一周能夠上線一個大功能模塊。Excel催化劑插件承諾個人用戶永久性免費使用!

Excel催化劑插件使用最新的布署技術,實現一次安裝,日後所有更新自動更新完成,無需重複關注更新動態,手動下載安裝包重新安裝,只需一次安裝即可隨時保持最新版本!

Excel催化劑插件下載鏈接:pan.baidu.com/s/1Iz2_NZ

聯繫作者

公眾號

取名催化劑,因Excel本身的強大,並非所有人能夠立馬享受到,大部分人還是在被Excel軟體所虐的階段,就是頭腦里很清晰想達到的效果,而且高手們也已經實現出來,就是自己怎麼弄都弄不出來,或者更糟的是還不知道Excel能夠做什麼而停留在不斷地重複、機械、手工地在做著數據,耗費著無數的青春年華歲月。所以催生了是否可以作為一種媒介,讓廣大的Excel用戶們可以瞬間點燃Excel的爆點,無需苦苦地掙扎地沒日沒夜的技巧學習、高級複雜函數的燒腦,最終走向了從入門到放棄的道路。

最後Excel功能強大,其實還需樹立一個觀點,不是所有事情都要交給Excel去完成,也不是所有事情Excel都是十分勝任的,外面的世界仍然是一個廣闊的世界,Excel只是其中一枚耀眼的明星,還有其他更多同樣精彩強大的技術、工具等。*Excel催化劑也將借力這些其他技術,讓Excel能夠發揮更強大的爆發!

關於Excel催化劑作者

姓名:李偉堅,從事數據分析工作多年(BI方向),一名同樣在路上的學習者。

服務過行業:零售特別是鞋服類的零售行業,電商(淘寶、天貓、京東、唯品會)

技術路線從一名普通用戶,通過Excel軟體的學習,從此走向數據世界,非科班IT專業人士。

歷經重重難關,終於在數據的道路上達到技術平原期,學習眾多的知識不再太吃力,同時也形成了自己的一套數據解決方案(數據採集、數據加工清洗、數據多維建模、數據報表展示等)。

擅長技術領域:Excel等Office家族軟體、VBA&VSTO的二次開發、Sqlserver資料庫技術、Sqlserver的商業智能BI技術、Powerbi技術、雲伺服器布署技術等等。

2018年開始職業生涯作了重大調整,從原來的正職工作,轉為自由職業者,暫無固定收入,暫對前面道路不太明朗,苦重新回到正職工作,對Excel催化劑的運營和開發必定受到很大的影響(正職工作時間內不可能維護也不可能隨便把工作時間內的成果公佈於外,工作外的時間也十分有限,因已而立之年,家庭責任重大)。

和廣大擁護者一同期盼:Excel催化劑一直能運行下去,我所惠及的群體們能夠給予支持(多留言鼓勵下、轉發下朋友圈推薦、小額打賞下和最重點的可以和所在公司及同行推薦推薦,讓我的技術可以在貴司發揮價值,實現雙贏(初步設想可以數據顧問的方式或一些小型項目開發的方式合作)。

技術交流QQ群

QQ群名:Excel催化劑開源討論群, QQ群號:788145319

Excel催化劑開源討論群二維碼

關於Excel催化劑

Excel催化劑先是一微信公眾號的名稱,後來順其名稱,正式推出了Excel插件,插件將持續性地更新,更新的周期視本人的時間而定爭取一周能夠上線一個大功能模塊。Excel催化劑插件承諾個人用戶永久性免費使用!

Excel催化劑插件使用最新的布署技術,實現一次安裝,日後所有更新自動更新完成,無需重複關注更新動態,手動下載安裝包重新安裝,只需一次安裝即可隨時保持最新版本!

Excel催化劑插件下載鏈接:pan.baidu.com/s/1Iz2_NZ

聯繫作者

公眾號

取名催化劑,因Excel本身的強大,並非所有人能夠立馬享受到,大部分人還是在被Excel軟體所虐的階段,就是頭腦里很清晰想達到的效果,而且高手們也已經實現出來,就是自己怎麼弄都弄不出來,或者更糟的是還不知道Excel能夠做什麼而停留在不斷地重複、機械、手工地在做著數據,耗費著無數的青春年華歲月。所以催生了是否可以作為一種媒介,讓廣大的Excel用戶們可以瞬間點燃Excel的爆點,無需苦苦地掙扎地沒日沒夜的技巧學習、高級複雜函數的燒腦,最終走向了從入門到放棄的道路。

最後Excel功能強大,其實還需樹立一個觀點,不是所有事情都要交給Excel去完成,也不是所有事情Excel都是十分勝任的,外面的世界仍然是一個廣闊的世界,Excel只是其中一枚耀眼的明星,還有其他更多同樣精彩強大的技術、工具等。*Excel催化劑也將借力這些其他技術,讓Excel能夠發揮更強大的爆發!

關於Excel催化劑作者

姓名:李偉堅,從事數據分析工作多年(BI方向),一名同樣在路上的學習者。

服務過行業:零售特別是鞋服類的零售行業,電商(淘寶、天貓、京東、唯品會)

技術路線從一名普通用戶,通過Excel軟體的學習,從此走向數據世界,非科班IT專業人士。

歷經重重難關,終於在數據的道路上達到技術平原期,學習眾多的知識不再太吃力,同時也形成了自己的一套數據解決方案(數據採集、數據加工清洗、數據多維建模、數據報表展示等)。

擅長技術領域:Excel等Office家族軟體、VBA&VSTO的二次開發、Sqlserver資料庫技術、Sqlserver的商業智能BI技術、Powerbi技術、雲伺服器布署技術等等。

2018年開始職業生涯作了重大調整,從原來的正職工作,轉為自由職業者,暫無固定收入,暫對前面道路不太明朗,苦重新回到正職工作,對Excel催化劑的運營和開發必定受到很大的影響(正職工作時間內不可能維護也不可能隨便把工作時間內的成果公佈於外,工作外的時間也十分有限,因已而立之年,家庭責任重大)。

和廣大擁護者一同期盼:Excel催化劑一直能運行下去,我所惠及的群體們能夠給予支持(多留言鼓勵下、轉發下朋友圈推薦、小額打賞下和最重點的可以和所在公司及同行推薦推薦,讓我的技術可以在貴司發揮價值,實現雙贏(初步設想可以數據顧問的方式或一些小型項目開發的方式合作)。

推薦閱讀:

TAG:MicrosoftExcel |