Excelに限らずInterop操作を行う場合には決まり事があります。
COMオブジェクトの参照カウントをきっちり解放する。
これを行わないと、処理が正常に行われていても
プロセスが残ったままになります。
COMの参照カウントを解放するには以下のクラスを利用します。
Marshal.ReleaseComObject(object)
ReleaseComObjectメソッドに該当するCOMオブジェクトを渡すと
そのオブジェクトの分の参照カウントが解放されます。(つまりカウントが減ります。)
最終的に、全COMオブジェクトにて参照カウント数が0になった後で
Excelを終了すると、ちゃんとプロセスが消えてくれる事になります。
でも実際に処理を書いてみると、これがめっちゃ大変です。
参照カウントを減らす為には、利用したオブジェクト全部を保持しておかないと駄目だからです。
Dim xlBooks As Excel.Workbooks = xlsApp.WorkBooks Dim xlBook As Excel.Workbook = xlBooks.Item(1)
上記の部分ですが
xlsApp.Workbooks.Item(1)
ってやってしまうのが普通です。
でも、上記のようにするとプロセスが落ちなくなります。
これは、真ん中のWorkbooksにアクセスした際に暗黙的に参照カウントが
増えているからです。
ちゃんと解放するためには、Workbooksで変数保持、Workbookで変数保持としなければなりません。
当然ブックをとって処理を終了するなんてことはないので、その後にSheetsとって、Sheetとって、Rangeとって・・・と
いう風に雪だるま式に解放用に保持していかなければなりません。
上記の例の言い換えですが、Interop操作を行う上で、決まり事がもう一つ存在することなります。
ドット(.)が二つ続く操作を行う場合は、一つずつ変数に保持して行かなければならない.
考えただけで面倒ですね。実際にちゃんと処理を書くと
じゃんぬねっとさんが書かれている以下の記事のようになります。
- COM オブジェクトの参照カウントを解放する
上記のじゃんぬねっとさんのページはとてもわかりやすく書かれているので必読です。
で、本題ですが、それでもなんとか楽にしたいってので、私の場合は以下のような
クラスを作成して利用しています。
Imports System Imports System.Collections.Generic Imports System.Runtime.InteropServices Imports Excel = Microsoft.Office.Interop.Excel ''' <summary> ''' Excel Interop操作の管理を担当するクラスです。 ''' </summary> Public Class ExcelInteropManager Implements IDisposable #Region "Fields" ''' <summary> ''' アプリケーションオブジェクト ''' </summary> Private _app As Excel.Application ''' <summary> ''' COMオブジェクトのスタック ''' </summary> ''' <remarks>最終的に参照をリリースする際に利用されます。</remarks> Private _comObjectStack As Stack(Of Object) #End Region #Region "Constructors" ''' <summary> ''' デフォルトコンストラクタ ''' </summary> Public Sub New() Me.New(New Excel.Application()) End Sub ''' <summary> ''' コンストラクタ ''' </summary> ''' <param name="app">アプリケーションオブジェクト</param> Public Sub New(ByVal app As Excel.Application) _app = app _comObjectStack = New Stack(Of Object)() End Sub #End Region #Region "Dispose, ReleaseComObject, Quit" ''' <summary> ''' オブジェクトが廃棄される際に呼ばれます。 ''' </summary> ''' <remarks>COMオブジェクトの解放処理が行われます。</remarks> Public Sub Dispose() Implements IDisposable.Dispose ' ' Excelが終了されていない場合はこのタイミングで終了させる. ' If (_app IsNot Nothing) Then Quit() GC.SuppressFinalize(Me) End If End Sub ''' <summary> ''' 内部で保持しているCOMオブジェクトを解放します。 ''' </summary> ''' <remarks> ''' 本メソッドを呼び出すとアプリケーションオブジェクト以外のCOMオブジェクトが解放されます。 ''' ''' デバッグを行う際は、本メソッドの ''' Marshal.ReleaseComObject(comObj) ''' の部分を ''' Console.WriteLine(Marshal.ReleaseComObject(comObj)) ''' などとすると、参照カウント数が出力されるようになります。 ''' 出力値に0以外の数値が表示されている場合は参照が残っています。 ''' </remarks> Public Sub ReleaseComObjects() Defence() ' ' 保持しているCOMオブジェクトを全て解放. ' For Each comObj As Object In _comObjectStack Marshal.ReleaseComObject(comObj) Next ' ' スタックの中身をクリア. ' _comObjectStack.Clear() End Sub ''' <summary> ''' Excelを終了します。 ''' </summary> ''' <remarks> ''' この処理を呼ぶ事によりアプリケーションオブジェクトが解放され、Excelが終了します。 ''' ''' デバッグを行う際は、本メソッドの ''' Marshal.ReleaseComObject(_app) ''' の部分を ''' Console.WriteLine(Marshal.ReleaseComObject(_app)) ''' などとすると、参照カウント数が出力されるようになります。 ''' 出力値に0以外の数値が表示されている場合は参照が残っています。 ''' </remarks> Public Sub Quit() Defence() ' ' 内部保持しているCOMオブジェクトがまだ解放されていない ' 場合はこのタイミングで解放処理を行う。 ' If (_comObjectStack.Count <> 0) Then ReleaseComObjects() End If ' ' Excelを終了. ' _app.Quit() Marshal.ReleaseComObject(_app) _app = Nothing ' ' 出来るだけ早く回収してもらうために ' 強制的にGCを発生させる。 ' GC.WaitForPendingFinalizers() GC.Collect() GC.WaitForPendingFinalizers() GC.Collect() End Sub #End Region #Region "Excel.Application" ''' <summary> ''' アプリケーションオブジェクトを取得します。 ''' </summary> ''' <returns>アプリケーションオブジェクト</returns> Public Function ExcelApplication() As Excel.Application Defence() Return _app End Function #End Region #Region "Excel.Workbooks" ''' <summary> ''' Workbooksを取得します。 ''' </summary> ''' <returns>Workbooksオブジェクト</returns> Public Function WorkBooks() As Excel.Workbooks Defence() Dim books As Excel.Workbooks = _app.Workbooks _comObjectStack.Push(books) Return books End Function #End Region #Region "Excel.Workbook" ''' <summary> ''' Workbookを追加します。 ''' </summary> ''' <param name="workBooks">Workbooksオブジェクト</param> ''' <returns>追加されたWorkbookオブジェクト</returns> Public Function AddWorkBook(ByVal workBooks As Excel.Workbooks) As Excel.Workbook Defence() Dim newBook As Excel.Workbook = workBooks.Add() _comObjectStack.Push(newBook) Return newBook End Function ''' <summary> ''' 指定されたファイルを開き、そのWorkbookオブジェクトを返します。 ''' </summary> ''' <param name="workBooks">Workbooksオブジェクト</param> ''' <param name="fileName">ファイル名</param> ''' <returns>Workbookオブジェクト</returns> Public Function OpenWorkBook(ByVal workBooks As Excel.Workbooks, ByVal fileName As String) As Excel.Workbook Defence() Dim openBook As Excel.Workbook = workBooks.Open(fileName) _comObjectStack.Push(openBook) Return openBook End Function ''' <summary> ''' 指定されたインデックスのWorkbookオブジェクトを取得します。 ''' </summary> ''' <param name="workBooks">Workbooksオブジェクト</param> ''' <param name="index">インデックス</param> ''' <returns>Workbookオブジェクト</returns> Public Function WorkBook(ByVal workBooks As Excel.Workbooks, ByVal index As Integer) As Excel.Workbook Defence() Dim book As Excel.Workbook = workBooks.Item(index) _comObjectStack.Push(book) Return book End Function #End Region #Region "Excel.Sheets" ''' <summary> ''' 指定されたWorkbookのシートオブジェクトを取得します。 ''' </summary> ''' <param name="workBook">Workbookオブジェクト</param> ''' <returns>Sheetsオブジェクト</returns> Public Function Sheets(ByVal workBook As Excel.Workbook) As Excel.Sheets Defence() Dim workSheets As Excel.Sheets = workBook.Worksheets() _comObjectStack.Push(workSheets) Return workSheets End Function #End Region #Region "Excel.Worksheet" ''' <summary> ''' 指定されたインデックスのシートオブジェクトを取得します。 ''' </summary> ''' <param name="sheets">Sheetsオブジェクト</param> ''' <param name="index">インデックス</param> ''' <returns>Worksheetオブジェクト</returns> Public Function Sheet(ByVal sheets As Excel.Sheets, ByVal index As Integer) As Excel.Worksheet Defence() Dim workSheet As Excel.Worksheet = CType(sheets.Item(index), Excel.Worksheet) _comObjectStack.Push(workSheet) Return workSheet End Function ''' <summary> ''' 指定された範囲のRangeオブジェクトを取得します。 ''' </summary> ''' <param name="sheet">Worksheetオブジェクト</param> ''' <param name="range1">範囲開始</param> ''' <param name="range2">範囲終了</param> ''' <returns>Rangeオブジェクト</returns> Public Function Range(ByVal sheet As Excel.Worksheet, ByVal range1 As Excel.Range, ByVal range2 As Excel.Range) As Excel.Range Defence() Dim newRange As Excel.Range = sheet.Range(range1, range2) _comObjectStack.Push(newRange) Return newRange End Function ''' <summary> ''' 全セルを範囲としてRangeオブジェクトを取得します。 ''' </summary> ''' <param name="sheet">Worksheetオブジェクト</param> ''' <returns>Rangeオブジェクト</returns> Public Function Cells(ByVal sheet As Excel.Worksheet) As Excel.Range Defence() Dim allCells As Excel.Range = sheet.Cells() _comObjectStack.Push(allCells) Return allCells End Function ''' <summary> ''' 指定された行と列に該当するセル(Rangeオブジェクト)を取得します。 ''' </summary> ''' <param name="sheet">Worksheetオブジェクト</param> ''' <param name="rowIndex">行インデックス</param> ''' <param name="colIndex">列インデックス</param> ''' <returns>該当セルを表すRangeオブジェクト</returns> Public Function Cell(ByVal sheet As Excel.Worksheet, ByVal rowIndex As Integer, ByVal colIndex As Integer) As Excel.Range Defence() Dim excelRange As Excel.Range = CType(sheet.Cells(rowIndex, colIndex), Excel.Range) _comObjectStack.Push(excelRange) Return excelRange End Function ''' <summary> ''' 指定した範囲に含まれる列全体を取得します。 ''' </summary> ''' <param name="cells">セル範囲</param> ''' <returns>Rangeオブジェクト</returns> Public Function EntireColumn(ByVal cells As Excel.Range) As Excel.Range Defence() Dim entireCols As Excel.Range = cells.EntireColumn _comObjectStack.Push(entireCols) Return entireCols End Function #End Region #Region "Excel.ChartObjects" ''' <summary> ''' ChartObjectsオブジェクトを取得します。 ''' </summary> ''' <param name="sheet">Worksheetオブジェクト</param> ''' <returns>ChartObjectsオブジェクト</returns> Public Function ChartObjects(ByVal sheet As Excel.Worksheet) As Excel.ChartObjects Defence() Dim charts As Excel.ChartObjects = CType(sheet.ChartObjects, Excel.ChartObjects) _comObjectStack.Push(charts) Return charts End Function #End Region #Region "Excel.ChartObject" ''' <summary> ''' ChartObjectオブジェクトを取得します。 ''' </summary> ''' <param name="charts">ChartObjectsオブジェクト</param> ''' <param name="index">インデックス</param> ''' <returns>ChartObjectオブジェクト</returns> Public Function ChartObject(ByVal charts As Excel.ChartObjects, ByVal index As Integer) As Excel.ChartObject Defence() Dim chartObj As Excel.ChartObject = CType(charts.Item(index), Excel.ChartObject) _comObjectStack.Push(chartObj) Return chartObj End Function ''' <summary> ''' 指定されたポジションとサイズで新しいChartObjectを作成します。 ''' </summary> ''' <param name="charts">ChartObjectsオブジェクト</param> ''' <param name="leftPosition">左位置</param> ''' <param name="topPosition">上位置</param> ''' <param name="width">幅</param> ''' <param name="height">高さ</param> ''' <returns>ChartObjectオブジェクト</returns> Public Function AddNewChart(ByVal charts As Excel.ChartObjects, ByVal leftPosition As Double, ByVal topPosition As Double, ByVal width As Double, ByVal height As Double) As Excel.ChartObject Defence() Dim newChart As Excel.ChartObject = charts.Add(leftPosition, topPosition, width, height) _comObjectStack.Push(newChart) Return newChart End Function #End Region #Region "Excel.Chart" ''' <summary> ''' Chartオブジェクトを取得します。 ''' </summary> ''' <param name="chartObject">ChartObjectオブジェクト</param> ''' <returns>Chartオブジェクト</returns> Public Function Chart(ByVal chartObject As Excel.ChartObject) As Excel.Chart Defence() Dim chartObj As Excel.Chart = chartObject.Chart _comObjectStack.Push(chartObj) Return chartObj End Function ''' <summary> ''' 指定されたAxisTypeのAxisオブジェクトを取得します。 ''' </summary> ''' <param name="chart">Chartオブジェクト</param> ''' <param name="axisType">タイプ</param> ''' <returns>Axisオブジェクト</returns> Public Function ChartAxis(ByVal chart As Excel.Chart, ByVal axisType As Excel.XlAxisType) As Excel.Axis Defence() Dim axis As Excel.Axis = CType(chart.Axes(axisType), Excel.Axis) _comObjectStack.Push(axis) Return axis End Function ''' <summary> ''' ChartTitleオブジェクトを取得します。 ''' </summary> ''' <param name="axis">Axisオブジェクト</param> ''' <returns>AxisTitleオブジェクト</returns> Public Function AxisTitle(ByVal axis As Excel.Axis) As Excel.AxisTitle Defence() Dim chartAxisTitle As Excel.AxisTitle = axis.AxisTitle _comObjectStack.Push(chartAxisTitle) Return chartAxisTitle End Function #End Region #Region "Private Methods" ''' <summary> ''' 本オブジェクトが必須としている情報が揃っているか否かを判別します。 ''' </summary> Private Sub Defence() If (_app Is Nothing) Then Throw New ArgumentException("Excel.Applicationが指定されていません。", "app") End If If (_comObjectStack Is Nothing) Then _comObjectStack = New Stack(Of Object)() End If End Sub #End Region End Class
ご覧通り、ただのラッパークラスですw
たいした事はなにもしてなくて、内部で一元管理しているだけです。
上記を利用すると、先に挙げたじゃんぬねっとさんで記述されている
サンプルコードが以下のように書けます。
' ' じゃんぬねっとさんのサンプルと同じような事をExcelInteropManagerで実装. ' Dim manager As ExcelInteropManager = Nothing Try manager = New ExcelInteropManager() ' 警告メッセージなどを表示しないようにする manager.ExcelApplication.DisplayAlerts = False Dim xlBooks As Excel.Workbooks = manager.WorkBooks() Dim xlBook As Excel.Workbook = manager.OpenWorkBook(xlBooks, "c:\test.xlsx") Dim xlSheets As Excel.Sheets = manager.Sheets(xlBook) Dim xlSheet As Excel.Worksheet = manager.Sheet(xlSheets, 1) Dim xlRange As Excel.Range = manager.Cell(xlSheet, 6, 4) manager.ExcelApplication.Visible = True System.Threading.Thread.Sleep(1000) xlRange.Value2 = "あと 1 秒で終了します" System.Threading.Thread.Sleep(1000) xlBook.Close() Finally If (manager IsNot Nothing) Then manager.ReleaseComObjects() manager.Quit() End If End Try
少し見栄え的にましになっているのかもしれません。
難点は、利用する機能をその都度記述していかなければならないって点ですが
実務でExcel Interopする場合、大抵やることは決まっているので
利用する機能を最初に一気に定義してしまえば問題ないかなって思います。
現在は、セルとかの基本操作とチャート周りしか定義していないです。
もし、利用される場合は、これを下地にして、必要な関数を定義していけばいいかと思います。
3〜4プロジェクトくらい経たら、大分関数もふえているのではないでしょうか。
後は、各機能毎にPartialクラスにしてしまうと管理しやすいかもしれません。
ちなみに、VSTOの場合は勝手に参照カウントは管理してくれるので
上記のような問題はありません。