QA@IT
この質問・回答は、@ITの旧掲示板からインポートされたものです。

Vb.netでExcelが終了しない

はじめまして。よろしくお願いします。
以前、http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=3895&forum=7
でExcelへの書き込みが増えるとExcelのプロセスが終了しないという書き込みがありましたが、同様の事象がVB.netでもありました。

コードを下記に明記します。
Private Sub a_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles a.Click

    Dim intRowPos As Integer

    Dim strFileName as Strimg = "C:\a.xls"

    Dim blnRet As Boolean

    Dim objExcel As New Excel.Application
    Dim objBooks As New Excel.Workbooks
    Dim objBook As New Excel.Workbook
    Dim objSheets As New Excel.Sheets
    Dim objSheet As New Excel.Worksheet
    Dim objCell As Excel.Range

    objExcel = CreateObject("Excel.Application")
    objBooks = Nothing
    objBook = Nothing
    objSheets = Nothing
    objSheet = Nothing

    Try

        If Dir$(strFileName) = "" Then
            objExcel.Workbooks.Add()
        Else
            objBooks = objExcel.Workbooks
            objBook = objBooks.Open(strFileName)
        End If

        objSheets = objBook.Sheets
        objSheet = objSheets(1)
        objCell = objSheet.Cells

        objCell.Clear()

        ''行の開始位置を設定
        intRowPos = 1

        Do
       if inrRowPos = 10001 Then
    Exit Do
       End if

            objCell(intRowPos, 1) = 1
            objCell(intRowPos, 2) = 2
            objCell(intRowPos, 3) = 3
            objCell(intRowPos, 4) = 4
            objCell(intRowPos, 5) = 5
            objCell(intRowPos, 6) = 6
            objCell(intRowPos, 7) = 7
            objCell(intRowPos, <img src="/bbs/phpBB/images/smiles/icon_cool.gif"> = 8
            objCell(intRowPos, 9) = 9
            objCell(intRowPos, 10) = 10
            objCell(intRowPos, 11) = 11
            objCell(intRowPos, 12) = 12
            objCell(intRowPos, 13) = 13
            objCell(intRowPos, 14) = 14
            objCell(intRowPos, 15) = 15
            objCell(intRowPos, 16) = 16
            objCell(intRowPos, 17) = 17
            objCell(intRowPos, 18) = 18
            objCell(intRowPos, 19) = 19
            objCell(intRowPos, 20) = 20

            intRowPos = intRowPos + 1
        Loop

        objExcel.DisplayAlerts = False

        objBook.SaveAs(strFileName, Excel.XlFileFormat.xlExcel9795)

        objExcel.DisplayAlerts = True

    Catch ex As Exception

        ''Excel操作によるエラー処理
        MsgBox(Err.Description)
        Return 

    End Try

    If (IsNothing(objCell) = False) Then
        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objCell)
        objCell = Nothing
    End If

    If (IsNothing(objSheet) = False) Then
        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objSheet)
        objSheet = Nothing
    End If

    If (IsNothing(objSheets) = False) Then
        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objSheets)
        objSheets = Nothing
    End If

    If (IsNothing(objBook) = False) Then
        ''保存したExcelシートのクローズ
        objBook.Close(False)

        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objBook)
        objBook = Nothing
    End If

    If (IsNothing(objBooks) = False) Then
        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objBooks)
        objBooks = Nothing
    End If

    'Excelを閉じる
    objExcel.Quit()

    'Excelオブジェクトの解放
    If Not objExcel Is Nothing Then
        ''ランタイム呼び出し可能ラッパーの参照カウントをデクリメントする
        System.Runtime.InteropServices.Marshal.ReleaseComObject(objExcel)
        objExcel = Nothing
    End If

    'ガベージコレクションのメモリの解放
    System.GC.Collect()

    'DoEvents を発行
    System.Windows.Forms.Application.DoEvents()

    Return

End Sub

どなたか解決方法をご伝授いただけませんでしょうか?
よろしくお願いします。

ちなみに、開発環境は、Windows2000/Excel2000/Vb.net2003です。

質問者:としちゃん

回答

Dim objExcel As New Excel.Application
objExcel = CreateObject("Excel.Application")

は,

Dim objExcel As Excel.Application
objExcel = New Excel.Application <----- (1)
objExcel = CreateObject("Excel.Application") <----- (2)

のようになるわけだから,(1) と (2) のふたつのインスタンスを作成しています。
この場合,(1)のインスタンス(参照カウント)を開放できなくなってしまいますよね?

