tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトルループ処理後にExcelのタスクが残る
記事No12012
投稿日: 2020/02/29(Sat) 19:51
投稿者mogi
vb.netを触り始めて3ヶ月ほどのものです。
いつも参考にさせて頂いております。

Microsoft.Office.Interop.Excelによる、Excel操作での質問です。
以下のようなExcel操作を行った際、Excel終了後もタスクマネージャー上にExcelのタスクが
残ってしまっています。恐らくComObjectの解放関連だと思うのですが、自力では
解決できず質問するに至りました。よろしければお力添えお願いします。

以下のソースは、このような処理を行うプログラムです。
1.対象のExcelBookを開き、1シート目をコピー。
2.コピーしたシートをiシート目として作成
3.iシート目の名前を変更
4.シートに繰り返し回数を書き込み
5.1〜3をi回繰り返し、対象ExcelBookのシートを増やす

---------------------以下コード-----------------------------------

Public xlApp As Microsoft.Office.Interop.Excel.Application
Public xlBooks As Workbooks
Public xlBook As Workbook
Public xlwkSheet As Worksheet
Public xlSheet As Sheets

xlApp = CreateObject("Excel.Application")
xlBooks = xlApp.Workbooks
xlBook = xlBooks.Open("Excelファイルパス")
xlSheet = xlBook.Worksheets
xlwkSheet = DirectCast(xlSheet("Sheet1"),Worksheet)
xlwkSheet.Visible = True

For i = 0 To (指定回数)
      Err.Clear()
      Dim xlwkSheet2 As Worksheet = Nothing
      Dim xlCells As Range = Nothing
      Dim xlRange As Range = Nothing
    
      'シートのコピー
      xlwkSheet2.Copy(After:=xlSheet(i+1))

      'コピーしたエクセルシートに対して名前の変更と書き込み
   xlwkSheet2 = DirectCast(xlSheet("Sheet1 (2)"),Worksheet)
      xlwkSheet2.Name = "A" + i
      xlCells = xlwkSheet2.Cells
      xlRange = DirectCast(xlCells.Item(1,1), Range)
      xlRange.Value = i

     'xlRangeの解放
    If Not xlRange Is Nothing Then 
     System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange)
        xlRange = Nothing
     End If

  'xlCellsの解放
    If Not xlCells Is Nothing Then 
     System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCells)
        xlCells = Nothing
     End If

     'xlwkSheet2の解放
    If Not xlwkSheet2 Is Nothing Then 
     System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet2)
        xlwkSheet2 = Nothing
     End If
  
Next i

'xlwkSheetの解放
If Not xlwkSheet Is Nothing Then 
   System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet)
   System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet)
   xlwkSheet = Nothing
End If

'xlSheetの解放
If Not xlSheet Is Nothing Then 
   System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
   xlSheet = Nothing
End If

'xlBookの解放
If Not xlBook2 Is Nothing Then
   System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
   xlBook = Nothing
End If  

'xlBooksの解放
If Not xlBooks Is Nothing Then 
   System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks)
   xlBooks = Nothing
End If

'xlApp解放
If Not xlApp Is Nothing Then
  Try
    xlApp.Quit()
  Finally
    System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp)
    xlApp = Nothing
  End Try
End If  

----------------------------------------------------------------------
現状の課題点は以下の2点です。
@Copyメソッドの使用後にComのカウントが2になっています。
 原因がわからず、現状付け焼刃の策としてxlwkSheet2の解放を2回行っていますが適切ではないと
 考えています。(以下のURLを参考にさせて頂きました。)
 (https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=32241&forum=7)

Aループの回数を1にしたときは適切にExcelのプロセスが終了されますが、
ループの回数が2以上になったときにExcelのプロセスが残ってしまうことを確認しています。
ループ処理の際に解放すべきオブジェクトを解放出来ていないものと考えていますが原因がわかりません。

おそらく無駄や不適切な処理等あり、可読性や技術的に拙い部分が多々あると思いますが、よろしくお願いします。
 

