第一次接触Grasshopper和EXCEL联动需求是在一个建筑日照分析项目中。当时需要把每小时计算的日照面积数据实时记录到报表,手动复制粘贴了三天后,我决定用C#脚本把这个过程自动化。没想到这个简单的需求解决后,团队的设计效率提升了近40%。
Grasshopper作为参数化设计的利器,其动态数据流特性让设计迭代变得异常高效。但当我们需要把这些数据整理成报告时,往往又回到了原始的手工操作。C#脚本就像一座桥梁,连接了参数化设计的动态世界和办公软件的静态报表。这种技术特别适合以下场景:
在开始编写脚本前,确保你的系统已经准备好以下环境:
我遇到过最棘手的问题就是Office版本不匹配。有次在32位Office上运行64位Rhino,COM接口直接罢工。建议用以下PowerShell命令检查Office位数:
powershell复制$officePath = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Office\16.0\Common\InstallRoot').Path
$exePath = Join-Path $officePath "EXCEL.EXE"
[System.Diagnostics.FileVersionInfo]::GetVersionInfo($exePath).FileDescription
在Grasshopper画布上拖拽一个C#脚本组件,右键点击选择"编辑脚本"。关键是要添加正确的程序集引用:
有个小技巧:先在Visual Studio中写好基础代码,测试通过后再复制到Grasshopper。这样可以利用VS的智能提示和调试功能,避免在GH中反复试错。
原始代码提供了最基本的数据写入功能,但在实际项目中我们需要更健壮的实现。这是我优化后的版本:
csharp复制using System;
using System.Linq;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
public void RunScript(
bool trigger,
List<object> data,
string filePath,
string sheetName,
string startCell,
ref object output)
{
if(!trigger) return;
Excel.Application excelApp = null;
Excel.Workbook workbook = null;
Excel.Worksheet worksheet = null;
try {
excelApp = new Excel.Application();
workbook = excelApp.Workbooks.Open(filePath);
worksheet = workbook.Sheets[sheetName] as Excel.Worksheet;
var range = worksheet.Range[startCell];
int startRow = range.Row;
int startCol = range.Column;
for(int i=0; i<data.Count; i++) {
worksheet.Cells[startRow + i, startCol] = data[i];
}
workbook.Save();
output = "数据写入成功";
}
catch(Exception ex) {
output = $"错误: {ex.Message}";
}
finally {
if(workbook != null) workbook.Close(false);
if(excelApp != null) excelApp.Quit();
ReleaseCOMObject(worksheet);
ReleaseCOMObject(workbook);
ReleaseCOMObject(excelApp);
}
}
private void ReleaseCOMObject(object obj) {
if(obj != null) {
Marshal.ReleaseComObject(obj);
obj = null;
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
这个版本主要改进了:
Grasshopper的数据流是动态变化的,我们需要考虑几种特殊情况:
csharp复制// 清空现有数据
var endCell = (Excel.Range)worksheet.Cells[startRow + data.Count - 1, startCol];
var writeRange = worksheet.Range[startCell, endCell];
writeRange.ClearContents();
csharp复制object formattedValue = item;
if(item is GH_Number) formattedValue = ((GH_Number)item).Value;
else if(item is GH_String) formattedValue = ((GH_String)item).Value;
// 其他类型处理...
worksheet.Cells[row, col] = formattedValue;
csharp复制object[,] valueArray = new object[data.Count, 1];
for(int i=0; i<data.Count; i++) {
valueArray[i, 0] = data[i];
}
var targetRange = (Excel.Range)worksheet.Range[startCell];
targetRange = targetRange.Resize[data.Count, 1];
targetRange.Value = valueArray;
在实际项目中,我们通常需要保持报表的格式和公式不变。我的做法是:
csharp复制// 复制模板工作表
var templateSheet = workbook.Sheets["Template"] as Excel.Worksheet;
templateSheet.Copy(After: workbook.Sheets[workbook.Sheets.Count]);
var newSheet = workbook.Sheets[workbook.Sheets.Count] as Excel.Worksheet;
newSheet.Name = $"方案_{DateTime.Now:yyyyMMdd_HHmmss}";
// 写入数据到新工作表
WriteDataToSheet(newSheet, data);
对于复杂项目,可能需要同步多种类型的数据。我设计了一个结构化的写入方案:
csharp复制var config = new Dictionary<string, CellMapping> {
{"日照分析", new CellMapping("B3", DataType.DoubleArray)},
{"能耗数据", new CellMapping("G10", DataType.KeyValuePairs)}
};
csharp复制foreach(var item in config) {
var data = GetDataByKey(item.Key); // 从GH获取对应数据
WriteData(worksheet, data, item.Value);
}
csharp复制// 刷新数据透视表
var pivotTable = worksheet.PivotTables("分析报表");
pivotTable.RefreshTable();
// 生成图表
var charts = worksheet.ChartObjects() as Excel.ChartObjects;
var chart = charts.Add(Left: 100, Top: 100, Width: 300, Height: 200);
chart.Chart.SetSourceData(worksheet.Range["B3:C20"]);
在数十个项目实践中,我总结了这些典型问题的应对方法:
问题1:Excel进程无法彻底关闭
症状:脚本运行后Excel.exe仍在后台进程
解决方案:
csharp复制// 在finally块中添加
System.Diagnostics.Process[] procs = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach(var proc in procs) {
if(proc.MainWindowTitle == "") { // 只关闭无窗口的残留进程
proc.Kill();
}
}
问题2:权限不足导致保存失败
症状:Save方法抛出异常
处理方法:
csharp复制try {
workbook.Save();
} catch {
string tempPath = Path.GetTempFileName() + ".xlsx";
workbook.SaveAs(tempPath);
output = $"文件保存到临时位置: {tempPath}";
}
问题3:数据格式错乱
症状:数字变成文本,日期显示异常
预防措施:
csharp复制// 在写入前设置单元格格式
var range = worksheet.Range[targetCell];
range.NumberFormat = "0.00"; // 两位小数
// 或
range.NumberFormat = "yyyy-mm-dd"; // 日期格式
在一个需要写入5000+数据点的幕墙项目中,原始脚本需要近2分钟完成操作。经过以下优化后缩短到5秒内:
csharp复制// 优化前:单单元格写入
for(int i=0; i<data.Count; i++) {
worksheet.Cells[row+i, col] = data[i];
}
// 优化后:数组批量写入
object[,] values = new object[data.Count, 1];
for(int i=0; i<data.Count; i++) {
values[i, 0] = data[i];
}
worksheet.Range[cell].Resize[data.Count, 1].Value = values;
csharp复制excelApp.ScreenUpdating = false;
// 执行写入操作...
excelApp.ScreenUpdating = true;
csharp复制excelApp.Calculation = Excel.XlCalculation.xlCalculationManual;
// 数据写入...
excelApp.Calculation = Excel.XlCalculation.xlCalculationAutomatic;
excelApp.Calculate();
对于需要团队协作的大型项目,我推荐采用以下架构:
csharp复制string centralFile = @"\\server\设计数据\项目中央数据库.xlsx";
string userSheet = $"{Environment.UserName}_{DateTime.Today:yyyyMMdd}";
LockAndWrite(centralFile, userSheet, myData);
csharp复制void SaveWithVersionControl(string filePath) {
string gitCmd = $"/C git add \"{filePath}\" && git commit -m \"自动更新 {DateTime.Now}\"";
Process.Start("cmd.exe", gitCmd).WaitForExit();
}
csharp复制// 1. 从Grasshopper获取数据
var analysisData = GetAnalysisResults();
// 2. 写入Excel模板
WriteToTemplate(analysisData);
// 3. 生成PDF报告
ExcelToPdf("报告模板.xlsx");
// 4. 发送邮件通知
SendEmail("设计报告已生成", "最新分析报告见附件");