DOBON.NET DOBON.NETプログラミング掲示板過去ログ

vb.netで2つのエクセルファイルを処理する場合の挙動について

環境/言語:[WindowsXP,VB2008,Excel2007]
分類:[.NET]

はじめて投稿します。

2つのエクセルファイルを同時に開き、FileAからFileBにシートをコピーした後、
FileAを終了させて、FileBに書き込みを行ってからFileBを終了する

といった処理を行おうと思っています。
実際に、FileAからFileBに対してシートをコピーし、FileAを終了させて
FileBを保存してから終了する処理を実行したところ、FileBを保存するところで
エラーとなってしまいます。

※エラー内容:
起動されたオブジェクトはクライアントから切断されました。 (HRESULT からの例外: 0x80010108 (RPC_E_DISCONNECTED))

FileAを終了する際に、Booksを解放せず、FileBの保存・終了後
FileAのBooksを開放すると、問題なく処理が実行されます。
なぜこのような現象が起こるのか、教えてください。
以上、宜しくお願いします。

-----以下実際のコード----
Public Sub openExcel(ByVal filePath As String, ByVal tmpPath As String, ByVal sheetName As String)
'テンプレートファイルのオープン
xlApp = New Excel.Application
'xlTmpApp = New Excel.Application
'xlTmpApp.Visible = False
'xlTmpApp.DisplayAlerts = False
'xlTmpBooks = xlTmpApp.Workbooks
xlTmpBooks = xlApp.Workbooks
xlTmpBook = xlTmpBooks.Open(tmpPath)
xlTmpSheets = xlTmpBook.Worksheets
xlTmpSheet = CType(xlTmpSheets.Item(1), Excel.Worksheet)

'xlApp = New Excel.Application
Dim shipGuide As String = IO.Path.Combine(My.Application.Info.DirectoryPath, SHIP_GUIDE_FILE_TMP)
xlApp.Visible = True
xlApp.DisplayAlerts = False
xlBooks = xlApp.Workbooks
xlBook = xlBooks.Open(shipGuide)
'xlBook = xlBooks.Add()
xlSheets = xlBook.Worksheets
xlSheet = CType(xlSheets.Item(xlSheets.Count), Excel.Worksheet)

'テンプレートのシートをコピー
xlTmpSheet.Copy(After:=xlSheet)
closeTmpExcel(True) @引数を省略すると、Aの個所でエラー

closeExcel(filePath)

'xlTmpBooks.Close()
'MRComObject(xlTmpBooks)
End Sub

Public Sub closeTmpExcel(Optional ByVal isBooksClose As Boolean = False)
MRComObject(xlTmpSheet)
MRComObject(xlTmpSheets)
xlTmpBook.Close()
MRComObject(xlTmpBook)
If Not isBooksClose = False Then
xlTmpBooks.Close()
MRComObject(xlTmpBooks)
End If
'xlTmpApp.DisplayAlerts = True
'xlTmpApp.Visible = True
'xlTmpApp.Quit()
'MRComObject(xlTmpApp)
End Sub

Public Sub closeExcel(ByVal saveFile As String)
MRComObject(xlSheet)
MRComObject(xlSheets)
xlBook.SaveAs(saveFile) Aこの個所でエラー
xlBook.Close()
MRComObject(xlBook)
xlBooks.Close()
MRComObject(xlBooks)
xlApp.Visible = True
xlApp.DisplayAlerts = True
xlApp.Quit()
MRComObject(xlApp)
End Sub
■No30892に返信(やむさんの記事)
> MRComObject
自作関数でしょうか。MRComObject に関する説明が抜けていますよ。

名前からして、Marshal.(Final)ReleaseComObjct メソッドを
読んでいるものであろうとは想像がつきますけれどね。


> xlTmpBooks  = xlApp.Workbooks
> xlBooks = xlApp.Workbooks
この 2 つは同じ COM インスタンスへの参照となるでしょうから、
xlTmpBooks を廃して、xlBooks のみで管理してみてください。


> なぜこのような現象が起こるのか、教えてください。
xlTmpBooks と xlBooks は同じインスタンスであるにも関わらず、
MRComObject(xlTmpBooks) を実施しているからでは無いでしょうか。

その後もまだ、xlBook.SaveAs にて Workbook を使用するのですから、
それより前のタイミングで、親オブジェクト(Workbooks や Application 等)の
終了処理を実施するのは避けるべきです。


> FileAを終了する際に、Booksを解放せず
> FileAのBooksを開放すると、
「解放」ですね。開放だと open の意味になってしまいます。

開放は、自身が管理している資源や制限を開け放ち、自由に出入りさせること。
解放は、束縛を解いて自由にすること/管理資源の所有権を破棄し解き放つこと。