なので,New を取って,

Dim objExcel As Excel.Application
objExcel = CreateObject("Excel.Application")

です。

また,これが実際の原因でないとしても,
他でも同様で,
New を宣言時に付けると "既定のExcelのインスタンス" ができてしまうので,

Dim objExcel As New Excel.Application
Dim objBooks As New Excel.Workbooks
Dim objBook As New Excel.Workbook
Dim objSheets As New Excel.Sheets
Dim objSheet As New Excel.Worksheet

でなく

Dim objExcel As Excel.Application
Dim objBooks As Excel.Workbooks
Dim objBook As Excel.Workbook
Dim objSheets As Excel.Sheets
Dim objSheet As Excel.Worksheet

の方がいいですよね?
というか,コードを見た限り,Newする必要はないです。

で,実際の開放は,Excelのアプリだけでいいハズです。

投稿者:yayadon

編集 履歴 (0)

稍丼さんの書き込み (2004-08-15 12:46) より:

で,実際の開放は,Excelのアプリだけでいいハズです。

いえ、ブックやシートも解放の必要があります。

AI-Lightというサイトで、最近「Excelの事でお聞きします」という質問がありました。こちらも参照してみてください。

投稿者:Jitta

編集 履歴 (0)

いえ、ブックやシートも解放の必要があります。

確かにそのようなんですが...

でも,例えば,横着して,

    Dim app As Excel.Application

    app = New Excel.ApplicationClass 
    app.Workbooks.Add()

    For i As Integer = 1 To 100
        app.Workbooks(1).Worksheets(1).Cells(i, 1).Formula _
            = "Hoge" & CStr(i)
    Next

    app.Workbooks(1).Close(False)
    app.Quit()
    System.Runtime.InteropServices. _
        Marshal.ReleaseComObject(app)
    app = Nothing

    GC.Collect()

    MessageBox.Show("終了")

でも,残らないんですよね...

理屈的には,上位オブジェクトさえちゃんと開放すれば,
下位オブジェクトは開放されないとおかしいじゃないかな
という気がするのですが...

投稿者:yayadon

編集 履歴 (0)

今回の件ですが,宣言時に New してしまうと,

Dim objBooks As New Excel.Workbooks

Dim objBooks As Excel.Workbooks
objBooks = New Excel.Workbooks

の意味になり,オートメーションなのに,
Applicationオブジェクトから参照をたどっていないので
既定のExcelのインスタンスができてしまっていて
それが開放されないのが主な原因のような気がします...

投稿者:yayadon

編集 履歴 (0)

理屈的には,上位オブジェクトさえちゃんと開放すれば,

下位オブジェクトは開放されないとおかしいじゃないかな

という気がするのですが...

そう単純な話でもないです。
COM ランタイムはオブジェクトの寿命を参照カウンタベースで管理しますが、現行の .NET の COM Interrop は参照カウンタを真面目に管理していません。
なので、比較的 COM 仕様に素直に従っている Office アプリケーションでは、「下位オブジェクトが居残っているが故に、Quit() してもサーバが終了しない」という事態が発生してしまいます。
IE なんかは、下位オブジェクトの参照が残っていても、Quit() で有無を言わさずサーバが終了するつくりになっています。

_________________// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/

投稿者:渋木宏明(ひどり)

編集 履歴 (0)

みなさん、いろいろとありがとうございました。
また、返事が遅くなりまして申し訳ございません。

実際のところVB6.0までは
Dim objExcel As Excel.Application

objExcel = CreateObject("Excel.Application")
objExcel.WorkBooks(1).Sheets(1).Cells(1,1).Value = 100

objExcel.Quit()
Set objExcel =Nothing

と記述すれば、Excelのプロセスは解放されていたみたいですが、VB.NETだと、「WorkBooks」
「Sheets」「Cells」それぞれに対してのオブジェクトが「objExcel」にひもづくオブジェ
クトとして、自動的に生成し確保されているみたいです。
(暗黙の了解で作成されているため、見た目では判断つかないそうです。)

そのため、ガベージコレクションで、「Excel.Application」オブジェクトを解放しようとし
た際、暗黙的で作られた、「WorkBooks」「Sheets」「Cells」の各オブジェクトは「暗黙
的なオブジェクト=必要なオブジェクト」と判断されるようです。すなわち、「明らかに不
要であるオブジェクト」と判断できないようです。その結果、親である「Excel.Application」
オブジェクトも解放されずに、Excelのプロセスが残ってしまってたみたいですね。

