tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトルSeriesCollection.Add後、Excelのタスクが残る
記事No12002
投稿日: 2020/01/27(Mon) 02:39
投稿者OrientalMelody
いつもお世話になっております。

VB.NETとあまり相性の良くない Microsoft.Office.Interop.Excelによる、
Excel操作での質問です。

Excel操作でSeriesCollection.Add後、Excelのタスクが残ってしまい、困っています。

以下のソースは、ExcelのA列をX軸、B列をY軸としたデータでグラフを描くVB.NETによるExcel操作になります。

(1)WindowsFormアプリの新規VBプロジェクトを作成してください。
(2)参照設定で、「Microsoft Excel 14.0 Object Library」を追加してください。
(3)ツールボックスよりButtonオブジェクトをForm1デザイン画面に貼り付けてください。
(4)その後、以下のソースをForm1コードウィンドウに貼り付けてください。


Option Strict Off
Option Explicit On
Imports Microsoft.Office.Interop.Excel
Imports System.Runtime.InteropServices '確実なオブジェクト解放用
Imports System.Linq
Imports System.Collections.Generic
Public Class Form1

    ''' <summary>
    ''' Excelオブジェクトの完全な開放
    ''' </summary>
    ''' <param name="obj">Excelオブジェクト</param>
    Sub ObjRelease(ByRef obj As Object)
        If Not (obj Is Nothing) Then
            Marshal.ReleaseComObject(obj)
            obj = Nothing
            Threading.Thread.Sleep(3) '削除安定待ち時間
        End If
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim xlAppObj As Application = Nothing
        Dim wbksObj As Workbooks = Nothing
        Dim wbkObj As Workbook = Nothing
        Dim wstsObj As Sheets = Nothing
        Dim wstObj As Worksheet = Nothing
        Dim rngObj As Range = Nothing
        Dim shpsObj As Shapes = Nothing
        Dim SeriesCollectionObj As Object = Nothing 'SeriesCollection宣言ではプロパティ代入時になぜかエラー。
        Dim srsObj As Series = Nothing
        Dim vArrayTXT(,) As Object = {{1, 0}, {6, 3}, {7, 4}, {8, 6.7}, {9, 6}}

        Dim shpObj As Shape = Nothing
        Dim chtObj As Chart = Nothing

        Try

            xlAppObj = New Application()
            xlAppObj.Visible = True
            wbksObj = xlAppObj.Workbooks
            wbkObj = wbksObj.Add()
            wstsObj = wbkObj.Worksheets

            wstObj = wstsObj(1)

            rngObj = wstObj.Range("A1:B5")
            rngObj.Value = vArrayTXT
            rngObj.Select()
            shpsObj = wstObj.Shapes
            shpObj = shpsObj.AddChart

            chtObj = shpObj.Chart
            chtObj.ChartType = XlChartType.xlXYScatterLines
            'グラフ内1系列のみではこの方法で問題ないが、2系列目以降ではSeriesCollection.Addコマンドが必要になる。
            'chtObj.SetSourceData(Source:=rngObj)

            SeriesCollectionObj = chtObj.SeriesCollection
            '系列があったら全て削除する
            Do Until SeriesCollectionObj.Count = 0
                ObjRelease(srsObj) '使い回しは解放が必要。
                srsObj = SeriesCollectionObj(1) 'コレクションは代入して消去が必要。
                srsObj.Delete()
            Loop

            'Y軸の値の指定
            ObjRelease(rngObj) '使い回しは解放が必要。
            rngObj = wstObj.Range("B1:B5") 'Y値のみ
            ObjRelease(srsObj) '使い回しは解放が必要。
            'Return 'ここでは成功

            srsObj = SeriesCollectionObj.Add(Source:=rngObj) '---@
            Return 'ここでは失敗
            'X軸の値の指定
            ObjRelease(rngObj) '使い回しは解放が必要。
            rngObj = wstObj.Range("A1:A5") 'X値のみ
            srsObj.XValues = rngObj 'X値のみ

            Return 'ここでは失敗

        Finally
            '各Excelオブジェクトの解放
            ObjRelease(srsObj)
            ObjRelease(SeriesCollectionObj)
            ObjRelease(rngObj)
            ObjRelease(shpsObj)
            ObjRelease(chtObj)
            ObjRelease(shpObj)
            ObjRelease(wstObj)
            ObjRelease(wstsObj)


            '保存ファイル名の生成。実行ファイルフォルダにSaveXL.xlsxとして保存。
            Debug.WriteLine(System.Windows.Forms.Application.StartupPath)
            Dim sSaveXLFile As String = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "SaveXL.xlsx")
            '上書保存
            xlAppObj.DisplayAlerts = False
            Call wbkObj.SaveAs(Filename:=sSaveXLFile, FileFormat:=XlFileFormat.xlWorkbookDefault)
            xlAppObj.DisplayAlerts = True
            wbkObj.Close()

            'その他の上位オブジェクトの解放
            ObjRelease(wbkObj)
            ObjRelease(wbksObj)
            xlAppObj.Quit()
            Debug.WriteLine("ここでExcelが非表示になり、タスクマネージャーのバックグラウンドプロセスリストに「Microsoft Excel」が表示される。")
            ObjRelease(xlAppObj)
            Debug.WriteLine("この時点で「Microsoft Excel」がタスクマネージャーに残っていたら失敗。")
        End Try
    End Sub

