如何正确清理Excel互操作对象?
我在C(ApplicationClass)中使用Excel互操作,并将以下代码放在我的finally子句中:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { } excelSheet = null; GC.Collect(); GC.WaitForPendingFinalizers();
尽管这种工作有效,但是即使我关闭Excel之后,Excel.exe
进程仍在后台。仅在我的应用程序手动关闭后才释放它。
我在做什么错,还是有其他方法可以确保正确处理互操作对象?
解决方案
这适用于我正在从事的项目:
excelApp.Quit(); Marshal.ReleaseComObject (excelWB); Marshal.ReleaseComObject (excelApp); excelApp = null;
我们了解到,完成对Excel COM对象的所有引用后,将其设置为null十分重要。其中包括单元格,表格和其他所有内容。
Excel不会退出,因为应用程序仍在保留对COM对象的引用。
我猜我们正在调用COM对象的至少一个成员,而没有将其分配给变量。
对我来说,这是excelApp.Worksheets对象,我直接使用它而不将其分配给变量:
Worksheet sheet = excelApp.Worksheets.Open(...); ... Marshal.ReleaseComObject(sheet);
我不知道内部为Worksheets COM对象创建了一个包装,但我的代码没有释放该包装(因为我不知道它),这是未卸载Excel的原因。
我在此页面上找到了解决问题的方法,对于在C#中使用COM对象也有一个很好的规则:
Never use two dots with COM objects.
因此,有了这些知识,完成上述操作的正确方法是:
Worksheets sheets = excelApp.Worksheets; // <-- The important part Worksheet sheet = sheets.Open(...); ... Marshal.ReleaseComObject(sheets); Marshal.ReleaseComObject(sheet);
Excel名称空间中的所有内容都需要释放。时期
我们不能这样做:
Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
你必须要做
Workbooks books = excel.WorkBooks; Workbook book = books[1]; Sheets sheets = book.WorkSheets; Worksheet ws = sheets[1];
然后释放对象。
我认为其中某些只是框架处理Office应用程序的方式,但我可能是错的。有时,某些应用程序会立即清理进程,而另一些日子似乎要等到应用程序关闭。总的来说,我放弃关注细节,只是确保一天结束时没有其他多余的过程。
另外,也许我正在简化事情,但是我认为我们可以...
objExcel = new Excel.Application(); objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); DoSomeStuff(objBook); SaveTheBook(objBook); objBook.Close(false, Type.Missing, Type.Missing); objExcel.Quit();
就像我之前说的,我不倾向于关注Excel进程何时出现或者消失的细节,但这通常对我有用。除了最短的时间之外,我也不希望保留Excel进程,但是我可能对此感到偏执。
正如其他人指出的那样,我们需要为我们使用的每个Excel对象创建一个显式引用,并对该引用调用Marshal.ReleaseComObject,如这篇知识库文章中所述。我们还需要使用try / finally以确保始终调用ReleaseComObject,即使抛出异常也是如此。 IE。代替:
Worksheet sheet = excelApp.Worksheets(1) ... do something with sheet
我们需要执行以下操作:
Worksheets sheets = null; Worksheet sheet = null try { sheets = excelApp.Worksheets; sheet = sheets(1); ... } finally { if (sheets != null) Marshal.ReleaseComObject(sheets); if (sheet != null) Marshal.ReleaseComObject(sheet); }
如果要关闭Excel,还需要在释放Application对象之前调用Application.Quit。
如我们所见,一旦我们尝试做任何中等复杂的事情,这很快就会变得非常笨拙。我已经使用简单的包装器类成功开发了.NET应用程序,该包装器类包装了Excel对象模型的一些简单操作(打开工作簿,写入范围,保存/关闭工作簿等)。包装器类实现IDisposable,在其使用的每个对象上仔细实现Marshal.ReleaseComObject,并且不会向应用程序的其余部分公开公开任何Excel对象。
但是,这种方法无法很好地满足更复杂的需求。
这是.NET COM Interop的一大缺陷。对于更复杂的方案,我将认真考虑使用VB6或者其他不受管理的语言编写ActiveX DLL,我们可以将与Office这类出站COM对象的所有交互委派给该ActiveX DLL。然后,我们可以从.NET应用程序中引用此ActiveX DLL,这将使事情变得容易得多,因为我们只需要释放这一引用即可。
实际上,我们可以干净地释放Excel Application对象,但必须小心。
关于为我们访问的每个COM对象绝对维护一个命名引用,然后通过Marshal.FinalReleaseComObject()显式释放它的建议在理论上是正确的,但是不幸的是,在实践中很难管理。如果有人滑到任何地方并使用"两个点",或者通过"对于每个"循环或者其他类似类型的命令迭代单元,那么我们将拥有未引用的COM对象,并可能会死机。在这种情况下,将无法在代码中找到原因。我们将不得不仔细检查所有代码并希望找到原因,对于大型项目而言,这几乎是不可能的。
好消息是,我们实际上不必维护对所使用的每个COM对象的命名变量引用。相反,请先调用GC.Collect()然后再调用GC.WaitForPendingFinalizers()释放所有我们不持有引用的(通常是次要的)对象,然后显式释放我们持有命名对象的对象。变量参考。
我们还应该以相反的顺序释放命名的引用:首先是范围对象,然后是工作表,工作簿,最后是Excel Application对象。
例如,假设我们有一个名为xlRng的Range对象变量,一个名为xlSheet的工作表变量,一个名为xlBook的工作簿变量和一个名为xlApp的Excel Application变量,那么清理代码可能类似于下列的:
// Cleanup GC.Collect(); GC.WaitForPendingFinalizers(); Marshal.FinalReleaseComObject(xlRng); Marshal.FinalReleaseComObject(xlSheet); xlBook.Close(Type.Missing, Type.Missing, Type.Missing); Marshal.FinalReleaseComObject(xlBook); xlApp.Quit(); Marshal.FinalReleaseComObject(xlApp);
在大多数代码示例中,我们会看到从.NET清除COM对象,对GC.Collect()和GC.WaitForPendingFinalizers()的调用是两次TWICE,如下所示:
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); GC.WaitForPendingFinalizers();
但是,除非我们使用的是Visual Studio Tools for Office(VSTO),否则它不是必需的,因为Visual Studio Tools for Office(VSTO)使用的终结器会使整个对象图在终结队列中得到提升。在下一次垃圾回收之前,不会释放此类对象。但是,如果我们不使用VSTO,则应该只能调用一次GC.Collect()和GC.WaitForPendingFinalizers()。
我知道显式地调用GC.Collect()
是不行的(当然,重复两次听起来很痛苦),但是说实话,这是没有办法的。通过正常的操作,我们将生成隐藏的对象,这些对象没有引用,因此只能通过调用GC.Collect()来通过其他任何方式释放。
这是一个复杂的主题,但实际上仅此而已。一旦为清理程序建立了此模板,就可以正常编写代码,而无需包装器等。:-)
我在这里有一个教程:
使用VB.Net/COM Interop自动化Office程序
它是为VB.NET编写的,但不要因此而推迟,其原理与使用C#时完全相同。
我们需要注意,Excel也对我们所运行的文化非常敏感。
我们可能会发现需要在调用Excel函数之前将区域性设置为EN-US。
这不适用于所有功能,但不适用于所有功能。
CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); System.Threading.Thread.CurrentThread.CurrentCulture = en_US; string filePathLocal = _applicationObject.ActiveWorkbook.Path; System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;
即使我们使用的是VSTO,这也适用。
有关详细信息:http://support.microsoft.com/default.aspx?scid=kb;zh-cn;Q320369
我找到了一个有用的通用模板,该模板可以帮助实现COM对象的正确处理模式,这些COM对象在超出范围时需要调用Marshal.ReleaseComObject:
用法:
using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application())) { try { using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing))) { // do something with your workbook.... } } finally { excelApplicationWrapper.ComObject.Quit(); } }
模板:
public class AutoReleaseComObject<T> : IDisposable { private T m_comObject; private bool m_armed = true; private bool m_disposed = false; public AutoReleaseComObject(T comObject) { Debug.Assert(comObject != null); m_comObject = comObject; } #if DEBUG ~AutoReleaseComObject() { // We should have been disposed using Dispose(). Debug.WriteLine("Finalize being called, should have been disposed"); if (this.ComObject != null) { Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName)); } //Debug.Assert(false); } #endif public T ComObject { get { Debug.Assert(!m_disposed); return m_comObject; } } private string ComObjectName { get { if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook) { return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name; } return null; } } public void Disarm() { Debug.Assert(!m_disposed); m_armed = false; } #region IDisposable Members public void Dispose() { Dispose(true); #if DEBUG GC.SuppressFinalize(this); #endif } #endregion protected virtual void Dispose(bool disposing) { if (!m_disposed) { if (m_armed) { int refcnt = 0; do { refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject); } while (refcnt > 0); m_comObject = default(T); } m_disposed = true; } } }
参考:
http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/
更新:添加了Ccode,并链接到Windows作业
我花了一些时间试图解决这个问题,当时XtremeVBTalk是最活跃和响应最快的。这是我的原始文章的链接,即使应用程序崩溃了,也要彻底关闭Excel Interop进程。下面是该帖子的摘要,以及将代码复制到了该帖子中。
- 在大多数情况下,使用Application.Quit()和Process.Kill()关闭Interop进程是有效的,但是如果应用程序灾难性地崩溃,则失败。 IE。如果应用程序崩溃,则Excel进程仍将无法运行。
- 解决方案是让操作系统使用Win32调用通过Windows Job Objects处理进程的清理。当主应用程序死亡时,关联的进程(即Excel)也将终止。
我发现这是一个干净的解决方案,因为操作系统正在做清理工作。我们要做的就是注册Excel流程。
Windows作业代码
包装Win32 API调用以注册Interop进程。
public enum JobObjectInfoType { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, BasicUIRestrictions = 4, EndOfJobTimeInformation = 6, ExtendedLimitInformation = 9, SecurityLimitInformation = 5, GroupInformation = 11 } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; public int bInheritHandle; } [StructLayout(LayoutKind.Sequential)] struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public Int64 PerProcessUserTimeLimit; public Int64 PerJobUserTimeLimit; public Int16 LimitFlags; public UInt32 MinimumWorkingSetSize; public UInt32 MaximumWorkingSetSize; public Int16 ActiveProcessLimit; public Int64 Affinity; public Int16 PriorityClass; public Int16 SchedulingClass; } [StructLayout(LayoutKind.Sequential)] struct IO_COUNTERS { public UInt64 ReadOperationCount; public UInt64 WriteOperationCount; public UInt64 OtherOperationCount; public UInt64 ReadTransferCount; public UInt64 WriteTransferCount; public UInt64 OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UInt32 ProcessMemoryLimit; public UInt32 JobMemoryLimit; public UInt32 PeakProcessMemoryUsed; public UInt32 PeakJobMemoryUsed; } public class Job : IDisposable { [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr CreateJobObject(object a, string lpName); [DllImport("kernel32.dll")] static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); private IntPtr m_handle; private bool m_disposed = false; public Job() { m_handle = CreateJobObject(null, null); JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); info.LimitFlags = 0x2000; JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); extendedInfo.BasicLimitInformation = info; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) throw new Exception(string.Format("Unable to set information. Error: {0}", Marshal.GetLastWin32Error())); } #region IDisposable Members public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion private void Dispose(bool disposing) { if (m_disposed) return; if (disposing) {} Close(); m_disposed = true; } public void Close() { Win32.CloseHandle(m_handle); m_handle = IntPtr.Zero; } public bool AddProcess(IntPtr handle) { return AssignProcessToJobObject(m_handle, handle); } }
关于构造函数代码的注意事项
- 在构造函数中,将调用" info.LimitFlags = 0x2000;"。 0x2000是JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,该值由MSDN定义为:
Causes all processes associated with the job to terminate when the last handle to the job is closed.
额外的Win32 API调用以获取进程ID(PID)
[DllImport("user32.dll", SetLastError = true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
使用代码
Excel.Application app = new Excel.ApplicationClass(); Job job = new Job(); uint pid = 0; Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid); job.AddProcess(Process.GetProcessById((int)pid).Handle);
普通开发人员,解决方案都不适合我,
所以我决定实施一个新技巧。
首先让我们指定"我们的目标是什么?" =>"在任务管理器中工作后看不到excel对象"
好的。让no挑战并开始销毁它,但请考虑不要销毁并行运行的其他实例os Excel。
因此,获取当前处理器的列表并获取EXCEL进程的PID,然后在完成工作后,我们在进程列表中有了一个具有唯一PID的新来宾,仅查找并销毁了那个客户机。
<请记住,在excel工作期间,任何新的excel流程都会被检测为新的并销毁>
<更好的解决方案是捕获新创建的excel对象的PID并销毁它>
Process[] prs = Process.GetProcesses(); List<int> excelPID = new List<int>(); foreach (Process p in prs) if (p.ProcessName == "EXCEL") excelPID.Add(p.Id); .... // your job prs = Process.GetProcesses(); foreach (Process p in prs) if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id)) p.Kill();
这解决了我的问题,也希望。
前言:我的答案包含两种解决方案,因此阅读时请务必小心,不要错过任何内容。
关于如何使Excel实例卸载,有不同的方法和建议,例如:
- 使用Marshal.FinalReleaseComObject()显式释放每个com对象(不要忘记隐式创建的com对象)。要释放每个创建的com对象,我们可以使用此处提到的2点规则:如何正确清理Excel互操作对象?
- 调用GC.Collect()和GC.WaitForPendingFinalizers()使CLR释放未使用的COM对象*(实际上,它可以工作,有关详细信息,请参见我的第二个解决方案)
- 检查com-server-application是否显示等待用户回答的消息框(尽管我不确定它是否可以阻止Excel关闭,但我听说过几次)
- 将WM_CLOSE消息发送到Excel主窗口
- 在单独的AppDomain中执行与Excel一起使用的功能。有人认为,卸载AppDomain后,Excel实例将关闭。
- 杀死所有在我们的excel互操作代码启动后实例化的excel实例。
但!有时,所有这些选项都无济于事或者不合适!
例如,昨天我发现在我的其中一个函数(与excel一起使用)中,Excel在函数结束后一直运行。我尝试了一切!我彻底检查了整个功能10次,并为所有内容添加了Marshal.FinalReleaseComObject()!我也有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到Excel主窗口。我在一个单独的AppDomain中执行了我的函数,并卸载了该域。没有任何帮助!关闭所有excel实例的选项是不合适的,因为如果用户在我的函数执行期间手动启动另一个Excel实例(也可与Excel一起使用),则该实例也将被我的函数关闭。我敢打赌用户不会满意!因此,说实话,这是一个la脚的选择(没有冒犯的人)。因此,我花了几个小时才找到一个好的解决方案(以我的拙见):通过其主窗口的hWnd杀死excel进程(这是第一个解决方案)。
这是简单的代码:
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); /// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary> /// <param name="hWnd">Handle to the main window of the process.</param> /// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns> public static bool TryKillProcessByMainWindowHwnd(int hWnd) { uint processID; GetWindowThreadProcessId((IntPtr)hWnd, out processID); if(processID == 0) return false; try { Process.GetProcessById((int)processID).Kill(); } catch (ArgumentException) { return false; } catch (Win32Exception) { return false; } catch (NotSupportedException) { return false; } catch (InvalidOperationException) { return false; } return true; } /// <summary> Finds and kills process by hWnd to the main window of the process.</summary> /// <param name="hWnd">Handle to the main window of the process.</param> /// <exception cref="ArgumentException"> /// Thrown when process is not found by the hWnd parameter (the process is not running). /// The identifier of the process might be expired. /// </exception> /// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception> /// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception> /// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception> public static void KillProcessByMainWindowHwnd(int hWnd) { uint processID; GetWindowThreadProcessId((IntPtr)hWnd, out processID); if (processID == 0) throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd"); Process.GetProcessById((int)processID).Kill(); }
如我们所见,根据Try-Parse模式,我提供了两种方法(我认为这是适当的):如果无法杀死Process(例如,该进程不再存在),则一种方法不会引发异常;如果Process没有被杀死,则另一个方法将引发异常。此代码中唯一的弱项是安全权限。从理论上讲,用户可能没有终止该进程的权限,但是在所有情况下,有99.99%的用户都具有该权限。我还用一个来宾帐户进行了测试,它可以完美运行。
因此,使用Excel的代码如下所示:
int hWnd = xl.Application.Hwnd; // ... // here we try to close Excel as usual, with xl.Quit(), // Marshal.FinalReleaseComObject(xl) and so on // ... TryKillProcessByMainWindowHwnd(hWnd);
瞧! Excel已终止! :)
好的,让我们回到第二个解决方案,正如我在文章开头所保证的那样。
第二种解决方案是调用GC.Collect()和GC.WaitForPendingFinalizers()。是的,它们确实有效,但是我们在这里需要小心!
许多人说(我说过),调用GC.Collect()并没有帮助。但这无济于事的原因是,如果仍然有对COM对象的引用! GC.Collect()无效的最常见原因之一是在Debug模式下运行项目。在调试模式下,不再真正引用的对象将在方法结束之前不会被垃圾回收。
因此,如果我们尝试使用GC.Collect()和GC.WaitForPendingFinalizers()并没有帮助,请尝试执行以下操作:
1)尝试在发布模式下运行项目,并检查Excel是否正确关闭
void GenerateWorkbook(...) { ApplicationClass xl; Workbook xlWB; try { xl = ... xlWB = xl.Workbooks.Add(...); ... } finally { ... Marshal.ReleaseComObject(xlWB) ... GC.Collect(); GC.WaitForPendingFinalizers(); } }
2)将使用Excel的方法包装在单独的方法中。
因此,而不是像这样的事情:
void GenerateWorkbook(...) { try { GenerateWorkbookInternal(...); } finally { GC.Collect(); GC.WaitForPendingFinalizers(); } } private void GenerateWorkbookInternal(...) { ApplicationClass xl; Workbook xlWB; try { xl = ... xlWB = xl.Workbooks.Add(...); ... } finally { ... Marshal.ReleaseComObject(xlWB) ... } }
你写:
现在,Excel将关闭=)
public Excel.Style xlStyleHeader = null; private void CreateHeaderStyle() { Excel.Styles xlStyles = null; Excel.Font xlFont = null; Excel.Interior xlInterior = null; Excel.Borders xlBorders = null; Excel.Border xlBorderBottom = null; try { xlStyles = xlWorkbook.Styles; xlStyleHeader = xlStyles.Add("Header", Type.Missing); // Text Format xlStyleHeader.NumberFormat = "@"; // Bold xlFont = xlStyleHeader.Font; xlFont.Bold = true; // Light Gray Cell Color xlInterior = xlStyleHeader.Interior; xlInterior.Color = 12632256; // Medium Bottom border xlBorders = xlStyleHeader.Borders; xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom]; xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium; } catch (Exception ex) { throw ex; } finally { Release(xlBorderBottom); Release(xlBorders); Release(xlInterior); Release(xlFont); Release(xlStyles); } } private void Release(object obj) { // Errors are ignored per Microsoft's suggestion for this type of function: // http://support.microsoft.com/default.aspx/kb/317109 try { System.Runtime.InteropServices.Marshal.ReleaseComObject(obj); } catch { } }
此处接受的答案是正确的,但也请注意,不仅需要避免"两个点"的引用,而且还需要避免通过索引检索的对象。我们也不必等到完成程序清理这些对象后,最好创建函数,尽可能在完成后清理它们。这是我创建的一个函数,用于分配名为xlStyleHeader
的Style对象的一些属性:
注意,我必须将xlBorders [Excel.XlBordersIndex.xlEdgeBottom]
设置为变量,以清理该变量(不是因为两个点所指向的枚举不需要释放,而是因为该对象我指的是确实需要释放的Border对象。
这种事情在标准应用程序中并不是真正必要的,它们可以很好地清理它们自己,但是在ASP.NET应用程序中,即使我们错过了其中之一,无论我们多么频繁地调用垃圾收集器,Excel都会仍在服务器上运行。
在编写此代码时,在监视"任务管理器"时,它需要大量关注细节和许多测试执行,但是这样做省却了我们拼命地搜索代码页面以查找丢失的一个实例的麻烦。这在循环中工作时特别重要,在循环中我们需要释放对象的每个实例,即使对象每次循环都使用相同的变量名。
objExcel = new Excel.Application(); objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
我不能相信这个问题困扰了全世界5年。...如果创建了一个应用程序,则需要先关闭它,然后再删除链接。
objBook.Close(true, Type.Missing, Type.Missing); objExcel.Application.Quit(); objExcel.Quit();
关闭时
当我们新建一个excel应用程序时,它将在后台打开一个excel程序。在释放链接之前,我们需要命令该excel程序退出,因为该excel程序不是我们直接控制的一部分。因此,如果释放链接,它将保持打开状态!
大家好编程~~
For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot objWorkBook.Close 'or whatever FinalReleaseComObject(objWorkBook) objWorkBook = Nothing Next 'The above does not work, and this is the workaround: For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter) objTempWorkBook.Saved = True objTempWorkBook.Close(False, Type.Missing, Type.Missing) FinalReleaseComObject(objTempWorkBook) objTempWorkBook = Nothing Next
段落数量不匹配