すなわち
Dim objExcel As Excel.Application

Dim objBooks As Excel.Workbooks

Dim objBook As Excel.Workbook

Dim objSheets As Excel.Sheets

Dim objSheet As Excel.Worksheet

Dim objCells As Excel.Range

Dim objCell As Excel.Range

objExcel = CreateObject("Excel.Application")
objBooks = Nothing
objBook = Nothing
objSheets = Nothing
objSheet = Nothing
objCells = Nothing
objCell = Nothing

objBooks = objExcel.Workbooks
objBook = objBooks.Open(strFileName)
objSheets = objBook.Sheets
objSheet = objSheets(1)
objCells = objSheet.Cells

objCell = objSheet.Cells(1, 1)
objCell.Value = 100

System.Runtime.InteropServices.Marshal.ReleaseComObject(objCell)
objCell = Nothing

System.Runtime.InteropServices.Marshal.ReleaseComObject(objCells)
objCells = Nothing

System.Runtime.InteropServices.Marshal.ReleaseComObject(objSheet)
objSheet = Nothing

System.Runtime.InteropServices.Marshal.ReleaseComObject(objSheets)
objSheet = Nothing

objBook.Close(False)
System.Runtime.InteropServices.Marshal.ReleaseComObject(objBook)
objBook = Nothing

System.Runtime.InteropServices.Marshal.ReleaseComObject(objBooks)
objBooks = Nothing

objExcel.Quit()

System.Runtime.InteropServices.Marshal.ReleaseComObject(objExcel)
objExcel = Nothing

System.GC.Collect()

System.Windows.Forms.Application.DoEvents()

といった形で、ワークシート・ブック・セルそれぞれに対して明確的なオブジェクトを参照
して明確なオブジェクトを解放するという手順を踏まえて、行うとプロセスが消えたみた
いです。

また、画面を使ってExcelの操作を行う場合は、実際の解放処理は画面に制御が戻った時な
ので、「DoEvents()」を用いて処理を行ったほうが、確実にプロセスが解放できるみたいです。

稍丼さん・Jittaさん・渋木宏明(ひどり)さん、ありがとうございました。

投稿者:としちゃん

編集 履歴 (0)

横着してもちゃんと消えた環境を書いておきます。

WinXP SP1 & Excel 2002 SP3 & VB.NET 2003

で確認しています。なんとなくですが,
Excelのバージョンよっても微妙に違うということも
なきにしもあらずという気もするんですが...

昨日からいろいろなパターンで検証しましたが,やはり,
上記の環境だと,Excelのインスタンスが残らないようにするには,
VB.NET側で,変数でうけたものは,obj = Nothing は必要ですが,
途中利用?する個別のオブジェクトをわざわざ ReleaseComObject(...) する必要はなく,
GC.Collect() の方が必須条件になっています。

投稿者:yayadon

編集 履歴 (0)

稍丼さんの書き込み (2004-08-18 00:57) より:

Excelのバージョンよっても微妙に違うということも

なきにしもあらずという気もするんですが...

 ひどりさんの、

渋木宏明(ひどり)さんの書き込み (2004-08-17 08:21) より:

COM ランタイムはオブジェクトの寿命を参照カウンタベースで管理しますが、現行の .NET の COM Interrop は参照カウンタを真面目に管理していません

なので、比較的 COM 仕様に素直に従っている Office アプリケーションでは、「下位オブジェクトが居残っているが故に、Quit() してもサーバが終了しない」という事態が発生してしまいます。

太字にしたところが、とっても重要なように思います
 また、ある環境ではOkでも、別の環境ではNGなら、NGな環境をOkにする作りにしておく必要がある、と思います。

投稿者:Jitta

編集 履歴 (0)

GC.Collect() の方がキーになっていると先に書いたんですが,
理由は,GC.Collect() で RCW 自体が開放されて,
握っていたCOMオブジェクトを放すためのようです。

 .NET Client --> RCW --> COMサーバー
                  ↑
          ここの参照は,1止まりなので
          RCWがGCされると,
          .NET Client からの参照数にかかわらず
          自動的にIUnknown::Releaseするため

ということで,GC.Collectは,最終手段としてかなり有効なため
GC.Collectしてもインスタンスが残っている場合は,
まず,暗黙の既定のインスタンスができている可能性が高いです。