End Class


なお、各Excelオブジェクトは最後のFinally句内で
子オブジェクトから親オブジェクトの順にObjRelease関数により解放するようにしています。

このとき、どうしても@の
srsObj = SeriesCollectionObj.Add(Source:=rngObj)
を実施すると、タスクマネージャーで見るとExcelのタスク「Microsoft Excel」が残ってしまいます。

これを実施する前では、タスクは綺麗になくなるのですが、
どのようにしたら、SeriesCollection.Add後もエクセルのタスクが消えるようになるのでしょうか?

実はChartオブジェクト内1系列のみであれば、
chtObj.SetSourceData(Source:=rngObj)
でも問題ないのですが、同じChartオブジェクト内2系列のグラフを表示したいと思っております。

とくにやり方にこだわっていませんので、
別のやり方で逃げる方法のご提案でも結構です。

もし何かお分かりの方がいらっしゃいましたら、
ご教示のほどよろしくお願いいたします。


使用OS:Windows10 64bit
言語  :VB2017
Excel :Excel2010,Excel2013,Excel2016

[ツリー表示へ]
タイトルRe: SeriesCollection.Add後、Excelのタスクが残る
記事No12003
投稿日: 2020/01/27(Mon) 15:03
投稿者魔界の仮面弁士
> '確実なオブジェクト解放用
> ''' Excelオブジェクトの完全な開放

解放(Release)と開放(Open,Free)になっていますよ。


> タスクマネージャーで見るとExcelのタスク「Microsoft Excel」が残ってしまいます。

まずは解放時に、ReleaseComObject の戻り値が 0 になっていることを確認してみてください。
ReleaseComObject は、呼び出すたびに COM の参照カウントを -1 するようになっており、
参照カウントが 0 になったときに、COM オブジェクトが処分されます。

全ての解放処理で ReleaseComObject が 0 を返してくるにも関わらず、
それでも Excel が残ってしまうようであれば、ReleaseComObject に渡し忘れている
COM オブジェクトが残っている、ということを意味します。


一方、ReleaseComObject の戻り値が 1 以上になっている場合、それは
同じ COM オブジェクトへの複数の参照が生成された状態を意味します。

通常、このメソッドが 0 以外を返す状態になることは少ないのですが、
メソッドをレイトバインドで呼び出したときに、そのメソッド引数に対して
COM オブジェクトを Object 型の変数を通じて受け渡した場合、データ型判定時に
内部的な参照カウントが予期せず増大してしまうことがあり、その場合、
ReleaseComObject を 1 回呼び出しただけでは解放されないことがあります。

もしも 0 を返してこなかった場合には、ReleaseComObject の代わりに
FinalReleaseComObject を呼び出せば、そのオブジェクトの参照カウントが一気に 0 になります。



> Do Until SeriesCollectionObj.Count = 0
>     ObjRelease(srsObj) '使い回しは解放が必要。
>     srsObj = SeriesCollectionObj(1) 'コレクションは代入して消去が必要。
>     srsObj.Delete()
> Loop

順番的には

Do Until SeriesCollectionObj.Count = 0
     srsObj = SeriesCollectionObj(1)
     srsObj.Delete()
     ObjRelease(srsObj)
Loop

とするべきでは無いでしょうか?

元のコードだと、最後に取得した srsObj の中身を解放することなく、
ループ後の @ に到達してしまい、同じ変数に対して別のインスタンスへの参照が
セットされてしまい、その前に保持されていたオブジェクトの解放処理が漏れることになりそうです。



=== 以下蛇足 ===


> Option Strict Off
> Option Explicit On
>
> Dim rngObj As Range = Nothing
>
> ObjRelease(rngObj)
>
> Sub ObjRelease(ByRef obj As Object)

上記では、仮引数が As Object 、実引数が As Range となっており、
それぞれ異なるデータ型が受け渡されていることになります。

現状の「Option Strict Off」であればコンパイルは通るのですが、
その場合、メソッドの呼び出し時において
 ObjRelease(wbkObj)
という処理が
 Dim dummy As Object = wbkObj
 ObjRelease(dummy)
 wbkObj = DirectCast(dummy, Workbook)
という処理に置き換えられることになるため、無駄なやりとりが多くなります。

そこで、このようなケースでは
 Sub ObjRelease(ByRef obj As Object)
ではなく、
 Sub ObjRelease(Of T)(ByRef obj As T)
というメソッド定義に変更することをお奨めします。この方が実行効率が良いですし、
「Option Strict On」時にもそのまま使うことができます。


ちなみに、「If Not (obj Is Nothing) Then」というのは古い書き方なので、現在は
英文法的に「If obj IsNot Nothing Then」の方がスマートかと思います。

[ツリー表示へ]
タイトルRe^2: SeriesCollection.Add後、Excelのタスクが残る
記事No12005
投稿日: 2020/01/29(Wed) 02:26
投稿者OrientalMelody
魔界の仮面弁士様

大変ありがとうございます。

FinalReleaseComObject を使えば、カウント数は消えるようですね。

提示したサンプルは、これで完璧にExcelのタスクは消えるようになりました。

具体的には、
ObjRelease関数を以下のように書き換えました。

''' <summary>
''' Excelオブジェクトの完全な解放
''' </summary>
''' <param name="obj">Excelオブジェクト</param>
Sub ObjRelease(ByRef obj As Object)
    If Not (obj Is Nothing) Then
        'Marshal.ReleaseComObject(obj)
        Dim intCountRCW As Integer = Marshal.ReleaseComObject(obj)
        Debug.WriteLine("intCountRCW = {0}", intCountRCW)
        If intCountRCW > 0 Then
            Dim intCountRCWFinal As Integer = Marshal.FinalReleaseComObject(obj)
            Debug.WriteLine("intCountRCWFinal = {0}", intCountRCWFinal)
        End If
        obj = Nothing
        Threading.Thread.Sleep(3) '2019/12/31
    End If
End Sub

提示したサンプルでは、Finally句の
ObjRelease(rngObj)
のところで、
intCountRCW = 1
となりました。

SeriesCollectionObj.Add(Source:=rngObj)
の引数の方だったのですね。

業務アプリにも反映しようとしたのですが、
例えば、今回修正したFinalReleaseComObjectつきのObjRelease関数を使用して、

Dim xlAppObj As Application = Nothing
Dim wbksObj As Workbooks = Nothing
Dim wbkObj As Workbook = Nothing
Dim wstsObj As Sheets = Nothing

xlAppObj = New Application()
xlAppObj.Visible = True
wbksObj = xlAppObj.Workbooks
wbkObj = wbksObj.Add()
wstsObj = wbkObj.Worksheets

Dim wstObj1 as As Worksheet = Nothing
Dim wstObj2 as As Worksheet = Nothing
wstObj1 = wstsObj(1)
wstObj2 = wstsObj(1)
ObjRelease(wstObj1)

wstObj2 .Activate

みたいなことをすると

「System.Runtime.InteropServices.InvalidComObjectException: COM object that has been separated from its underlying RCW cannot be used.
   at System.StubHelpers.StubHelpers.StubRegisterRCW(Object pThis)
   at Microsoft.Office.Interop.Excel._Worksheet.Activate()」
エラーが発生するようになりました。

これは、
wstObj1 を FinalReleaseComObject すると、
wstObj2 の参照も消えてしまうのですね。

Sub ObjRelease(Of T)(ByRef obj As T)
の記述もこれから行ってみます。

とりあえず、ご連絡まで。

[ツリー表示へ]
タイトルRe^3: SeriesCollection.Add後、Excelのタスクが残る
記事No12007
投稿日: 2020/01/29(Wed) 13:06
投稿者魔界の仮面弁士
> ObjRelease関数を以下のように書き換えました。
その実装パターンなら、ReleaseComObject は呼ばずに、
いきなり FinalReleaseComObject を呼ぶだけでも良さそうですね。



> Dim SeriesCollectionObj As Object = Nothing 'SeriesCollection宣言ではプロパティ代入時になぜかエラー。
どのプロパティの代入時に失敗していますか?
(なお、chtObj.SeriesCollection はプロパティではなくメソッドです)


> srsObj = SeriesCollectionObj(1) 'コレクションは代入して消去が必要。
これなんですが、SeriesCollection が IEnumerable であるという点に注意が必要です。

相互運用機能アセンブリの実装如何によっては、上記が最初の系列ではなく、
その次の系列を返してくる可能性があるのです。


つまり、SeriesCollectionObj.Count = 2 な状態においては、マネージ呼び出しによって
 srsObj = SeriesCollectionObj(0) ' 系列1
 srsObj = SeriesCollectionObj(1) ' 系列2
が返されてしまうという事です。最初の要素は 0 から始まります。

なお要素数が 1 の時に SeriesCollectionObj(1) を指定した場合、srsObj は Nothing になります。
この書き方の場合、範囲外の整数を引数に渡した場合に Nothing が返されるためです。

※ Enumerable.ElementAtOrDefault(Of T) 拡張メソッドと同様の動作


一方 Excel VBA で使う時のように、最初の要素が 1 から始まる指定にする場合は、
以下のいずれかの構文が必要となります。
 srsObj = chtObj.SeriesCollection(1)
 srsObj = SeriesCollectionObj.Item(1)
 srsObj = SeriesCollectionObj._Default(1)
 srsObj = CallByName(SeriesCollectionObj, "[DispId=0]", CallType.Method, 1)
 srsObj = CallByName(SeriesCollectionObj, "", CallType.Method, 1)

この書き方の場合、範囲外の整数を引数に渡すと Excel ライブラリから COM 例外が返されます。


> SeriesCollectionObj.Add(Source:=rngObj)
> の引数の方だったのですね。

以下、参照カウントの内部増加を軽減させるための修正案を載せておきます。
一応 Option Strict On にも対応させたうえで、変数のスコープはできるだけ狭くしてあります。


Dim shcolObj As SeriesCollection = DirectCast(chtObj.SeriesCollection(Type.Missing), SeriesCollection)
Do Until shcolObj.Count = 0
    Dim foo As Series = DirectCast(chtObj.SeriesCollection(1), Series)
    foo.Delete()
    ObjRelease(foo)
Loop
Dim rngB1B5 As Range = wstObj.Range("B1:B5")
' Source:=(rngB1B5) でも良いのですが、式として渡していることを明確にするため、
' Source:=CObj(rngB1B5) という回りくどい表現にしています
srsObj = DirectCast(shcolObj.Add(Source:=CObj(rngB1B5)), Series)
ObjRelease(rngB1B5)
ObjRelease(shcolObj)

[ツリー表示へ]