図書館の一般カイホウ、校庭の一時カイホウなどは前者で、
人質のカイホウ、ストレスからのカイホウなどは後者です。


> If Not isBooksClose = False Then
「If isBooksClose Then」の方が素直では?
■No30893に返信(魔界の仮面弁士さんの記事)
> ■No30892に返信(やむさんの記事)
>>MRComObject
> 自作関数でしょうか。MRComObject に関する説明が抜けていますよ。
>
> 名前からして、Marshal.(Final)ReleaseComObjct メソッドを
> 読んでいるものであろうとは想像がつきますけれどね。

はい、魔界の仮面弁士さんのおっしゃる通りです。
その関数内で解放処理を行っています。

>>FileAを終了する際に、Booksを解放せず
>>FileAのBooksを開放すると、
> 「解放」ですね。開放だと open の意味になってしまいます。
>
> 開放は、自身が管理している資源や制限を開け放ち、自由に出入りさせること。
> 解放は、束縛を解いて自由にすること/管理資源の所有権を破棄し解き放つこと。
>
> 図書館の一般カイホウ、校庭の一時カイホウなどは前者で、
> 人質のカイホウ、ストレスからのカイホウなどは後者です。

すいません。変換ミスしてました。
開放ではなく、解放です。

>>If Not isBooksClose = False Then
> 「If isBooksClose Then」の方が素直では?

「コード全体を通じてIf文の判断基準がTrueだったりFalseだったりすると
読みづらくなるので、統一した方が良い」
とこの業界に入った当時教えられたため、癖となっています。
何らかのアラートを表示する場合、Falseを使っているので、Falseで統一しています。

>>xlTmpBooks = xlApp.Workbooks
>>xlBooks = xlApp.Workbooks
> この 2 つは同じ COM インスタンスへの参照となるでしょうから、
> xlTmpBooks を廃して、xlBooks のみで管理してみてください。
>>なぜこのような現象が起こるのか、教えてください。
> xlTmpBooks と xlBooks は同じインスタンスであるにも関わらず、
> MRComObject(xlTmpBooks) を実施しているからでは無いでしょうか。
>
> その後もまだ、xlBook.SaveAs にて Workbook を使用するのですから、
> それより前のタイミングで、親オブジェクト(Workbooks や Application 等)の
> 終了処理を実施するのは避けるべきです。

なるほど。
確かにxlBooksとxlTmpBooksは、同じxlAppを参照しています。
ただ、さらに質問で申し訳ないんですが、
@で引数を省略せず、closeTmpExcel関数でxlTmp...を解放し、
closeExcel関数でxlAppまで解放してしまった後、
openExcel関数の最後での
・xlTmpBooks.Close()
・MRComObject(xlTmpBooks)
はエラーなく実行されました。
エラーが起こらなければむしろおかしいのでは?と思うのですが、
何か違うのでしょうか。
xlAppが解放された状態であっても、xlTmpBooksはcloseして解放するのみなので
問題ないということなんでしょうか。
そもそも、xlAppを解放する前にxlTmpBooksを解放しろって話ですが…
■No30894に返信(やむさんの記事)
> 引数を省略せず、
これは「引数 isBooksClose に True を渡した場合」という意味ですね?


> 確かにxlBooksとxlTmpBooksは、同じxlAppを参照しています。
参照先は、xlApp.Workbooks ですね。
xlBooks Is xlTmpBooks の結果は True でしょうか。


> closeTmpExcel関数でxlTmp...を解放し、
> closeExcel関数でxlAppまで解放してしまった後、
closeTmpExcel は関数ではありませんが、それはさておき。

True / False 時の動作が、自分の予想と真逆なんですよね…。

==============================
closeTmpExcel(isBooksClose As Boolean) に True が渡された場合、
FileA が xlTmpBook.Close() で閉じられた後、さらに追加で
xlTmpBooks.Close() が実行されることになります。

通常はここで、FileB のファイル未保存確認の通知が表示されるところですが、
DisplayAlerts = False のため、両ブックともそのまま閉じられます。

さて、問題はその後の closeExcel(saveFile As String)。

この時点では、FileB が既に閉じられているため、
xlBook.Close() や MRComObject(xlBook) を実施する前であっても、
xlBook.SaveAs(saveFile) や xlBook.Close は
RPC_E_DISCONNECTED になると予想しています。

――しかし、今回はそれがエラーにならなかったのですね?


==============================
一方、引数無しで closeTmpExcel(isBooksClose As Boolean) を
呼び出した場合は、isBooksClose に False が渡されたことを意味しますので、
この場合には Workbooks.Close が実行されず、FileA のみが閉じられますね。

