いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

ExcelのInterop操作を管理するクラス (Microsoft.Office.Interop.Excel, COM, Marshal.ReleaseComObject, 解放)

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操作を行う上で、決まり事がもう一つ存在することなります。

ドット(.)が二つ続く操作を行う場合は、一つずつ変数に保持して行かなければならない.


考えただけで面倒ですね。実際にちゃんと処理を書くと
じゃんぬねっとさんが書かれている以下の記事のようになります。


上記のじゃんぬねっとさんのページはとてもわかりやすく書かれているので必読です。


で、本題ですが、それでもなんとか楽にしたいってので、私の場合は以下のような
クラスを作成して利用しています。

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の場合は勝手に参照カウントは管理してくれるので
上記のような問題はありません。