为 C# 互操作对象创建可重用 Excel 样式
Create reusable Excel Styles for C# interop objects
我正在使用 C# 生成几个电子表格,我正在尝试创建一个可重复使用的 Style,它可以应用于每个电子表格中的特定范围。我遇到的问题是当我的方法完成执行时 Excel 进程没有正确退出。事实上,我已经能够将问题范围缩小到我正在创建 Excel Style
.
的代码中
Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;
Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12; // if I comment this out, excel quits correctly
Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);
样式在我应用时按预期工作,但在我退出 excelApplication 时 Excel 进程没有退出。如果我注释掉 columnHeader.Font.Size = 12;
行,Excel 进程会正确退出。我错过了什么吗?
更新
我修改了 Govert 的示例 WinForms 应用程序以反映我的 class 的结构,它正在创建多个电子表格。他的应用正常退出了Excel进程,但是我修改后的版本没有:
using System;
using Excel = Microsoft.Office.Interop.Excel;
using System.Windows.Forms;
namespace ExcelCOMReferenceTesting
{
public partial class Form1 : Form
{
private Excel.Application excelApplication;
private Excel.Style columnHeader;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DoMyExcelStuff();
GarbageCleanup();
}
private void DoMyExcelStuff()
{
StartExcel();
var wBook = GenerateWorksheet();
excelApplication.Range["A1"].Value = "Name";
excelApplication.Range["A1"].Style = columnHeader;
wBook.SaveAs(@"c:\Test\tst" + DateTime.Now.ToString("mmss") + ".xlsx");
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
StopExcel();
}
private void StartExcel()
{
excelApplication = new Excel.Application();
excelApplication.Visible = false;
}
private void StopExcel()
{
excelApplication.UserControl = false;
excelApplication.Quit();
}
private Excel._Workbook GenerateWorksheet()
{
Excel.Workbooks books = excelApplication.Workbooks;
Excel.Workbook wBook = books.Add("");
Excel.Styles styles = wBook.Styles;
columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12;
columnHeader.Font.Bold = true;
return wBook;
}
private void GarbageCleanup()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
可能是对 Font / columnHeader 或样式的挂起引用,在 Excel 互操作中访问属性的属性可能会导致 Excel 进程挂起;试试这个
Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;
Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
Font font = columnHeader.Font;
font.Size = 12;
Marshal.ReleaseComObject(font);
Marshal.ReleaseComObject(columnHeader);
Marshal.ReleaseComObject(styles);
Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);
font = null;
columnHeader = null;
styles = null;
wSheet = null;
wBook = null;
books = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
您永远不需要在此上下文中调用 Marshal.ReleaseComObject
。运行时完全能够跟踪 COM 对象并在不再引用它们时释放它们。调用 Marshal.ReleaseComObject
是一个令人困惑的反模式,令人遗憾的是,甚至一些 Microsoft 文档也错误地暗示了这一点。您也不必将 local 变量设置为 null
- 方法完成时引用将超出范围。
要关闭 Excel,您需要调用 excelApplication.Quit()
,然后确保您没有对 Excel COM 对象的实时引用,然后调用垃圾收集器进行清理.您应该调用垃圾收集器两次 - 您可能会遇到引用形成循环的情况,第一次 GC 调用会打破循环,但 COM 对象可能只会在第二次调用时才能正确释放。
您还必须小心调试构建中的此类代码。方法中的引用被人为地保持活动状态,直到方法结束,以便它们仍然可以在调试器中访问。这意味着您的本地 Excel COM 对象可能无法通过在该方法内调用 GC 来清理。为避免此问题,您可以遵循如下模式:
public void DoMyExcelStuffAndCleanup()
{
DoMyExcelStuff();
// Call GC twice to ensure that cleanup after cycles happens immediately
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
public void DoMyExcelStuff()
{
Application excelApplication = ...
// Here you access all those Excel objects ...
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
excelApplication.Quit();
}
我现在已经用一个新的 WinForms 应用程序进行了测试,我在其中添加了一个按钮,并在后面添加了以下代码:
using System;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;
namespace WinFormsComCleanup
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DoMyExcelStuff();
GarbageCleanup();
}
private void DoMyExcelStuff()
{
Excel.Application excelApplication = new Excel.Application();
Excel.Workbooks books = excelApplication.Workbooks;
Excel.Workbook wBook = books.Add("");
Excel.Worksheet wSheet = (Excel.Worksheet)wBook.ActiveSheet;
Excel.Styles styles = wBook.Styles;
Excel.Style columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12;
columnHeader.Font.Bold = true;
excelApplication.Range["A1"].Value = "Name";
excelApplication.Range["A1"].Style = columnHeader;
wBook.SaveAs(@"c:\Temp\tst" + DateTime.Now.ToString("mmss") +".xlsx");
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
excelApplication.Quit();
}
private void GarbageCleanup()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
一切都按预期进行,Excel 进程在 GarbageCleanup()
调用期间停止。
更新
问题更新后,建议修改如下:
private void StopExcel()
{
excelApplication.UserControl = false;
excelApplication.Quit();
// Set the form-level variables to null, so that no live references to Excel remain
columnHeader = null;
excelApplication = null;
}
这将确保更新问题中引入的对象级字段 Excel 在 GC 运行之前删除它们的引用。
在我的测试中,通过此修改 Excel 再次退出。这与我的理解是一致的,即 .NET 运行时正确地跟踪 COM 引用,并且 Excel 互操作永远不需要 Marshal.ReleaseComObject
。
我正在使用 C# 生成几个电子表格,我正在尝试创建一个可重复使用的 Style,它可以应用于每个电子表格中的特定范围。我遇到的问题是当我的方法完成执行时 Excel 进程没有正确退出。事实上,我已经能够将问题范围缩小到我正在创建 Excel Style
.
Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;
Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12; // if I comment this out, excel quits correctly
Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);
样式在我应用时按预期工作,但在我退出 excelApplication 时 Excel 进程没有退出。如果我注释掉 columnHeader.Font.Size = 12;
行,Excel 进程会正确退出。我错过了什么吗?
更新
我修改了 Govert 的示例 WinForms 应用程序以反映我的 class 的结构,它正在创建多个电子表格。他的应用正常退出了Excel进程,但是我修改后的版本没有:
using System;
using Excel = Microsoft.Office.Interop.Excel;
using System.Windows.Forms;
namespace ExcelCOMReferenceTesting
{
public partial class Form1 : Form
{
private Excel.Application excelApplication;
private Excel.Style columnHeader;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DoMyExcelStuff();
GarbageCleanup();
}
private void DoMyExcelStuff()
{
StartExcel();
var wBook = GenerateWorksheet();
excelApplication.Range["A1"].Value = "Name";
excelApplication.Range["A1"].Style = columnHeader;
wBook.SaveAs(@"c:\Test\tst" + DateTime.Now.ToString("mmss") + ".xlsx");
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
StopExcel();
}
private void StartExcel()
{
excelApplication = new Excel.Application();
excelApplication.Visible = false;
}
private void StopExcel()
{
excelApplication.UserControl = false;
excelApplication.Quit();
}
private Excel._Workbook GenerateWorksheet()
{
Excel.Workbooks books = excelApplication.Workbooks;
Excel.Workbook wBook = books.Add("");
Excel.Styles styles = wBook.Styles;
columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12;
columnHeader.Font.Bold = true;
return wBook;
}
private void GarbageCleanup()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
可能是对 Font / columnHeader 或样式的挂起引用,在 Excel 互操作中访问属性的属性可能会导致 Excel 进程挂起;试试这个
Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;
Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
Font font = columnHeader.Font;
font.Size = 12;
Marshal.ReleaseComObject(font);
Marshal.ReleaseComObject(columnHeader);
Marshal.ReleaseComObject(styles);
Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);
font = null;
columnHeader = null;
styles = null;
wSheet = null;
wBook = null;
books = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
您永远不需要在此上下文中调用 Marshal.ReleaseComObject
。运行时完全能够跟踪 COM 对象并在不再引用它们时释放它们。调用 Marshal.ReleaseComObject
是一个令人困惑的反模式,令人遗憾的是,甚至一些 Microsoft 文档也错误地暗示了这一点。您也不必将 local 变量设置为 null
- 方法完成时引用将超出范围。
要关闭 Excel,您需要调用 excelApplication.Quit()
,然后确保您没有对 Excel COM 对象的实时引用,然后调用垃圾收集器进行清理.您应该调用垃圾收集器两次 - 您可能会遇到引用形成循环的情况,第一次 GC 调用会打破循环,但 COM 对象可能只会在第二次调用时才能正确释放。
您还必须小心调试构建中的此类代码。方法中的引用被人为地保持活动状态,直到方法结束,以便它们仍然可以在调试器中访问。这意味着您的本地 Excel COM 对象可能无法通过在该方法内调用 GC 来清理。为避免此问题,您可以遵循如下模式:
public void DoMyExcelStuffAndCleanup()
{
DoMyExcelStuff();
// Call GC twice to ensure that cleanup after cycles happens immediately
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
public void DoMyExcelStuff()
{
Application excelApplication = ...
// Here you access all those Excel objects ...
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
excelApplication.Quit();
}
我现在已经用一个新的 WinForms 应用程序进行了测试,我在其中添加了一个按钮,并在后面添加了以下代码:
using System;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;
namespace WinFormsComCleanup
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DoMyExcelStuff();
GarbageCleanup();
}
private void DoMyExcelStuff()
{
Excel.Application excelApplication = new Excel.Application();
Excel.Workbooks books = excelApplication.Workbooks;
Excel.Workbook wBook = books.Add("");
Excel.Worksheet wSheet = (Excel.Worksheet)wBook.ActiveSheet;
Excel.Styles styles = wBook.Styles;
Excel.Style columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12;
columnHeader.Font.Bold = true;
excelApplication.Range["A1"].Value = "Name";
excelApplication.Range["A1"].Style = columnHeader;
wBook.SaveAs(@"c:\Temp\tst" + DateTime.Now.ToString("mmss") +".xlsx");
// No need for Marshal.ReleaseComObject(...)
// No need for ... = null
excelApplication.Quit();
}
private void GarbageCleanup()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
一切都按预期进行,Excel 进程在 GarbageCleanup()
调用期间停止。
更新
问题更新后,建议修改如下:
private void StopExcel()
{
excelApplication.UserControl = false;
excelApplication.Quit();
// Set the form-level variables to null, so that no live references to Excel remain
columnHeader = null;
excelApplication = null;
}
这将确保更新问题中引入的对象级字段 Excel 在 GC 运行之前删除它们的引用。
在我的测试中,通过此修改 Excel 再次退出。这与我的理解是一致的,即 .NET 运行时正确地跟踪 COM 引用,并且 Excel 互操作永远不需要 Marshal.ReleaseComObject
。