この場合、closeExcel 内の xlBook.SaveAs の行に到達するまでに
下記の状態になると予想します。

 共有物:xlApp       As Application  :生存中

 共有物:xlTmpBooks  As Workbooks    :生存中
 FileA :xlTmpBook   As Workbook     :MRComObject済み
 FileA :xlTmpSheets As Sheets       :MRComObject済み
 FileA :xlTmpSheet  As WorkSheet    :MRComObject済み

 共有物:xlBooks     As Workbooks    :生存中
 FileB :xlBook      As Workbook     :生存中
 FileB :xlSheets    As Sheets       :MRComObject済み
 FileB :xlSheet     As WorkSheet    :MRComObject済み


共有物であるはずの Workbooks 変数が 2 つあるのは不自然ですが、
xlBook.SaveAs の呼び出しについては、特に問題無さそうです。

RPC_E_DISCONNECTED にはなりそうに無いのですが…。不思議。
■No30897に追記(魔界の仮面弁士)
> この時点では、FileB が既に閉じられているため、
> xlBook.Close() や MRComObject(xlBook) を実施する前であっても、
> xlBook.SaveAs(saveFile) や xlBook.Close は
> RPC_E_DISCONNECTED になると予想しています。

この根拠は、Excel VBA での検証結果によるものです。
(VB.NET から呼ぶと解放が面倒なので、先に VBA で検証していました)


>>> ※エラー内容:
>>> 起動されたオブジェクトはクライアントから切断されました。 (HRESULT からの例外: 0x80010108 (RPC_E_DISCONNECTED))

Workbooks.Close 後に Workbook.Close あるいは Workbook.SaveAs を
実行しようとすれば、RPC_E_DISCONNECTED が通知されたので、これが
原因だと思い込んでいたのです。
(やむさんの VB.NET 環境ではエラー無く呼べたみたいですが)

  Dim X As Excel.Application
  Set X = New Excel.Application   '自身とは別の excel.exe を生成
  X.Visible = True

  Dim BS As Excel.Workbooks
  Set BS = X.Workbooks

  Dim B As Excel.Workbook
  Set B = BS.Add()

  X.DisplayAlerts = False
  BS.Close  'これを実行すると、BS.Count は 0 になった

  B.Close   'ここでエラー &H80010108 発生(B.SaveAs を呼んだ場合も同様)
            '『オートメーション エラーです。
            ' 起動されたオブジェクトはクライアントから切断されました。』


> xlAppが解放された状態であっても、xlTmpBooksはcloseして解放するのみなので
> 問題ないということなんでしょうか。

xlAppが解放された段階で、Excel.exe が終了しているかどうかを
タスクマネージャで確認してみてください。生き残っているようであれば、
Close を呼び出せる可能性はありそうですね。

上記 VBA コードの例で恐縮ですが、最後の B.Close の行をコメントにして

  X.DisplayAlerts = False
  BS.Close
  'B.Close
  X.Quit
  Set X = Nothing
  BS.Close

のようにしてみても、二回目の BS.Close は成功したためです。

そもそも、上記で X.Quit を実行した段階では、Excel が
非表示に変更されるだけで、Excel.exe は生き残っていました。

流石に、その excel.exe 自体を強制終了した後では、WS.Close が
『リモート サーバーがないか、使用できる状態ではありません。』
というエラー(Err.Number = 462)になりました。


なお、Quit を呼ぶ前もしくは呼んだ後に
Set BS = Nothing を実行した場合は、Excel.exe も終了します。
ブックが最上位オブジェクトだった時代の名残ですかね…。


>>>> MRComObject
>>> 名前からして、Marshal.(Final)ReleaseComObjct メソッドを
>>> 読んでいるものであろうとは想像がつきますけれどね。
>> はい、魔界の仮面弁士さんのおっしゃる通りです。
>> その関数内で解放処理を行っています。

MRComObject の内部実装がまだ公表されていませんが、
もしも Marshal.FinalReleaseComObject ではなく
Marshal.ReleaseComObject を使用しているのだとしたら、
その時の戻り値もチェックしてみてください。

戻り値が  = 0 になれば正常ですが、< 0 の場合は過解放となりますし、
> 0 の場合は、どこかで参照カウントが内部増加していることになります。

たとえば、Workbooks オブジェクトの参照カウントが残っていれば、
MRComObject(xlBooks) を実施した後でも、xlTmpBooks への操作は
続行できるかと思います。
2012/09/06(Thu) 09:45:37 編集(投稿者)

致命的な間違いをしてました。まずはお詫びを。
魔界の仮面弁士さん申し訳ない。

closeTmpExcel(isBooksClose As Boolean)でエラーの起こるTrue/Falseの引数を
逆で投稿してました。
なんか混乱してます。ちょっと整理させてください。