また,よく調べてみると,
ReleaseCOMObjectは,
GC.Collect() を呼ぶ必要がないように用意されたということなので,
.ReleaseCOMObject(...) をしつこく呼でいても,
GC.Collect() を最後に呼んでしまうのならば,
苦労した意味がなくなってしまうような気がします。
GC.Collect() を最後に呼ぶのならば,
あまり細かいことを気にしなくて
ただ,GC.Collect() すればいいことになるわけで。


で,ReleaseCOMObject は,
GC.Collectに頼らなくてもいいように用意されたメソッドなのに,
なので,ReleaseCOMObject(...) を小刻みに呼んでいるのに
どうして開放されないのか,再度,調べてみると...

まず,Cells を Range でうけて,開放します。


  略
 Dim wsh As Excel.Worksheet
 Dim rng As Excel.Range

 略

  rng = wsh.Cells

  For i As Integer = 1 To 100
    rng(i, 1).Formula = "Hoge" & CStr(i)
  Next

  System.Runtime.InteropServices.Marshal.ReleaseComObject(rng)
  rng = Nothing

  略


上のパターンだとまだ残ります。

で,
Excelのオブジェクトをオブジェクトブラウザで眺めていたんですが,
rng(i, 1) ですが,よく考えてみると,.Cells( row , col ) は,
Rangeオブジェクトから,
再度,新規のRangeオブジェクトが返ってくる形のようです。

なので,


  略
  Dim wsh As Excel.Worksheet
  Dim rng As Excel.Range

  略

  rng = wsh.Cells

  For i As Integer = 1 To 100
    Dim tmpRange As Excel.Range
    tmpRange = rng(i, 1)
    tmpRange.Formula = "Hoge" & CStr(i)
    System.Runtime.InteropServices.Marshal.ReleaseComObject(tmpRange)
    tmpRange = Nothing
  Next

  System.Runtime.InteropServices.Marshal.ReleaseComObject(rng)
  rng = Nothing

  略


のようにしたらみごと,
GC.Collect() を呼ばないでも
ReleaseCOMOjbectだけで,インスタンスはきれいに消えました。

なので,

  worksheet.Cells( row , col ).Format = "Hoge"

は,

  rng1 = worksheet.Cells
  rng2 = rng1( row , col )
  rng2.Format = "Hoge"

のように分割しないといけないということのようです。

つまり,一度rangeでうけていても,
あるセルを参照するたびに,rangeでうけて,
そして,それを毎回 ReleaseCOMObject( range ) する必要があるということのようです。

ReleaseCOMObjectだけで,消えてくれたので,うれしいのですが,
死ぬほど面倒なので,GC.Collect() が許される状況ならば,
VB/VBA上のコードのように,今までどおり,obj = Nothing するだけにしておいて,
最後に GC.Collect() でまるっとRCWを開放した方が
やっぱり楽かなという感じです...

[ メッセージ編集済み 編集者: 稍丼 編集日時 2004-08-19 03:26 ]

[ メッセージ編集済み 編集者: 稍丼 編集日時 2004-08-19 03:27 ]

[ メッセージ編集済み 編集者: 稍丼 編集日時 2004-08-19 03:28 ]
[ メッセージ編集済み 編集者: 稍丼 編集日時 2004-08-19 03:29 ]

投稿者:yayadon

編集 履歴 (0)

稍丼さん

 詳細にありがとうございます。

 先にリンクをしています、AI-Lightの投稿では、『GC.Collect()をしてもダメだった』とあります。この方の最初の投稿で、.NET側のプログラムで保持している変数は、すべてNothing代入をしているので、参照が残っているという状況ではない、と思います。

 自分がやってみた感想としては、GC.Collect()をすることは絶対で、ReleaseCOMObjectを併用しておく方が安全、ってところでしょうか。ExcelApplicationなどは、CloseしてQuitしてReleaseしておかなければ落ちてくれないように、記憶しています。

投稿者:Jitta

編集 履歴 (0)

自分がやってみた感想としては、GC.Collect()をすることは絶対で、
ReleaseCOMObjectを併用しておく方が安全、ってところでしょうか。

COM Interop( 特に,RCW(Runtime Callable Wrapper)) の実装?が
当てにならない(flawed)という記述を,ひどりさんのレスだけでなく,
調べてみると,海外のMVPの方のブログでも見かけました。
なので,今のところは,
安全な方へ転ぶように書くのが無難なんでしょうね...

投稿者:yayadon

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。