[ツリー表示へ]
タイトルRe: ループ処理後にExcelのタスクが残る
記事No12013
投稿日: 2020/03/01(Sun) 17:29
投稿者魔界の仮面弁士
> Public xlBooks As Workbooks
> Public xlSheet As Sheets
Workbooks の変数名を Books とするのであれば、
Sheets の変数名もまた、Sheet ではなく Sheets とするべきかと。


> xlApp = CreateObject("Excel.Application")
それは参照設定しない場合の呼び方ですね。
あながち間違いというわけでも無いのですが、特に理由が無ければ
『xlApp = New Excel.Application()』と記述するが望ましいです。

CreateObject ですとインスタンス生成前に CLSID の逆引きが必要になりますし、
戻り値の型が Object 型になってしまうため「Option Strict On」との相性が悪いためです。


それと xlApp を .Visible = False な状態のまま操作しているようですが、
実行環境によっては、バックグラウンド プロセスの実行優先度が
フォアグラウンド プロセスに比べて著しく落ちる可能性があります。
非表示だと遅かった処理が、Visible = True にすることで高速化することも…。


> For i = 0 To (指定回数)
上記がちょっと奇妙に思えました。
回数と称するのであれば、以下のいずれかになるべきかと。

『For i = 1 To (指定回数)』
『For i = 0 To (指定回数) - 1』
『For i = (指定回数) To 1 Step -1』
『For i = (指定回数) - 1 To 0 Step -1』



> Dim xlwkSheet2 As Worksheet = Nothing
> Dim xlCells As Range = Nothing
> Dim xlRange As Range = Nothing
> 'シートのコピー
> xlwkSheet2.Copy(After:=xlSheet(i+1))
上記は明らかに不自然です。
Nothing なオブジェクトに対して Copy メソッドを呼び出そうとすれば、
NullReferenceException の例外になってしまうはずですよね。

もしかして「xlwkSheet.Copy(After:=xlSheet(i+1))」の間違い?

それとも例外を握りつぶしている箇所があるのでしょうか?
(Try 句と Err オブジェクトが共存している点も気にかかるところ)

あるいは実際のコードでは、変数 xlwkSheet2 に対して、
あらかじめ有効なインスタンスがセットしてあるのでしょうか?

もしも、xlwkSheet2 のインスタンスがセット済みだとしても、渡すべき引数が
> xlwkSheet2.Copy(After:=xlSheet(i+1))
になっていては、引数に渡した COM オブジェクトを解放できなくなるという問題があります。

引数に渡す COM オブジェクトも変数に受け、解放するようにしてください。
正しい Copy 手順については、次の投稿にて後述します。

また、xlwkSheet2 にインスタンス変数がセットされていたのだとすれば、
その変数が参照していた直前のインスタンスを解放することなく、
> xlwkSheet2 = DirectCast(xlSheet("Sheet1 (2)"),Worksheet)
のように、別のインスタンスへの参照に上書きしてしまうのも、あまりに乱暴です。
参照先を書き換える場合は、事前に参照していたインスタンスの解放も忘れずに。


> xlwkSheet2.Name = "A" + i
上記は「コンパイルエラー」になってしまい、実行すらできないはず…。
どのバージョンの VB をお使いでしょうか?

もしもコンパイル時のオプション設定が
 Option Strict Off
 Option Infer Off
 Option Explicit Off
になっていれば、コンパイルだけは一応通るのですが、それでも実行すれば
InvalidCastException の例外を引き起こしてしまうと思います。


> System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet)
> System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet)
そもそも、ループ中で一回も xlwkSheet が登場していないのですが…?

それと、参照カウントが 0 になるまで解放することを目的とするのであれば、
ReleaseComObject を複数回呼び出すかわりに、
FinalReleaseComObject を呼ぶ方が、コードの意図が分かり易くなりますよ。
(まぁ、参照カウントが増えすぎてしまうこと自体、そもそも望ましくないのですけれども)


> @Copyメソッドの使用後にComのカウントが2になっています。
> Aループの回数を1にしたときは適切にExcelのプロセスが終了されますが、

