Excel 相互運用オブジェクトを適切にクリーンアップするにはどうすればよいですか?
-
03-07-2019 - |
質問
C# で Excel 相互運用機能を使用しています (ApplicationClass
)そして、finally 句に次のコードを配置しました。
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
このような作品ではありますが、 Excel.exe
Excel を閉じた後でも、プロセスはバックグラウンドで実行されたままです。アプリケーションが手動で閉じられた場合にのみ解放されます。
私の何が間違っているのでしょうか、それとも相互運用オブジェクトが適切に破棄されることを保証する代替手段はあるのでしょうか?
解決
アプリケーションはまだCOMオブジェクトへの参照を保持しているため、Excelは終了しません。
変数に割り当てずに、COMオブジェクトの少なくとも1つのメンバーを呼び出していると思います。
私にとっては、変数に割り当てることなく直接使用した excelApp.Worksheets オブジェクトでした:
Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);
内部でC#が Worksheets COMオブジェクトのラッパーを作成したことを知りませんでした。このオブジェクトは、コードによってリリースされませんでした(私はそれを知らなかったため) Excelはアンロードされませんでした。
このページ。C#でのCOMオブジェクトの使用に関する優れたルールもあります。
COMオブジェクトで2つのドットを使用しないでください。
そのため、上記のことを行う正しい方法は次のとおりです。
Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);
POST MORTEM UPDATE:
私と他の多くの開発者が偶然出会ったtrapについて説明しているので、すべての読者にこのHans Passantの答えを注意深く読んでほしい。数年前にこの回答を書いたとき、デバッガがガベージコレクタに与える影響については知りませんでしたが、間違った結論を導きました。私は歴史のために答えを変更しませんが、このリンクを読んで、しないでください、<!> quot; 2つのドット<!> quot ;: .NETのガベージコレクションについておよび IDisposableでExcel Interopオブジェクトをクリーンアップ
他のヒント
実際には、Excelアプリケーションオブジェクトをきれいに解放できますが、注意する必要があります。
絶対にアクセスするすべてのCOMオブジェクトの名前付き参照を維持し、Marshal.FinalReleaseComObject()
を介して明示的に解放するというアドバイスは、理論上は正しいですが、残念ながら、実際には管理が非常に困難です。どこかでスリップして<!> quot; 2つのドット<!> quot;を使用したり、for each
ループまたはその他の同様のコマンドを介してセルを反復したりすると、参照されていないCOMオブジェクトが存在し、ハングするリスクがあります。この場合、コードで原因を見つける方法はありません。すべてのコードを目で見て確認し、できれば原因を見つける必要があります。これは、大規模なプロジェクトではほとんど不可能な作業です。
良いニュースは、使用するすべてのCOMオブジェクトへの名前付き変数参照を実際に保持する必要がないことです。代わりに、GC.Collect()
を呼び出してからGC.WaitForPendingFinalizers()
を呼び出して、参照を保持していないすべての(通常はマイナーな)オブジェクトを解放し、名前付き変数参照を保持しているオブジェクトを明示的に解放します。
重要な逆順で名前付き参照も解放する必要があります。最初に範囲オブジェクト、次にワークシート、ワークブック、最後にExcelアプリケーションオブジェクトです。
たとえば、xlRng
という名前のRangeオブジェクト変数、xlSheet
という名前のWorksheet変数、xlBook
という名前のWorkbook変数、およびxlApp
という名前のExcelアプリケーション変数があると仮定すると、クリーンアップコードは次のようになります。次のようなもの:
// 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オブジェクトをクリーンアップするために表示されるほとんどのコード例では、<=>および<=>呼び出しは次のように2回行われます。
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
ただし、オブジェクトのグラフ全体をファイナライズキューで昇格させるファイナライザーを使用するVisual Studio Tools for Office(VSTO)を使用している場合を除き、これは必要ありません。そのようなオブジェクトは、 next ガベージコレクションまで解放されません。ただし、VSTOを使用していない場合は、<=>および<=>を一度だけ呼び出すことができます。
<=>を明示的に呼び出すことは無意味である(そして確かに2回それを行うことは非常に苦痛に聞こえます)ことを知っていますが、正直なところ、それを回避する方法はありません。通常の操作では、参照を持たない非表示オブジェクトを生成します。したがって、<=>を呼び出す以外の方法では解放できません。
これは複雑なトピックですが、実際にはこれですべてです。クリーンアップ手順用にこのテンプレートを確立すると、ラッパーなどを必要とせずに通常どおりコーディングできます:-)
これに関するチュートリアルがあります:
VB.Net / COM相互運用機能によるOfficeプログラムの自動化
VB.NET向けに書かれていますが、それで先送りしないでください。原則はC#を使用する場合とまったく同じです。
序文:私の回答には 2 つの解決策が含まれているため、読むときは注意して、何も見逃さないようにしてください。
Excel インスタンスをアンロードするには、次のようなさまざまな方法とアドバイスがあります。
すべてのcomオブジェクトをMarshal.finalReleasecomobject()で明示的にリリースします(暗黙的に作成されたcom-objectsを忘れないでください)。作成されたすべてのCOMオブジェクトをリリースするには、ここに記載されている2つのドットのルールを使用できます。
Excel 相互運用オブジェクトを適切にクリーンアップするにはどうすればよいですか?gc.collect()およびgc.waitforpindingfinalizers()を呼び出して、CLRをUnused com-objects *をリリースするようにします(実際には、詳細については2番目のソリューションを参照してください)
com-server-applicationがユーザーが応答するのを待っているメッセージボックスが表示される可能性があるかどうかを確認すると(Excelが閉じるのを防ぐことができるかどうかはわかりませんが、何度か聞いたことがあります)
wm_closeメッセージをメインのExcelウィンドウに送信します
別のAppDomainでExcelで動作する関数を実行します。一部の人々は、AppDomainがアンロードされたときにExcelインスタンスが閉じられると信じています。
Excel 相互運用コードの開始後にインスタンス化されたすべての Excel インスタンスを強制終了します。
しかし! 場合によっては、これらのオプションがすべて役に立たなかったり、適切ではなかったりすることがあります。
たとえば、昨日、関数の 1 つ (Excel で動作する) で、関数が終了した後も Excel が実行され続けることがわかりました。全部試してみた!関数全体を 10 回徹底的にチェックし、すべてに Marshal.FinalReleaseComObject() を追加しました。GC.Collect() と GC.WaitForPendingFinalizers() もありました。非表示のメッセージボックスを確認しました。WM_CLOSE メッセージを Excel のメイン ウィンドウに送信しようとしました。別の AppDomain で関数を実行し、そのドメインをアンロードしました。何も役に立ちませんでした!すべての Excel インスタンスを閉じるオプションは不適切です。Excel で動作する関数の実行中にユーザーが別の Excel インスタンスを手動で起動すると、そのインスタンスも関数によって閉じられるからです。きっとユーザーは満足しないでしょう!したがって、正直に言って、これはつまらない選択肢です(気分を害さないでください)。そこで、良いものを見つけるまでに数時間を費やしました(私の謙虚な意見です) 解決: ExcelプロセスをメインウィンドウのhWndで強制終了します。 (これが最初の解決策です)。
簡単なコードは次のとおりです。
[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 パターンに従って 2 つのメソッドを提供しました (ここでは適切だと思います)。1 つのメソッドは、プロセスを強制終了できなかった場合 (たとえば、プロセスがもう存在しない場合) に例外をスローしません。もう 1 つのメソッドは、プロセスが強制終了されなかった場合に例外をスローします。このコードの唯一の弱点はセキュリティ権限です。理論的には、ユーザーはプロセスを強制終了する権限を持っていない可能性がありますが、すべての場合の 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);
出来上がり!エクセルが終了してしまいました!:)
さて、投稿の冒頭で約束したように、2 番目の解決策に戻りましょう。2 番目の解決策は、GC.Collect() と GC.WaitForPendingFinalizers() を呼び出すことです。 はい、実際には機能しますが、ここで注意する必要があります。
GC.Collect() を呼び出しても役に立たないと多くの人が言います (そして私も言いました)。しかし、それが役に立たない理由は、COM オブジェクトへの参照がまだ存在する場合です。GC.Collect() が役に立たない最も一般的な理由の 1 つは、プロジェクトをデバッグ モードで実行することです。デバッグ モードでは、実際には参照されなくなったオブジェクトは、メソッドが終了するまでガベージ コレクションされません。
したがって、GC.Collect() と GC.WaitForPendingFinalizers() を試しても役に立たなかった場合は、次のことを試してみてください。
1) プロジェクトをリリース モードで実行して、Excel が正しく終了したかどうかを確認してください。
2) Excel で作業するメソッドを別のメソッドでラップします。したがって、次のようなものの代わりに:
void GenerateWorkbook(...)
{
ApplicationClass xl;
Workbook xlWB;
try
{
xl = ...
xlWB = xl.Workbooks.Add(...);
...
}
finally
{
...
Marshal.ReleaseComObject(xlWB)
...
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
あなたが書く:
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 が終了します =)
更新:C#コードを追加し、Windowsジョブにリンクします
私はこの問題を理解しようといくつかの時間を費やし、そのときXtremeVBTalkが最もアクティブで反応が良かった。元の投稿へのリンクは、 Excel相互運用プロセスを完全に終了する、アプリケーションがクラッシュした場合でも。以下は、投稿の概要と、この投稿にコピーされたコードです。
-
Application.Quit()
およびProcess.Kill()
を使用した相互運用プロセスの終了はほとんどの場合機能しますが、アプリケーションが壊滅的にクラッシュすると失敗します。つまりアプリがクラッシュした場合、Excelプロセスは引き続き実行されません。 - 解決策は、 Win32呼び出しを使用したWindowsジョブオブジェクト。メインアプリケーションが終了すると、関連するプロセス(Excelなど)も終了します。
OSがクリーンアップの実際の作業を行っているため、これはクリーンなソリューションであることがわかりました。必要なことは、Excelプロセスを登録することだけです。
Windowsジョブコード
Win32 API呼び出しをラップして、相互運用プロセスを登録します。
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 として:
ジョブに関連付けられているすべてのプロセスは、 ジョブの最後のハンドルが閉じられます。
追加の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);
これは私が取り組んでいたプロジェクトで機能しました:
excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;
完了したら、Excel COMオブジェクトへのすべて参照をnullに設定することが重要であることを学びました。これには、セル、シートなどが含まれます。
Excel名前空間にあるものはすべてリリースする必要があります。期間
できません:
Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
やらなければならない
Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];
オブジェクトのリリースが続きます。
まず-Excelの相互運用を行うときにMarshal.ReleaseComObject(...)
またはMarshal.FinalReleaseComObject(...)
を呼び出す必要はありません決して。紛らわしいアンチパターンですが、Microsoftを含む、これに関する情報は、.NETからCOM参照を手動でリリースする必要があることを示していますが、これは誤りです。事実は、.NETランタイムとガベージコレクターがCOM参照を正しく追跡してクリーンアップすることです。コードでは、これは、上部の `while(...)ループ全体を削除できることを意味します。
第二に、プロセスの終了時にアウトプロセスCOMオブジェクトへのCOM参照がクリーンアップされるようにしたい場合(Excelプロセスが終了するように)、ガベージコレクターが実行されることを確認する必要があります。 GC.Collect()
およびGC.WaitForPendingFinalizers()
を呼び出して、これを正しく行います。これを2回呼び出すことは安全であり、サイクルも確実にクリーンアップされます(必要かどうかはわかりませんが、これを示す例に感謝します)。
第3に、デバッガーで実行する場合、ローカル参照はメソッドの終了まで人為的に維持されます(したがって、ローカル変数検査が機能します)。そのため、rng.Cells
呼び出しは、同じメソッドからの<=>のようなオブジェクトのクリーニングには効果的ではありません。 COM相互運用を実行するコードをGCクリーンアップから個別のメソッドに分割する必要があります。 (これは、@ nightcoderがここに投稿した回答の一部から、私にとって重要な発見でした。)
したがって、一般的なパターンは次のようになります。
Sub WrapperThatCleansUp()
' NOTE: Don't call Excel objects in here...
' Debugger would keep alive until end, preventing GC cleanup
' Call a separate function that talks to Excel
DoTheWork()
' Now let the GC clean up (twice, to clean up cycles too)
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
GC.WaitForPendingFinalizers()
End Sub
Sub DoTheWork()
Dim app As New Microsoft.Office.Interop.Excel.Application
Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
app.Visible = True
For i As Integer = 1 To 10
worksheet.Cells.Range("A" & i).Value = "Hello"
Next
book.Save()
book.Close()
app.Quit()
' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub
MSDNやStack Overflow(および特にこの質問!)への多くの投稿など、この問題に関する多くの誤った情報と混乱があります。
最終的に詳細な検討と適切なアドバイスを見つけるように私を確信させたのは、ブログ投稿 Marshal.ReleaseComObjectは危険と見なされます と、以前のテストを混乱させていたデバッガーの下で参照が保持されている問題を見つけます。
I 見つかった a 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 /
この問題が5年間世界を悩ませているとは思えません...アプリケーションを作成した場合、リンクを削除する前にまずシャットダウンする必要があります。
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
閉じるとき
objBook.Close(true, Type.Missing, Type.Missing);
objExcel.Application.Quit();
objExcel.Quit();
Excelアプリケーションを新規作成すると、Excelプログラムがバックグラウンドで開きます。リンクをリリースする前に、そのExcelプログラムは直接制御の一部ではないため、そのExcelプログラムを終了するように命令する必要があります。したがって、リンクが解放されても開いたままになります!
みなさん良いプログラミング~~
一般的な開発者、あなたのソリューションはどれも私にとってはうまくいきませんでした。 そこで、新しいトリックを実装することにしました。
まず<!> quot;私たちの目標は何ですか?<!> quot; = <!> gt; <!> quot;タスクマネージャーでのジョブの後にExcelオブジェクトが表示されない<!> quot;
わかりました。挑戦して破壊を開始することはできませんが、並行して実行されているExcelの他のインスタンスを破壊しないことを考慮してください。
したがって、現在のプロセッサのリストを取得し、EXCELプロセスのPIDを取得します。ジョブが完了すると、一意のPIDを持つプロセスリストに新しいゲストが追加され、そのPIDだけが検索および破棄されます。
<!> lt; Excelジョブ中の新しいExcelプロセスは、新しいものとして検出され、破壊されることに注意してください<!> gt; <!> lt;より良い解決策は、新しく作成されたExcelオブジェクトのPIDをキャプチャし、それを破壊することです<!> gt;
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を適切に閉じるには3つの重要なことがあります。
1:作成したExcelアプリケーションへの参照が残っていないことを確認します(とにかく参照を1つだけにしてください。null
に設定します)
2:GC.Collect()
3:ユーザーがプログラムを手動で閉じるか、ExcelオブジェクトでQuit
を呼び出すことにより、Excelを閉じる必要があります。 (GC.WaitForPendingFinalizers()
は、ユーザーがプログラムを閉じようとした場合と同様に機能し、Excelが表示されていない場合でも、未保存の変更がある場合は確認ダイアログを表示します。キャンセルを押すと、Excelは閉じられました。)
1は2より前に発生する必要がありますが、3はいつでも発生する可能性があります。
これを実装する1つの方法は、相互運用Excelオブジェクトを独自のクラスでラップし、コンストラクタで相互運用インスタンスを作成し、DisposeでIDisposableを次のように実装することです
if (!mDisposed) {
mExcel = null;
GC.Collect();
mDisposed = true;
}
これは、プログラムの側面からExcelをクリーンアップします。 Excelが閉じられると(ユーザーが手動で、または<=>を呼び出して)、プロセスは終了します。プログラムが既に閉じられている場合、プロセスは<=>呼び出しで消えます。
(それがどれほど重要かはわかりませんが、<=>呼び出しの後に<=>呼び出しが必要な場合がありますが、Excelプロセスを削除する必要はありません。)
これは何年も問題なく機能しました。ただし、これは機能しますが、実際に機能するには正常に閉じる必要があります。 Excelがクリーンアップされる前にプログラムを中断すると、通常はプログラムのデバッグ中に<!> quot; stop <!> quot;を押すことでexcel.exeプロセスを蓄積します。
Excelが閉じない理由に追加するのは、読み取り、作成時に各オブジェクトに直接参照を作成する場合でも、「For」ループです。
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
私は伝統的に VVSの回答に記載されているアドバイスに従いました。ただし、この回答を最新のオプションで最新に保つために、今後のプロジェクトではすべて<!> quot; NetOffice <!> quot;を使用すると思います。ライブラリ。
NetOffice は、Office PIAの完全な代替品であり、完全にバージョンに依存しません。 .NETでMicrosoft Officeを操作するときにこのような頭痛の種となることが多いクリーンアップを処理できるマネージCOMラッパーのコレクションです。
主な機能は次のとおりです。
- ほとんどバージョンに依存しない(およびバージョンに依存する機能が文書化されている)
- 依存関係なし
- PIAなし
- 登録なし
- VSTOなし
私はプロジェクトとは一切関係ありません。頭痛の激減を本当に感謝しています。
ここで受け入れられている答えは正しいですが、<!> quot; two dot <!> quot;だけでなく、参照は避ける必要がありますが、インデックスを介して取得されるオブジェクトも避ける必要があります。また、これらのオブジェクトをクリーンアップするためにプログラムの処理が完了するまで待つ必要はありません。可能な場合は、オブジェクトの処理が完了したらすぐにクリーンアップする関数を作成することをお勧めします。 xlStyleHeader
:
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 { }
}
それをクリーンアップするために変数にxlBorders[Excel.XlBordersIndex.xlEdgeBottom]
を設定する必要があったことに注意してください(2つのドットのため、リリースする必要のない列挙を参照していますが、参照しているオブジェクトのためtoは、実際には解放する必要があるBorderオブジェクトです)。
この種のことは標準的なアプリケーションでは必要ありません。標準的なアプリケーションでは、後からクリーンアップするのに非常に役立ちますが、ASP.NETアプリケーションでは、これらの1つでも、ガベージコレクターを頻繁に呼び出しても、Excelは引き続きサーバーで実行されます。
このコードを記述するときにタスクマネージャーを監視している間、細部と多くのテストの実行に多くの注意を払う必要がありますが、そうすることで、コードページを必死に検索して見逃したインスタンスを見つける手間が省けます。これは、ループするたびに同じ変数名を使用する場合でも、オブジェクトの各インスタンスを解放する必要があるループで作業する場合に特に重要です。
試した後
- 逆順でCOMオブジェクトをリリースする
- 最後に
GC.Collect()
とGC.WaitForPendingFinalizers()
を2回追加 - 2つ以下のドット
- ワークブックを閉じてアプリケーションを終了
- リリースモードで実行
私にとって有効な最終的な解決策は、1つのセットを移動することです
GC.Collect();
GC.WaitForPendingFinalizers();
次のように、関数の最後にラッパーに追加したもの:
private void FunctionWrapper(string sourcePath, string targetPath)
{
try
{
FunctionThatCallsExcel(sourcePath, targetPath);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
私はこれに正確に従いました...しかし、私はまだ1000回のうち1回の問題に遭遇しました。誰が理由を知っています。ハンマーを引き出す時間...
Excel Applicationクラスがインスタンス化された直後に、作成されたばかりのExcelプロセスを把握します。
excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();
その後、上記のCOMクリーンアップをすべて完了したら、プロセスが実行されていないことを確認します。まだ実行中の場合は、終了します!
if (!process.HasExited)
process.Kill();
<!>#168; <!>#176; <!>#186; <!>#164; <!>#248; <!>#8222; <!>#184; Excel procを撃ち、バブルガムを噛む<!>#184; <!>#8222; <!>#248; <!>#164; <!>#186; <!>#176; <!>#168;
public class MyExcelInteropClass
{
Excel.Application xlApp;
Excel.Workbook xlBook;
public void dothingswithExcel()
{
try { /* Do stuff manipulating cells sheets and workbooks ... */ }
catch {}
finally {KillExcelProcess(xlApp);}
}
static void KillExcelProcess(Excel.Application xlApp)
{
if (xlApp != null)
{
int excelProcessId = 0;
GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
Process p = Process.GetProcessById(excelProcessId);
p.Kill();
xlApp = null;
}
}
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
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;en-us;Q320369
<!> quot; COMオブジェクトで2つのドットを使用しないでください<!> quot; COM参照の漏えいを防ぐための優れた経験則ですが、Excel PIAは一見して明らかなよりも多くの方法で漏えいを引き起こす可能性があります。
これらの方法の1つは、ExcelオブジェクトモデルのCOMオブジェクトのいずれかによって公開されるイベントをサブスクライブすることです。
たとえば、ApplicationクラスのWorkbookOpenイベントにサブスクライブします。
COMイベントに関するいくつかの理論
COMクラスは、コールバックインターフェイスを介してイベントのグループを公開します。イベントをサブスクライブするために、クライアントコードはコールバックインターフェイスを実装するオブジェクトを登録するだけで、COMクラスは特定のイベントに応答してメソッドを呼び出します。コールバックインターフェイスはCOMインターフェイスであるため、イベントハンドラーの(パラメーターとして)受信するCOMオブジェクトの参照カウントを減らすことは、実装オブジェクトの役割です。
Excel PIAがCOMイベントを公開する方法
Excel PIAは、Excel ApplicationクラスのCOMイベントを従来の.NETイベントとして公開します。クライアントコードが .NETイベントにサブスクライブするたびに( 'a'を強調)、PIAはコールバックインターフェイスを実装するクラスの an インスタンスを作成し、Excelに登録します。
したがって、.NETコードからのさまざまなサブスクリプション要求に応じて、多数のコールバックオブジェクトがExcelに登録されます。イベントサブスクリプションごとに1つのコールバックオブジェクト。
イベント処理のためのコールバックインターフェイスは、PIAがすべての.NETイベントサブスクリプション要求のすべてのインターフェイスイベントにサブスクライブする必要があることを意味します。選んで選ぶことはできません。イベントコールバックを受信すると、コールバックオブジェクトは、関連付けられている.NETイベントハンドラーが現在のイベントに関心があるかどうかを確認し、ハンドラーを呼び出すか、サイレントにコールバックを無視します。
COMインスタンスの参照カウントへの影響
これらすべてのコールバックオブジェクトは、(パラメータとして)受信するCOMオブジェクトの参照カウントを、コールバックメソッドのいずれかに対して(暗黙的に無視されるメソッドに対しても)減少させません。 COMオブジェクトを解放するために CLR ガベージコレクターのみに依存しています。
GCの実行は非決定的であるため、これによりExcelプロセスが必要以上に長く保留され、「メモリリーク」の印象が生じる可能性があります。
解決策
現時点での唯一の解決策は、COMクラスのPIA <!>#8217; sイベントプロバイダーを回避し、COMオブジェクトを確定的に解放する独自のイベントプロバイダーを作成することです。
Applicationクラスの場合、これはAppEventsインターフェイスを実装し、 IConnectionPointContainerインターフェース。 Applicationクラス(さらに言えば、コールバックメカニズムを使用してイベントを公開するすべてのCOMオブジェクト)は、IConnectionPointContainerインターフェイスを実装します。
他の人が指摘したように、このKB記事。また、例外がスローされた場合でも、ReleaseComObjectが常に呼び出されるようにtry / finallyを使用する必要があります。つまり代わりに:
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を呼び出す必要もあります。
お分かりのように、これは中程度に複雑なことをしようとするとすぐに非常に扱いにくくなります。 Excelオブジェクトモデルのいくつかの簡単な操作(ワークブックを開く、範囲に書き込む、ワークブックを保存/閉じるなど)をラップする単純なラッパークラスを使用して.NETアプリケーションを正常に開発しました。ラッパークラスはIDisposableを実装し、使用するすべてのオブジェクトにMarshal.ReleaseComObjectを慎重に実装し、Excelオブジェクトをアプリの他の部分に公開することはありません。
しかし、このアプローチは、より複雑な要件にはうまく対応しません。
これは、.NET COM相互運用の大きな欠陥です。より複雑なシナリオでは、OfficeなどのアウトプロセスCOMオブジェクトとのすべての対話を委任できるVB6またはその他のアンマネージ言語でActiveX DLLを書くことを真剣に検討します。その後、.NETアプリケーションからこのActiveX DLLを参照できます。この1つの参照のみを解放するだけで済むので、作業はずっと簡単になります。
上記のすべてが機能しない場合、Excelにシートを閉じる時間を与えてみてください:
app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();
...
FinalReleaseComObject(app);
Excelに関連するすべてのオブジェクトを必ずリリースしてください!
私はいくつかの方法を試して数時間を費やしました。すべてが素晴らしいアイデアですが、私はついに私の間違いを見つけました。すべてのオブジェクトをリリースしないと、上記の方法はどれもあなたの助けにはなりません。必ず範囲1を含むすべてのオブジェクトを解放してください!
Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);
オプションは一緒にこちら。
2つのドットのルールは私にとってはうまくいきませんでした。私の場合、次のようにリソースをクリーニングするメソッドを作成しました。
private static void Clean()
{
workBook.Close();
Marshall.ReleaseComObject(workBook);
excel.Quit();
CG.Collect();
CG.WaitForPendingFinalizers();
}
Word / Excel相互運用アプリケーションの使用には十分注意する必要があります。すべてのソリューションを試した後、まだ多くの<!> quot; WinWord <!> quot;がありました。プロセスはサーバー上で開いたままです(2000人以上のユーザーがいます)。
問題に数時間取り組んだ後、異なるスレッドでWord.ApplicationClass.Document.Open()
を使用して複数のドキュメントを同時に開くと、IISワーカープロセス(w3wp.exe)がクラッシュし、すべてのWinWordプロセスが開いたままになることに気付きました!
だからこの問題に対する絶対的な解決策はないが、 Office Openなどの他の方法に切り替えると思うXML 開発。
COMオブジェクトのリリースに関する優れた記事は、 2.5 COMオブジェクトのリリース (MSDN)。
私が提唱する方法は、Excel.Interop参照が非ローカル変数である場合、それらをNULLにしてから、GC.Collect()
とGC.WaitForPendingFinalizers()
を2回呼び出すことです。ローカルスコープの相互運用変数は自動的に処理されます。
これにより、すべてのCOMオブジェクトの名前付き参照を保持する必要がなくなります。
記事から引用した例を次に示します。
public class Test {
// These instance variables must be nulled or Excel will not quit
private Excel.Application xl;
private Excel.Workbook book;
public void DoSomething()
{
xl = new Excel.Application();
xl.Visible = true;
book = xl.Workbooks.Add(Type.Missing);
// These variables are locally scoped, so we need not worry about them.
// Notice I don't care about using two dots.
Excel.Range rng = book.Worksheets[1].UsedRange;
}
public void CleanUp()
{
book = null;
xl.Quit();
xl = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
これらの単語は記事から直接引用したものです:
ほとんどすべての状況で、RCW参照をnullにしてガベージコレクションを強制すると、適切にクリーンアップされます。 GC.WaitForPendingFinalizersも呼び出すと、ガベージコレクションはできる限り確定的になります。つまり、WaitForPendingFinalizersへの2番目の呼び出しからの戻り時に、オブジェクトがいつクリーンアップされたかを正確に確認できます<!>#8212;別の方法として、Marshal.ReleaseComObjectを使用できます。ただし、この方法を使用する必要はほとんどないことに注意してください。
私の解決策
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
private void GenerateExcel()
{
var excel = new Microsoft.Office.Interop.Excel.Application();
int id;
// Find the Excel Process Id (ath the end, you kill him
GetWindowThreadProcessId(excel.Hwnd, out id);
Process excelProcess = Process.GetProcessById(id);
try
{
// Your code
}
finally
{
excel.Quit();
// Kill him !
excelProcess.Kill();
}
受け入れられた答えは私にはうまくいきませんでした。デストラクタの次のコードが仕事をしました。
if (xlApp != null)
{
xlApp.Workbooks.Close();
xlApp.Quit();
}
System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}
私は現在、Officeの自動化に取り組んでおり、私にとっては常に機能するこのソリューションを見つけました。これは単純で、プロセスを強制終了する必要はありません。
現在アクティブなプロセスをループするだけで、開いているExcelプロセスに何らかの方法で「アクセス」すると、Excelの浮遊インスタンスが削除されます。以下のコードは、名前が「Excel」であるプロセスを単純にチェックし、プロセスのMainWindowTitleプロパティを文字列に書き込みます。このプロセスとの「相互作用」により、WindowsがExcelのフリーズインスタンスに追いつき、中止するように思われます。
アンロードイベントを発生させるため、開発中のアドインが終了する直前に以下のメソッドを実行します。ハングしているExcelのインスタンスを毎回削除します。正直なところ、これがなぜ機能するのか完全にはわかりませんが、私にとってはうまく機能し、二重ドット、Marshal.ReleaseComObject、またはプロセスの強制終了を心配することなく、Excelアプリケーションの最後に配置できます。これがなぜ効果的であるかについての提案に非常に興味があります。
public static void SweepExcelProcesses()
{
if (Process.GetProcessesByName("EXCEL").Length != 0)
{
Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
if (process.ProcessName.ToString() == "excel")
{
string title = process.MainWindowTitle;
}
}
}
}
その一部は、フレームワークが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(オブジェクト)を閉じる方法は重要ではありません。また、どのように開くか、またプロジェクトの種類によっても重要です。
WPFアプリケーションでは、基本的に同じコードが問題なく、またはほとんど問題なく動作しています。
同じExcelファイルが異なるパラメータ値で数回処理されているプロジェクトがあります-例えば汎用リスト内の値に基づいて解析します。
Excel関連のすべての関数を基本クラスに、パーサーをサブクラスに配置します(異なるパーサーは一般的なExcel関数を使用します)。ジェネリックリストの各アイテムに対してExcelを再度開いたり閉じたりしたくないので、基本クラスで一度だけ開き、サブクラスで閉じました。コードをデスクトップアプリケーションに移動するときに問題が発生しました。上記のソリューションの多くを試しました。 GC.Collect()
は以前に実装されていました。提案どおり2回です。
次に、Excelを開くためのコードをサブクラスに移動することにしました。一度だけ開くのではなく、新しいオブジェクト(基本クラス)を作成し、すべてのアイテムに対してExcelを開き、最後に閉じます。パフォーマンスにはいくらかのペナルティがありますが、いくつかのテストに基づいて、Excelプロセスは問題なく終了します(デバッグモード)。したがって、一時ファイルも削除されます。更新を取得する場合はテストを続行し、さらに記述します。
一番下の行は、特に多くのクラスなどがある場合は、初期化コードも確認する必要があります。