closeTmpExcel(isBooksClose As Boolean)の引数にTrueを渡した場合
・closeTmpExcel()でxlTmpBooks.Close()とMRComObject(xlTmpBooks)が実行されて
xlTmpBooksが解放される
・closeExcel(ByVal saveFile As String)のxlBook.SaveAs(saveFile)でエラー
・openExcel(...)最終にあるxlTmpBooks.Close()とMRComObject(xlTmpBooks)はコメントアウトしてあるので、実行されません。
※コメントアウトしてあるという事実が抜けてました。

closeTmpExcel(isBooksClose As Boolean)の引数を省略した場合
・closeTmpExcel()でxlTmpBooks.Close()とMRComObject(xlTmpBooks)が実行されないのでxlTmpBooksは生存したまま
・closeExcel(ByVal saveFile As String)でxlApp,xlBooks,xlBook,xlSheets,xlSheetを全て終了
・xlTmpBooksを終了させていないので、openExcel(...)最終にあるxlTmpBooks.Close()とMRComObject(xlTmpBooks)はコメントアウトを解除した状態
→この場合、親であるxlAppが終了されているのに、xlTmpBooks.Close()が実行されてしまってる不思議

以上です。
なお、Falseを渡し(引数を省略)たら、最後のxlTmpBooks
最初に回答して頂いた、xlTmpBooksをなくし、xlBooksのみで試してみたところ、
エラーなく処理が進みましたので、最初の質問に関しては終了なのですが、
追加でアドバイス頂いたことを実際に確認し、結果を書き込んでから
終了したいと思います。

お礼が遅くなってしまいましたが、魔界の仮面弁士さん、ありがとうございます。

追記
イミディエイトウィンドウで
?xlBooks Is xlTmpBooksを実行しましたが、Falseでした。
■No30899に返信(やむさんの記事)
> closeTmpExcel(isBooksClose As Boolean)でエラーの起こるTrue/Falseの引数を
> 逆で投稿してました。
それでようやく話が繋がりました。

エラーコード 0x80010108 (RPC_E_DISCONNECTED) は
 『アプリ側では参照を保持しているが、その参照は既に COM オブジェクト側から切断されている』
という状態を意味します。

このエラーになった理由は、先に Workbooks.Close が実施されることで、
全てのブックが閉じられているのに、その後で FileB のブックすなわち
xlBook を操作しようとしたことが原因です。
xlBook 以外の変数を MRComObject してあるかどうかは無関係です。


一方、xlApp 終了後に xlTmpBooks.Close() が実行できるという追加質問の場合は、
事前に Workbooks.Close や xlApp.Quit が実行されていたかは関係なく、
単純に xlTmpBooks が MRComObject されたかどうかだけが焦点となります。




> イミディエイトウィンドウで
> ?xlBooks Is xlTmpBooksを実行しましたが、Falseでした。

両者が別であるということから、
>→この場合、親であるxlAppが終了されているのに、xlTmpBooks.Close()が実行されてしまってる不思議
の説明が付きそうです。


概念的には、Workbooks オブジェクトは Application 単位で単一のオブジェクトです。
そのため通常は、Workbooks 変数は一つだけ用意すれば十分なはずです。

一方、今回の xlBooks と xlTmpBooks の例を見ても分かるように、このオブジェクトは、
xlApp.Workbooks にアクセスするたびに、異なるインスタンスが取得されています。

別インスタンスであることが、xlApp処分後にxlTmpBooks.Close()を呼び出せてしまった要因となります。
No30898 にも書いたように、Workbooks オブジェクトへの参照が残っている限り、
Excel.exe は終了しないからです。
(勿論、事後操作すべきではありませんし、xlApp より先に解放されるべきですが)


そのため、xlTmpBooks と xlBooks の 2 つの変数に受け取ってしまった場合、
それぞれが別インスタンスなら、両方に対して Marshal.ReleaseComObject せねば
ならないということになります。そうしないと Excel が残ってしまいます。
(同一インスタンスだった場合は、どちらか一方のみを処分します)


ちなみに、Sheets コレクションなども、アクセスするたびに、
異なるインスタンスが取得されるようです。

これが Sheet オブジェクトなどは、何度 xlSheets(1) から取得しても
同じインスタンスが返されてきました。
恐らくは、コレクション系オブジェクトの場合は事情が異なるのでしょう。
No30899の追記の続きです。

xlTmpBooksを最後に解放するまで、Excel.exeはタスクマネージャ上に残っていました。

>概念的には、Workbooks オブジェクトは Application 単位で単一のオブジェクトです。
>そのため通常は、Workbooks 変数は一つだけ用意すれば十分なはずです。
この事を理解していなかった事がそもそもだったのですね…

魔界の仮面弁士さん、長らくお付き合いいただきありがとうございました。

これにて終了とさせていただきます。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板