提示頂いたコードが不完全なので確証は持てませんが、根本的には
Sheet 型のインスタンスを保持・解放し忘れているのが原因だと思います。

特に、xlSheet(i+1) の戻り値は、As Object であることも要因の一つです。
また、「COM オブジェクトを引数に取るメソッド」に対して、
明示的な型ではなく、汎用の Object 型を通じて引数を渡した場合、
内部的な型判定の際に参照カウントが増加しやすくなるという傾向があります。

引数として COM オブジェクトを引き渡す必要がある場合は、
「そのインスタンスを変数に保持しておく」
「Object 型ではなく、正しい型で渡すようにする」
「引数に渡し終わった後は、きちんと解放する」の 3 つを守りましょう。

[ツリー表示へ]
タイトルRe^2: ループ処理後にExcelのタスクが残る
記事No12014
投稿日: 2020/03/01(Sun) 17:40
投稿者魔界の仮面弁士
> もしも、xlwkSheet2 のインスタンスがセット済みだとしても、渡すべき引数が
>> xlwkSheet2.Copy(After:=xlSheet(i+1))
> になっていては、引数に渡した COM オブジェクトを解放できなくなるという問題があります。
> 引数に渡す COM オブジェクトも変数に受け、解放するようにしてください。
> 正しい Copy 手順については、次の投稿にて後述します。

というわけで、引数のインスタンスも解放されるよう、下記に修正案を示します。
ここでは 2 種類の実装例を用意してみました。

【パターン1】は
 i = 0 の時は、xlSheet(1) の直後に、xlwkSheet2 のコピーを挿入
 i = 1 の時は、「i=0 でコピーしたシート」の直後に、xlwkSheet2 のコピーを挿入
 i = 2 の時は、「i=1 でコピーしたシート」の直後に、xlwkSheet2 のコピーを挿入
という場合であり、【パターン2】方は
 i = 0 の時は、xlSheet("Sheet1") の直後に、xlwkSheet2 のコピーを挿入
 i = 1 の時は、「i=0 でコピーしたシート」の直後に、xlwkSheet2 のコピーを挿入
 i = 2 の時は、「i=1 でコピーしたシート」の直後に、xlwkSheet2 のコピーを挿入
という場合の記述方法です。


元々の指定方法だと、元のワークブック構成によっては、
xlSheet(i+1) が指し示すシートが "Sheet1" である可能性もあれば
xlSheet(i+1) と "Sheet1" が別物という可能性もあったため、
後者では明示的に、"Sheet1" を基準とするよう記述してみました。


【パターン1】
For i = 0 To …
  xlwkSheet2 = …
  Dim xlWorksheet1 = DirectCast(xlSheet(i + 1), Worksheet)
  xlwkSheet2.Copy(After:=xlWorksheet1)
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorksheet1)
  Dim xlWorksheet2 = DirectCast(xlSheet(i + 2), Worksheet)
  xlWorksheet2.Name = "新しい名前"
   :
   :
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorksheet2)
   :
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet2)
Next


【パターン2】
xlwkSheet = DirectCast(xlSheet("Sheet1"), Worksheet)
For i = 0 To …
  xlwkSheet2 = …
  Dim xlWorksheet1 = DirectCast(xlSheet(xlwkSheet.Index + i), Worksheet)
  xlwkSheet2.Copy(After:=xlWorksheet1)
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorksheet1)
  Dim xlWorksheet2 = DirectCast(xlSheet(xlwkSheet.Index + i + 1), Worksheet)
  xlWorksheet2.Name = "新しい名前"
   :
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlwkSheet2)
   :
  System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorksheet2)
Next



上記では、ワークシートの選択方法も見直しています。
元のコードにあった "Sheet1 (2)" という名前は
「コピー時に生成された名前」を指しているのですよね。

そして元のコードでは、ループに入る前には
 xlwkSheet = DirectCast(xlSheet("Sheet1"),Worksheet)
という「固定的な名前」で指定しているのに対し、ループ内のコピー時には
 xlwkSheet2.Copy(After:=xlSheet(i+1))
という「数値での指定」に変わっており、さらにコピー後には、
 xlwkSheet2 = DirectCast(xlSheet("Sheet1 (2)"),Worksheet)
という「コピー時に生成されたであろう動的な名前」で指定されており、
指定方法に統一性が無く、曖昧に見えます

コピー後に自動生成される名前は、同一名があればさらに別の名になるため不確実です。
After:= や Copy:= は、コピー先の位置が明確になっているわけですから、
Index で指定した方が確実だと思いますので、その点を書き換えてみました。



なお上記では、xlwkSheet2 のインスタンスが xlwkSheet とは異なるシートという想定で記述しています。

[ツリー表示へ]
タイトルRe^2: ループ処理後にExcelのタスクが残る
記事No12015
投稿日: 2020/03/02(Mon) 19:15
投稿者mogi
魔界の仮面弁士さん
アドバイスありがとうございます。
返信が遅くなり申し訳ありません。

> > Dim xlwkSheet2 As Worksheet = Nothing
> > Dim xlCells As Range = Nothing
> > Dim xlRange As Range = Nothing
> > 'シートのコピー
> > xlwkSheet2.Copy(After:=xlSheet(i+1))
> 上記は明らかに不自然です。
> Nothing なオブジェクトに対して Copy メソッドを呼び出そうとすれば、
> NullReferenceException の例外になってしまうはずですよね。
>
> もしかして「xlwkSheet.Copy(After:=xlSheet(i+1))」の間違い?

すみません、これは記述ミスでご推察の通り「xlwkSheet.Copy(After:=xlSheet(i+1))」の間違いです。

> > xlwkSheet2.Name = "A" + i
> 上記は「コンパイルエラー」になってしまい、実行すらできないはず…。
> どのバージョンの VB をお使いでしょうか?

こちらも記述ミスです。実際は違う値が入力され、実行できるものになっています。
省略できる部分を省略しようと思ったのですが、当方の知識不足で混乱させてしまい申し訳ありません。
  
> 提示頂いたコードが不完全なので確証は持てませんが、根本的には
> Sheet 型のインスタンスを保持・解放し忘れているのが原因だと思います。
>
> 特に、xlSheet(i+1) の戻り値は、As Object であることも要因の一つです。
> また、「COM オブジェクトを引数に取るメソッド」に対して、
> 明示的な型ではなく、汎用の Object 型を通じて引数を渡した場合、
> 内部的な型判定の際に参照カウントが増加しやすくなるという傾向があります。

原因はご指摘の通り、xlSheet(i+1)の戻り値がAs Objectであるためでした。
(参考したサイトにも同じことが書かれていたのにも関わらず恥ずかしい限りです。)

>引数に渡す COM オブジェクトも変数に受け、解放するようにしてください。
>正しい Copy 手順については、次の投稿にて後述します。

書いていただいたコードを参考にし、xlSheet(i+1)をWorkSheet型にキャストし代入の後、
Copyメソッドを実行することでExcelのプロセスが残ることなく終了するようになりました。
原因はxlSheet(i+1)がObject型になることを知らなったことです。
また、確かにご指摘の通り可読性の面から変数名も良くないと感じたため訂正致します。

> 引数として COM オブジェクトを引き渡す必要がある場合は、
> 「そのインスタンスを変数に保持しておく」
> 「Object 型ではなく、正しい型で渡すようにする」
> 「引数に渡し終わった後は、きちんと解放する」の 3 つを守りましょう。

漸くこの辺が理解できて来たと思った矢先にこのバグでした。
改めて意識するように努めます。

最後になりますが、今回はこちら側が提示したコードが不完全なものにも関わらず
問題を見つけて下さった上に、サンプルコードまで書いて頂き本当にありがとうございました。
大変助かりました。また、掲題の件以外にも細かなアドバイスを下さり参考になりました。
今後も初歩的な部分でお世話になるかもしれませんが、その際はどうかよろしくお願いします。

[ツリー表示へ]