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

[ 親記事をトピックトップへ ]

このトピックに書きこむ

■35258 / inTopicNo.1)  Re[8]: Excel Com オブジェクトの増殖
  
□投稿者/ たこ 一般人(20回)-(2022/11/28(Mon) 23:06:02)
  • アイコンNo35257に返信(たこさんの記事)

    ちょっとした実験をしてみました。
    --------------------------------------------------------------------
    Dim w As Excel.Worksheets
    Dim s = w.Item(1)     '←WorkSheetオブジェクトのItemなので、当然WorkSheet型だと思っている
    Dim n = CType(s, Excel.Worksheets).name    '←怒られる…
    --------------------------------------------------------------------

    Dim s As Excel.Worksheets = w.Item(1)   '←やっぱり怒られる…
                         'Option Strict On では'Object'から'Worksheets'への暗黙的な変換は許可されていません。
                         'ReadOnly Property Excel.Wroksheets.Item(Index As Object) As Object

    Dim s As Object = w.Item(1)        'これが正解の様だ…
                         'ここは戻り値がObjectなので理解出来る。

    Dim n = CType(s, Object).name       '←怒られる…
                         'Option Strict On では、遅延バインディングを使用できません。
    一体何の型にキャストすればシート名が取得できるのだろう…

    >『.』の連続」が見えにくくなる
    .を分解するのも苦労します(>_<)

    あ…そう言えばと思い。。。

    Dim w As Excel.Worksheets
    Dim s As Object = w.Item(1)
    Dim sh As Excel.Worksheet = CType(s, Excel.Worksheet)
    Dim n As String = sh.Name

    怒られなくなりました!

    この場合、MRComObjectをしなければならないのは…
    w、s、shで合っていますでしょうか^^?


違反を報告
引用返信 削除キー/
■35257 / inTopicNo.2)  Re[7]: Excel Com オブジェクトの増殖
□投稿者/ たこ 一般人(19回)-(2022/11/28(Mon) 22:38:47)
  • アイコンNo35252に返信(魔界の仮面弁士さんの記事)
    No35253に返信(魔界の仮面弁士さんの記事)
    No35255に返信(魔界の仮面弁士さんの記事)
    No35256に返信(radianさんの記事)

    魔界の仮面弁士様、radian様、返信ありがとうございます。

    返信が前後しますが、まずはradian様ご紹介ありがとうございます。
    どうしようも無くなった時は検討させて頂きます(^^ゞ

    余談にはなりますが…
    元々私、プログラミングは全部独学でして、スキルアップの為に今回の質問をさせて頂いています(^^ゞ
    (いつもはどうしようも無くなって質問させて頂いていますが…(^^ゞ)
    まだしっかりと解ってる訳では無いですが、VB6.0からオブジェクトの概念を解る様になるまでは苦労しました^^;

    最終的には会社で5年分くらいExcelで見積書が溜まっていますので、これをデータベース化するのが目的です。。。
    最初はExcel VBAで5年分の見積書の内容をまとめようかと思っていましたが、クラスの概念を覚えてしまうとクラスを使った方が楽なので(^^ゞ
    VB6.0やVBAにもクラスはありますが、使い方が良く解りません…
    …と言う訳でVB.NETで…(^^ゞ

    お気持ちは有難く頂戴致しました。
    ありがとうございます。



    今回、いろいろ調べてやっとExcelのオブジェクトモデルを発見出来ました。
    https://learn.microsoft.com/ja-jp/office/vba/api/overview/excel/object-model
    VBAと同じなのですね…
    当たり前と言えば当たり前ですが…

    Option Strict Onも調べました…
    https://atmarkit.itmedia.co.jp/fdotnet/vb6tonet/vb6tonet26/vb6tonet26_03.html

    DirectCast、CTypeの違いも調べました…
    http://vb.navi-ch.net/2015/07/18/post-118/
    https://learn.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/operators/directcast-operator



    さて、本題に戻ります。

    No35252に返信(魔界の仮面弁士さんの記事)
    >>  Private _xlsApplication As Excel.Application = Nothing
    >>  Friend ReadOnly Property xlsApplication As Excel.Application
    >>    Get
    >>      Return _xlsApplication
    >>    End Get
    >>  End Property
    > これではあまり意味が無いと思いますよ。結局のところ、
    >  Friend ReadOnly xlsApplication As Excel.Application
    > な読み取り専用フィールドと、さほど変わらないように見えます。
    >
    >
    > COM オブジェクトを直接公開してしまうと、ExcelEx の外部で
    >  ExEx1.xlsApplication.Workbooks.Add()
    > などと書かれてしまえば、COM オブジェクトの解放漏れに繋がります。
    >
    > IDisposable としてカプセル化するのであれば、Excel の COM オブジェクトは
    > 外部からは直接操作できないようにして、すべてクラス内に隠蔽します。
    > 戻り値と返すのも COM オブジェクトではなく、.NET のマネージオブジェクトにします。

    全てをクラス内でやってしまう…と言う考え方で良いのでしょうか…
    例えば…
    -----------------------------------------------------------------
    Private _xlsApplication as Excel.Application = Nothing

    Private _ExcelVisible As Boolean = True
    Public Property ExcelVisible As Boolean
      Set(Value as Boolean)
        _ExcelVisible = Value
        If value then
          _xlsApplication.Visible = True
        Else
          _xlsApplication.Visible = False
        End If
      End Set
      Get
        Return _ExcelVisible
      End Get
    End Property
    -----------------------------------------------------------------
    …の様な感じで…

    >>  _xlsWorkSheet = xlsApplication.Sheets.Add()
    > この処理には、問題点が 2 つあります。
    >
    > 1 つは「.」による COM オブジェクトの連続呼び出しであり、
    > Sheets プロパティから返されるコレクションの解放が漏れています。
    Excel.Applicationオブジェクト(COMモデル)の中の、Application.Sheetsプロパティ…あれ?w
    https://learn.microsoft.com/ja-jp/office/vba/api/excel.application.sheets
    COMオブジェクトの場合は(COMオブジェクトの)プロパティの解放も必要と理解して良いのでしょうか…

    >  Dim obj1 = 対象ワークブック.Sheets 'これは Sheets 型        …と言う事は Dim obj1 As Excel.Sheets
    >  Dim obj2 = obj1(strSheetName) 'これは Object 型となることに注意   …と言う事は Dim obj2 As Object
    …で合ってますかね^^?
    ここの理解はまだ追い付いていません(>_<)

    > ただし厳密にいえば、xlsSheets によって列挙されるシートが Excel.Worksheet である保証はありません。
    > Excel.Chart や Excel.DialogSheet が列挙される可能性もあることは頭の片隅に置いといてください。

    …と言う事は…
    -------------------------------------------------------------------------------------
    Friend Sub WorkSheet_Select(strSheetName As String)
      Dim check As Boolean = False
      For x As Integer = 1 to _xlsSheets.Count
        If _xlsSheets(x) Is Excel.Worksheet then                '← 怒られました^^;
          If CType(_xlsSheets (x), Excel.Worksheet).Name = strSheetName Then
            check = True
            Exit For
          End If
        End If
      Next
      If Not check Then
        _xlsWorkSheet = CType(_xlsSheets.Add(), Excel.Worksheet)
        _xlsWorkSheet.Name = strSheetName
      Else
        _xlsWorkSheet = CType(_xlsSheets(strSheetName), Excel.Worksheet)
      End If
      If SheetVisible Then _xlsWorkSheet.Select()
    End Sub
    -------------------------------------------------------------------------------------

    下の方に書いてありました^^;
    >   Dim ws = DirectCast(xlsSheets(n), Excel.Worksheet)
    これだと『WorkSheet』以外はNothingになるのでしょうか…

    (DirectCastは明示的な型変換と書いてありました…
    http://vb.navi-ch.net/2015/07/18/post-118/
    -------------------------------------------------------------------------------------
    サンプルソース
    Dim a As Object = 3.14
    Dim b As Integer = DirectCast(a, Integer)
    MsgBox(b)
    End Sub
    End Class
    実行結果
    メッセージーボックスも表示されません。
    -------------------------------------------------------------------------------------

    頭大パニック中…
違反を報告
引用返信 削除キー/
■35256 / inTopicNo.3)  Re[6]: Excel Com オブジェクトの増殖
□投稿者/ radian 一般人(1回)-(2022/11/28(Mon) 11:57:41)
  • アイコンちなみに、OfficeのCOMの自動管理をやってくれるライブラリもあります。
    解放漏れを特定出来ない&コードが煩雑になっている場合、
    導入を検討してみるのも手かもしれません。

    https://www.nuget.org/packages/NetOfficeFw.Excel/
    https://github.com/NetOfficeFw/NetOffice
違反を報告
引用返信 削除キー/
■35255 / inTopicNo.4)  Re[6]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1494回)-(2022/11/28(Mon) 06:16:23)
  • アイコンNo35251に返信(たこさんの記事)
    > 問題は無い様に思えるのですが…
    Friend Sub WorkSheet_Select には、もう一つ問題点がありそうです。

    > If Not check Then
    >   _xlsWorkSheet = CType(xlsSheets.Add(), Excel.Worksheet)
    >   xlsWorkSheet.Name = strSheetName
    > Else
    >   _xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)
    > End If
    これだと、「以前に _xlsWorkSheet が参照していた COM オブジェクト」が
    解放されなくなってしまいます。


    No35247 において、解説の最後に
    >> Dim y As Excel.Workbooks = x.Workbooks
    >> Dim z As Excel.Workbooks = x.Workbooks
    という実験コードを書いていますが、この場合、VBA や VBS とは異なり、
    .NET においては y と z が別インスタンスとなることに注意が必要です。

    この場合、COM の参照カウントは y と z それぞれに対して減じねばなりません。
    y と z の両方を RelaseComObject した場合と、どちらか一方しか
    解放しなかった場合とで、Excel の残存性を確認してみてください。

    仮に同一インスタンスを返す仕様であったとしたら、
     Trace.WriteLine(Marshal.ReleaseComObject(z)) 'ア
     Trace.WriteLine(Marshal.ReleaseComObject(y)) 'イ
    において、ReleaseComObject の戻り値から得られる残存参照カウント数が
    「イ = ア - 1」の関係となるはずですが、実際には別インスタンスであるため
    「イ = ア」な戻り値で返されていると思います。
違反を報告
引用返信 削除キー/
■35253 / inTopicNo.5)  Re[6]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1492回)-(2022/11/27(Sun) 22:56:50)
  • アイコンNo35251に返信(たこさんの記事)
    > Dim check As Boolean = False
    > For Each sh In xlsSheets
    >   If CType(sh, Excel.Worksheet).Name = strSheetName Then
    >     check = True
    >     Exit For
    >   End If
    > Next

    ここがマズイですね。
    MRComObject(sh) が明らかに漏れています。


    また、 No35252 でも紹介したように、COM オブジェクトに対して For Each を使うと、
    System.Runtime.InteropServices.ComTypes.IEnumVARIANT インターフェイス
    (を実装した EnumeratorViewOfEnumVariant クラス) の解放を行うことが難しくなるので
    For ループ または Do ループ あるいは While ループ に置き換えることをお奨めします。



    > Private _ExcelVisible As Boolean = True
    > Friend Property ExcelVisible As Boolean
    >   Set(value As Boolean)
    >     _ExcelVisible = value
    >     If Not _xlsApplication Is Nothing Then _xlsApplication.Visible = _ExcelVisible
    >   End Set
    >   Get
    >     Return _ExcelVisible
    >   End Get
    > End Property

    フラグ管理したいのであれば _xlsApplication などの COM オブジェクトを公開してはいけません。
    ExEx1.xlsApplication.Visible を直接操作されたら、上記の _ExcelVisible フラグが連動しなくなりますよね?

    同様に、Range や Worksheet や Workbook などを公開するのも問題があります。
    公開したいのであれば、カプセル化した独自のマネージ クラスを Return するようにします。


    逆に、COM オブジェクトを公開する仕様とするのであれば、ExcelEx 側の役目はヘルパーライブラリに徹するものとし、
    COM オブジェクトの解放タイミングは呼び出し側に担当させる仕様の方が良いでしょう。この場合、下記の 2 つをルールとします。

    (1) プロシージャ内で生成された COM オブジェクトは、ExcelEx 自身が即座に解放する。
    (2) 呼び出し元から渡された COM オブジェクトや、呼び出し元に Return する COM オブジェクトは、呼び出し側で解放する。
違反を報告
引用返信 削除キー/
■35252 / inTopicNo.6)  Re[5]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1491回)-(2022/11/27(Sun) 22:32:57)
  • アイコンNo35250に返信(たこさんの記事)
    >   Private _xlsApplication As Excel.Application = Nothing
    >   Friend ReadOnly Property xlsApplication As Excel.Application
    >     Get
    >       Return _xlsApplication
    >     End Get
    >   End Property
    これではあまり意味が無いと思いますよ。結局のところ、
     Friend ReadOnly xlsApplication As Excel.Application
    な読み取り専用フィールドと、さほど変わらないように見えます。


    COM オブジェクトを直接公開してしまうと、ExcelEx の外部で
     ExEx1.xlsApplication.Workbooks.Add()
    などと書かれてしまえば、COM オブジェクトの解放漏れに繋がります。

    IDisposable としてカプセル化するのであれば、Excel の COM オブジェクトは
    外部からは直接操作できないようにして、すべてクラス内に隠蔽します。
    戻り値と返すのも COM オブジェクトではなく、.NET のマネージオブジェクトにします。


    > Option Strict Onをすると『Option Strict On では、遅延バインディングを使用できません。』といっぱい怒られます(^^ゞ
    書き換えながら、御自身で正解に向かったところもあるようですが、一応ひとつひとつ見ていきましょうか。


    > If Not check Then
    >   _xlsWorkSheet = xlsApplication.Sheets.Add()
    この処理には、問題点が 2 つあります。

    1 つは「.」による COM オブジェクトの連続呼び出しであり、
    Sheets プロパティから返されるコレクションの解放が漏れています。

    もう一つは、処理の曖昧さ。上記だと Excel 内で複数のワークブックが同時に開かれていた場合に、
    どのワークブックに対してシートを追加しようとしているのか曖昧になりますよね?
    たとえ VBA であったとしても、あまり望ましくない記述方法と言えます。


    > Else
    >   _xlsWorkSheet = xlsApplication.Sheets(strSheetName)
    これも同様に、Sheets の解放が漏れています。

    後程詳しく紹介しますが、この場合は xlsApplication.Sheets(strSheetName) ではなく、
     Dim obj1 = 対象ワークブック.Sheets 'これは Sheets 型
     Dim obj2 = obj1(strSheetName) 'これは Object 型となることに注意
     _xlsWorkSheet = DirectCast(obj2, Excel.Worksheet)
    のように、途中の COM オブジェクトを変数に受け取る必要があります。
    この obj1 は COM オブジェクトなので、後ほど MRComObject せねばなりません。

    また、_xlsWorkSheet と obj2 は同一インスタンス(Is で比較すると Trueになるはず) なので、
    実際には obj2 を省略して
     Dim obj1 = 対象ワークブック.Sheets
     _xlsWorkSheet = DirectCast(obj1(strSheetName), Excel.Worksheet)
     MRComObject(obj1)
    と書きますし、実際のところは
      _xlsWorkSheet = DirectCast(xlsSheets(strSheetName), Excel.Worksheet)
    とすることになるでしょう。



    > For Each sh In xlsSheets
    >   If sh.Name = strSheetName Then  '←ここで怒られましたが、これは『Ctype(sh,Excel.Worksheet).Name = strSheetName』とする事で解決。
    それで OK です。

    ただし厳密にいえば、xlsSheets によって列挙されるシートが Excel.Worksheet である保証はありません。
    Excel.Chart や Excel.DialogSheet が列挙される可能性もあることは頭の片隅に置いといてください。

    まぁ、実際に使うのはせいぜい Chart (グラフシート) ぐらいで、
    DialogSheet (Excel 5 ダイアログ シート) を使うことはまず無いでしょうけれども。


    それはそれとして、実は For Each はできる限り使わない方が無難です。

    COM に対する For Each は、COM の IEnumVARIANT インターフェイスを通じて行われるため、
    稀に、これが解放の妨げになることがあるためです。
    https://learn.microsoft.com/ja-jp/windows/win32/api/oaidl/nn-oaidl-ienumvariant
    https://divakk.co.jp/aoyagi/csharp_tips_vssenum.html

    なので For Each ループではなく、For ループを Count プロパティと共に使う方が安全です。
    https://qiita.com/mima_ita/items/aa811423d8c4410eca71


    > If Not check Then
    >   _xlsWorkSheet = xlsApplication.Sheets.Add()  '←ここでも怒られますが、『CType(xlsApplication.Excel.Sheets, Excel.Worksheet).Add()』としてみましたが、怒られたままです><
    xlsApplication は Excel.Application 型ですよね。

    書き直した結果の方について見てみると、xlsApplication.Excel.Sheets と書かれていますが、
    Excel.Application 型に Excel というプロパティやメソッドは存在しませんので、
    xlsApplication.Excel.Sheets 以前の問題として
    xlsApplication.Excel とすら書けません。

    ですから当然、
    >   '『 CType(CType(xlsApplication.Excel, Excel.Application).Sheets, Excel.Worksheet).Add()』とかしてみましたが、駄目でした><
    も当然 エラーになったわけです。

    もしかしたら
     CType(xlsApplication.Sheets, Excel.Worksheet).Add()
    と書くつもりだったのかもしれませんが、Sheets プロパティが返す型は
    Excel.Worksheet 型では無く Excel.Sheets 型なので、これもおかしいでsね。



    そもそも何故、元のコードが Option Strict On 時に怒られているのかと言えば、
    代入処理において、右辺にある Add メソッドの戻り値が As Object であるのに、
    左辺にある代入先の変数が Object ではなく Excel.Worksheet だからです。

    これはつまり、Add メソッドの戻り値の Object 型をキャストして、
    本来の Worksheet 型にする必要があったと…という事を意味します。



    > '>> 基本的に、『.』を連続して使うのは赤信号だと思ってください。ここにヒントがある気はしまして…
    連続利用と言っても、たとえば「名前空間.列挙型名.列挙値」などは OK です。
    最初に投稿いただいたコードにある Excel.XlWindowState.xlMaximized などはコレなので安全です。

    一方、「何某.プロパティ名.メソッド名」の場合、
    プロパティが返す値が COM オブジェクトの場合は NG となります。
    プロパティが返す値が マネージ オブジェクトなら OK です。

    そして xlsApplication.Sheets.Add() の場合、Sheets オブジェクトが返すものが
    COM オブジェクトなので NG である、ということです。

    すなわち
     _xlsWorkSheet = xlsApplication.Sheets.Add()
    という処理は、
     Dim a = xlsApplication.Sheets
     _xlsWorkSheet = DirectCast(a.Add(), Excel.Worksheet)
    のようにした方が望ましい、ということです。
    これなら、後で a を MRComObject できますので、解放漏れを防げます。
    (MRComObject しなければ同じことですけれどね…)


    ただし上記の修正もまだ十分ではありません。『xlsApplication.Sheets』を使うと、
    先の例と同様、どのワークブックの Sheets を操作するのかが曖昧になるので、
    Application オブジェクトの Sheets プロパティではなく
    Workbook オブジェクトの Sheets プロパティを用いることが望ましいです。
    これは VB.NET だけでなく、VBA から呼び出す場合にも言えることです。


    >   'いろいろやってみました。『CType(xlsSheets.Add(), Excel.Worksheet)』で良さそうな感じが…(まだエラーだらけで実行出来ていません(^^ゞ
    それで OK です。


    > Else
    >   _xlsWorkSheet = xlsApplication.Sheets(strSheetName)  '←怒られた
    >   '『 CType(xlsApplication.Sheets(strSheetName), Excel.Worksheet)』で良さそう…

    怒られたのは、代入式の左辺が Excel.Worksheet 型、右辺が Object 型だからなので、
    たしかにその書き換えによってコンパイルエラーは防げます。

    しかし、この修正ではいけません。COM オブジェクトの解放漏れにつながります。


    そもそも「xlsApplication.Sheets(strSheetName)」の Sheets メンバーというモノは、
     Property Sheets(n As String) As Object
    なプロパティでもなければ
     Function Sheets(n As String) As Object
    なメソッドでもありません。

    その実態は
     ReadOnly Property Sheets() As Excel.Sheets
    という『引数の無いメンバー』です。

    そして Sheets オブジェクトは、既定のプロパティ(いわゆるインデクサ)を持っているので、
    実際にはこれは、
     _xlsWorkSheet = xlsApplication.Sheets().Item(strSheetName)
    に相当する操作を意味します。「『.』を連続して使うのは赤信号」でしたよね?

    そして、Sheets プロパティが返すのは COM オブジェクトなので、
    これは解放漏れ案件に当たります。

    VB の場合、引数の無いプロパティの呼び出しでは括弧を省略できるので、上記は
     _xlsWorkSheet = xlsApplication.Sheets.Item(strSheetName)
    と書けますし、この .Item は Sheets 型の既定のプロパティであるため、省略して
     _xlsWorkSheet = xlsApplication.Sheets(strSheetName)
    になっていた…というだけです。

    元の「xlsApplication.Sheets(strSheetName)」表記だと、一見すると
    『.』の連続には見えないので注意が必要ですよね。


    こうした「既定のプロパティ」によって、「『.』の連続」が見えにくくなる事象は
    他の COM コレクション型に対しても発生します。たとえば
     Dim r As Excel.Range = Sheet1.Cells(1, 1) 'Cells プロパティの引数に 1,1 を渡しているように見えるが…?
     r.Value = 123
    なども解放漏れ要因になるので NG ですね。

    上記で解放漏れを防ぐには、
     Dim r1 As Excel.Range = Sheet1.Cells '実は Cells プロパティは、「引数を持たない」仕様です
     Dim r2 As Excel.Range = r1(1, 1) 'そしてこれは、Range オブジェクトの .Item(1, 1) に相当します
     r2.Value = 123
     MRComObject(r2)
     MRComObject(r1)
    などのような操作が求められます。



    >   '最終『_xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)』で良さそう…
    そうですね。それでよいと思います。
    ついでに CType を DirectCast にしておくと良さそう。


    > For Each sh In xlsSheets
    >   If CType(sh, Excel.Worksheet).Name = strSheetName Then
    >     check = True
    >     Exit For
    >   End If
    > Next

    For Each を For に置き換えるとこんな感じ。

    Dim found As Boolean = False
    For n As Integer = 1 To xlsSheets.Count
      Dim ws = DirectCast(xlsSheets(n), Excel.Worksheet)
      Dim nm = ws.Name
      MRComObject(ws)
      If nm = strSheetName Then
        found = True
        Exit For
      End If
    Next
違反を報告
引用返信 削除キー/
■35251 / inTopicNo.7)  Re[5]: Excel Com オブジェクトの増殖
□投稿者/ たこ 一般人(18回)-(2022/11/27(Sun) 19:11:16)
  • アイコンNo35250に返信(たこさんの記事)

    実行出来ました!
    …が結果は変わらず…
    タスクマネージャーでMicrosoft Excelがフォアグランドとバックグラウンドを行ったり来たりし、(_xlsApplication.VisibleをTrueにしたりFalseにしたりしているので、問題は無いとは思っています。)
    最終的にバックグラウンドに残ります。
    作成したアプリを終了させると、バックグラウンドに残っているMicrosoft Excelが無くなりますが、全部ではありません。
    (全部のMicrosoft Excelが無くなる事もある)

    Form1クラス ExcelExテスト用起動処理
    ----------------------------------------------------------------------------------------------------
      Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ExEx1 = New ExcelEx()
        ExEx1.WorkBook_Open(見積書 & "【テスト】見積り一覧.xlsx")
        ExEx1.xlsApplication.WindowState = Excel.XlWindowState.xlMaximized
        ExEx1.WorkSheet_Select("見積一覧")
        ExEx1.WorkBook_Close()
        ExEx1 = Nothing
      End Sub
    ----------------------------------------------------------------------------------------------------

    ExcelExクラス(参照しているプロパティ、メソッドを全記載)
    ----------------------------------------------------------------------------------------------------
    Option Explicit On
    Option Strict On

    Imports Microsoft.Office.Interop

    Public Class ExcelEx

      'Excel のアプリケーション参照用オブジェクト
      Private _xlsApplication As Excel.Application = Nothing
      Friend ReadOnly Property xlsApplication As Excel.Application
        Get
          Return _xlsApplication
        End Get
      End Property

      'Excel の Workbooks 参照用オブジェクト (Workbook の Collection)
      Private _xlsWorkBooks As Excel.Workbooks = Nothing
      Friend ReadOnly Property xlsWorkBooks As Excel.Workbooks
        Get
          Return _xlsWorkBooks
        End Get
      End Property

      'Excel の Workbooks 内の1個の Workbook 参照用オブジェクト
      Private _xlsWorkBook As Excel.Workbook = Nothing
      Friend ReadOnly Property xlsWorkBook As Excel.Workbook
        Get
          Return _xlsWorkBook
        End Get
      End Property

      'Excel の Workbook 内の Worksheets 参照用オブジェクト (Worksheet の Collection)
      Private _xlsSheets As Excel.Sheets = Nothing
      Friend ReadOnly Property xlsSheets As Excel.Sheets
        Get
          Return _xlsSheets
        End Get
      End Property

      'Excel の Sheets 内の1個の Worksheet 参照用オブジェクト
      Private _xlsWorkSheet As Excel.Worksheet = Nothing
      Friend ReadOnly Property xlsWorkSheet As Excel.Worksheet
        Get
          Return _xlsWorkSheet
        End Get
      End Property

      'Excel の Sheet 内の1個の Range 参照用オブジェクト
      Friend Property xlsRange As Excel.Range

      Private _ExcelVisible As Boolean = True
      Friend Property ExcelVisible As Boolean
        Set(value As Boolean)
          _ExcelVisible = value
          If Not _xlsApplication Is Nothing Then _xlsApplication.Visible = _ExcelVisible
        End Set
        Get
          Return _ExcelVisible
        End Get
      End Property

      Private _SheetVisible As Boolean = True
      Friend Property SheetVisible As Boolean
        Set(value As Boolean)
          _SheetVisible = value
          If Not _xlsApplication Is Nothing And _SheetVisible Then _xlsWorkSheet.Select()
        End Set
        Get
          Return _SheetVisible
        End Get
      End Property

      Friend Property CellsVisible As Boolean = True

      Friend Property WorkBookName As String = Nothing

      Friend Sub New()
        'Excel アプリケーション起動
        _xlsApplication = New Excel.Application          '←(a
        'Excel の Workbooks 取得
        _xlsWorkBooks = _xlsApplication.Workbooks
        'Excel非表示
        _xlsApplication.Visible = False
        _xlsApplication.DisplayAlerts = False
      End Sub

      Friend Sub WorkBook_Open(strFileName As String)
        '既存 Excel ファイルを開く
        _xlsWorkBook = xlsWorkBooks.Open(strFileName)
        'Excel の Worksheets 取得
        xlsSheets = xlsWorkBook.Worksheets
        'Excel の Worksheet 取得
        _xlsWorkSheet = CType(xlsSheets.Item(1), Excel.Worksheet)
        _xlsWorkSheet.Select()
        _xlsApplication.Visible = ExcelVisible          '←(b
      End Sub

      Friend Sub WorkSheet_Select(strSheetName As String)
        Dim check As Boolean = False
        For Each sh In xlsSheets
          If CType(sh, Excel.Worksheet).Name = strSheetName Then
            check = True
            Exit For
          End If
        Next
        If Not check Then
          _xlsWorkSheet = CType(xlsSheets.Add(), Excel.Worksheet)
          xlsWorkSheet.Name = strSheetName
        Else
          _xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)
        End If
        If SheetVisible Then xlsWorkSheet.Select()
      End Sub

      Friend Sub WorkBook_Close()
        '終了処理
        'xlsRange の解放
        MRComObject(xlsRange)
        'xlsWorkSheet の解放
        MRComObject(xlsWorkSheet)
        'xlsWorkSheets の解放
        MRComObject(xlsSheets)
        'xlsWorkbookを閉じる
        xlsWorkBook.Close(SaveChanges:=False)
        'xlsWorkbook の解放
        MRComObject(xlsWorkBook)
        'xlsWorkbooks の解放
        MRComObject(xlsWorkBooks)
        'Excelを閉じる
        xlsApplication.Quit()              ' ←(c
        'xlsApplication を解放
        MRComObject(xlsApplication)         ' ←(d
      End Sub

      'COM オブジェクトへの参照を解放
      ''' <summary>
      ''' COMオブジェクトの参照カウントをデクリメントします。
      ''' </summary>
      ''' <typeparam name="T">(省略可能)</typeparam>
      ''' <param name="objCom">
      ''' COM オブジェクト持った変数を指定します。
      ''' このメソッドの呼出し後、この引数の内容は Nothing となります。
      ''' </param>
      ''' <param name="force">
      ''' すべての参照を強制解放する場合はTrue、現在の参照のみを減ずる場合はFalse。
      ''' </param>
      Friend Sub MRComObject(Of T As Class)(ByRef objCom As T, Optional ByVal force As Boolean = False)
        If objCom Is Nothing Then Return
        Try
          If System.Runtime.InteropServices.Marshal.IsComObject(objCom) Then
            If force Then
              System.Runtime.InteropServices.Marshal.FinalReleaseComObject(objCom)
            Else
              System.Runtime.InteropServices.Marshal.ReleaseComObject(objCom)
            End If
          End If
        Finally
          objCom = Nothing
        End Try
      End Sub
    ----------------------------------------------------------------------------------------------------

    以下タスクマネージャーでMicrosoft Excelを確認。
    ----------------------------------------------------------------------------------------------------
    a)の位置でバックグラウンドに出現。
    b)の位置でフォアグラウンドに出現。(バックグラウンドからは無くなる。)
    c)の位置でバックグラウンドの再出現。
    d)の位置でバックグラウンドから無くなるのが普通の動作かなと思っている。
    ----------------------------------------------------------------------------------------------------
    なお、作成したアプリを終了させると、バックグラウンドに残っているMicrosoft Excelが無くなりますが、全部ではありません。

    問題は無い様に思えるのですが…

違反を報告
引用返信 削除キー/
■35250 / inTopicNo.8)  Re[4]: Excel Com オブジェクトの増殖
□投稿者/ たこ 一般人(17回)-(2022/11/27(Sun) 16:46:37)
  • アイコンしばらく見ない間に返信も増殖してました^^;

    No35244に返信(魔界の仮面弁士さんの記事)
    @ 魔界の仮面弁士様のアドバイスを元に、各オブジェクトをプライベートに設定し、プロパティとメソッドを分けました。
    ----------------------------------------------------------------------------------------------------
      'Excel のアプリケーション参照用オブジェクト
      Private _xlsApplication As Excel.Application = Nothing
      Friend ReadOnly Property xlsApplication As Excel.Application
        Get
          Return _xlsApplication
        End Get
      End Property

      〜〜(以下同じ)〜〜
       xlsWorkBooks
       xlsWorkBook
       xlsSheets
       xlsWorkSheet
       xlsRange

      〜〜(省略)〜〜

      Friend Sub New()
        'Excel アプリケーション起動
        _xlsApplication = New Excel.Application
        'Excel の WorkbookBeforeClose イベントを取得
        'AddHandler xlsApplication.WorkbookBeforeClose, AddressOf xlsApplication_WorkbookBeforeClose
        'Excel の Workbooks 取得
        _xlsWorkBooks = _xlsApplication.Workbooks
        'Excel非表示
        _xlsApplication.Visible = False
        _xlsApplication.DisplayAlerts = False
      End Sub

      Friend Sub WorkBook_Open(strFileName As String)
        '既存 Excel ファイルを開く
        _xlsWorkBook = xlsWorkBooks.Open(strFileName)
        'Excel の Worksheets 取得
        _xlsSheets = xlsWorkBook.Worksheets
        'Excel の Worksheet 取得
        _xlsWorkSheet = CType(xlsSheets.Item(1), Excel.Worksheet)  ' (a)
        _xlsWorkSheet.Select()
        _xlsApplication.Visible = ExcelVisible
      End Sub

      Friend Sub Workbook_Save()
        If Not WorkBookName Is Nothing Then
          _xlsWorkBook.SaveAs(Filename:=WorkBookName)
        Else
          _xlsWorkBook.Save()
        End If
      End Sub

      Friend Sub WorkBook_Close()
        '終了処理
        'xlsRange の解放
        MRComObject(xlsRange)
        'xlsWorkSheet の解放
        MRComObject(xlsWorkSheet)
        'xlsWorkSheets の解放
        MRComObject(xlsSheets)
        'xlsWorkbookを閉じる
        xlsWorkBook.Close(SaveChanges:=False)
        'xlsWorkbook の解放
        MRComObject(xlsWorkBook)
        'xlsWorkbooks の解放
        MRComObject(xlsWorkBooks)
        'Excelを閉じる
        xlsApplication.Quit()
        'xlsApplication を解放
        MRComObject(xlsApplication)
      End Sub

      Friend Sub WorkSheet_Select(strSheetName As String)
        Dim check As Boolean = False
        For Each sh In xlsSheets
          If sh.Name = strSheetName Then
            check = True
            Exit For
          End If
        Next
        If Not check Then
          _xlsWorkSheet = xlsApplication.Sheets.Add()
          xlsWorkSheet.Name = strSheetName
        Else
          _xlsWorkSheet = xlsApplication.Sheets(strSheetName)
        End If
        If SheetVisible Then xlsWorkSheet.Select()
      End Sub
    ----------------------------------------------------------------------------------------------------

    No35247に返信(魔界の仮面弁士さんの記事)
    A
    >>xlsWorkSheet = xlsWorkSheets.Item(1)
    > .Item の戻り値は Object のはずですので、明示的にキャストしましょう。

    明示的にキャストしました。…(a)


    > 参照カウントが不用意に増加することを防ぐため、可能であれば
    > 「Option Strict On」で実装することをお奨めします。

    Option Strict Onをすると『Option Strict On では、遅延バインディングを使用できません。』といっぱい怒られます(^^ゞ
    調べたところ… (https://learn.microsoft.com/ja-jp/dotnet/visual-basic/misc/bc30574
    『明示的に書け』だそうで…

    例えばシートを選択する処理…
    ----------------------------------------------------------------------------------------------------
      Friend Sub WorkSheet_Select(strSheetName As String)
        Dim check As Boolean = False
        For Each sh In xlsSheets
          If sh.Name = strSheetName Then  '←ここで怒られましたが、これは『Ctype(sh,Excel.Worksheet).Name = strSheetName』とする事で解決。
            check = True
            Exit For
          End If
        Next
        If Not check Then
          _xlsWorkSheet = xlsApplication.Sheets.Add()  '←ここでも怒られますが、『CType(xlsApplication.Excel.Sheets, Excel.Worksheet).Add()』としてみましたが、怒られたままです><
                                 ’■No35248に返信(魔界の仮面弁士さんの記事) 参考情報: https://www.petitmonte.com/bbs/answers?question_id=25306 ここの…
                                 '>> 基本的に、『.』を連続して使うのは赤信号だと思ってください。ここにヒントがある気はしまして…
                                 '『 CType(CType(xlsApplication.Excel, Excel.Application).Sheets, Excel.Worksheet).Add()』とかしてみましたが、駄目でした><
                                 'いろいろやってみました。『CType(xlsSheets.Add(), Excel.Worksheet)』で良さそうな感じが…(まだエラーだらけで実行出来ていません(^^ゞ
          xlsWorkSheet.Name = strSheetName
        Else
          _xlsWorkSheet = xlsApplication.Sheets(strSheetName)  '←怒られた
                                     '『 CType(xlsApplication.Sheets(strSheetName), Excel.Worksheet)』で良さそう…

                                     '<参考>
                                     '> ■No35245に返信(たこさんの記事)
                                     '> xlsWorkSheet = xlsApplication.Sheets(strSheetName) ではなく
                                     '> xlsWorkSheet = xlsWorkSheets(strSheetName) でしょう。
                                     '最終『_xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)』で良さそう…

        End If
        If SheetVisible Then xlsWorkSheet.Select()
      End Sub
    ----------------------------------------------------------------------------------------------------

    > もしもレイトバインド前提で実装するのなら、.NET ではなく
    > VBS や VBA から利用した方が安全です。

    VB.NETからExcelを操作する勉強も兼ねているので、このまま行きます(^^ゞ

    例えばシートを選択する処理最終版…
    ----------------------------------------------------------------------------------------------------
      Friend Sub WorkSheet_Select(strSheetName As String)
        Dim check As Boolean = False
        For Each sh In xlsSheets
          If CType(sh, Excel.Worksheet).Name = strSheetName Then
            check = True
            Exit For
          End If
        Next
        If Not check Then
          _xlsWorkSheet = CType(xlsSheets.Add(), Excel.Worksheet)
          xlsWorkSheet.Name = strSheetName
        Else
          _xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)
        End If
        If SheetVisible Then xlsWorkSheet.Select()
      End Sub
    ----------------------------------------------------------------------------------------------------

    他の処理でもエラーが多すぎて頭が追い付かず…
    実行出来たらまた報告致します。。。

    (私、実はすごくなんとなくで作っているんだなぁ…と実感しました…
違反を報告
引用返信 削除キー/
■35249 / inTopicNo.9)  Re[3]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1490回)-(2022/11/27(Sun) 12:56:43)
  • アイコンNo35245に返信(たこさんの記事)
    >>> ExEx1.Sheets(1)
    > Sheetsの実装はこうなっております。
    > Public Sub Sheets(strSheetName As String)

    ん? よく見たら型が一致していませんね。

    Sub Sheets(x As Integer) なオーバーロードが別にあったのか、
    それともサンプルのミスで、実際には ExEx1.Sheets("1") と書いていたのか、
    それとも Option Strict Off による暗黙の型変換が発生していたのか…?
違反を報告
引用返信 削除キー/
■35248 / inTopicNo.10)  Re[4]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1489回)-(2022/11/27(Sun) 11:43:12)
  • アイコンNo35247に追記(魔界の仮面弁士の記事)
    > 参照カウントが不用意に増加することを防ぐため、可能であれば
    > 「Option Strict On」で実装することをお奨めします。

    参考情報:

    Excel のメソッドに対して、COM オブジェクトを
    明示的な型ではなく、汎用の Object 型で受け渡したときに、
    参照カウントが増加してしまった事例を紹介しておきます。

    https://www.petitmonte.com/bbs/answers?question_id=25306
    >> 魔界の仮面弁士 2005-12-22 11:29:19 No: 128686
    >> どうやら、レイトバインドの場合、
    >> Dim Hs As Object = S.HPageBreaks
    >> Dim H As Object = Hs.Add(R)
    >> の処理を行った時点で、変数 R の参照カウントが
    >> 増加してしまっていたようです。

    ※このあたりの動作は、Office のバージョンなどに左右される可能性があります。
違反を報告
引用返信 削除キー/
■35247 / inTopicNo.11)  Re[3]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1488回)-(2022/11/27(Sun) 11:36:26)
  • アイコンNo35241に返信(たこさんの記事)
    > xlsWorkSheet = xlsWorkSheets.Item(1)
    .Item の戻り値は Object のはずですので、明示的にキャストしましょう。

    参照カウントが不用意に増加することを防ぐため、可能であれば
    「Option Strict On」で実装することをお奨めします。

    たとえば「Range オブジェクトを引数に受け取るプロパティやメソッド」に対して
    Range 型の変数ではなく Object 型の変数を通じて受け渡した場合、
    相互運用機能アセンブリによる内部的な型判定の時点で参照カウントが増加し、
    解放漏れにつながることがあります。

    もしもレイトバインド前提で実装するのなら、.NET ではなく
    VBS や VBA から利用した方が安全です。


    No35245に返信(たこさんの記事)
    >   Public Sub Sheets(strSheetName As String)
    >     xlsWorkSheet = xlsApplication.Sheets(strSheetName)
    >     If SheetVisible Then xlsWorkSheet.Select()
    >   End Sub

    案の定、Sheets メソッドに原因がありそうですね。

    xlsWorkSheet = xlsApplication.Sheets(strSheetName) ではなく
    xlsWorkSheet = xlsWorkSheets(strSheetName) でしょう。


    ただし上記は厳密には同義ではありません。
    xlsWorkSheets は WorkBook_Open 時に
     xlsWorkSheets = xlsWorkbook.Worksheets
    で代入されていますので、strSheetName の処理対象が
    「WorkBook_Open 時のワークブック」のシートではなく
    「現在アクティブなワークブック」のシートの操作を意図しているなら
      Dim wBook = xlsApplication.ActiveWorkbook
      Dim wSheets = wBook.Sheets
      xlsWorkSheets = wSheets(strSheetName)
      MRComObject(wSheets)
      MRComObject(wBook)
    といった形になるかと思います。



    いずれの方法をとるにせよ、xlsWorkSheet の初期値は
    xlsWorkSheets.Item(1) だったはずなので、xlsWorkSheet 変数への
    代入前に MRComObject(xlsWorkSheet) も呼んでおく必要があります。


    Dim x As New Excel.Application()
    Dim y As Excel.Workbooks = x.Workbooks
    Dim z As Excel.Workbooks = x.Workbooks

    MsgBox(y Is z) 'これは True ではなく、False になる可能性が高い

    x.Quit()
    Trace.WriteLine(Marshal.ReleaseComObject(z))
    Trace.WriteLine(Marshal.ReleaseComObject(y))
    Trace.WriteLine(Marshal.ReleaseComObject(x))
違反を報告
引用返信 削除キー/
■35245 / inTopicNo.12)  Re[2]: Excel Com オブジェクトの増殖
□投稿者/ たこ 一般人(16回)-(2022/11/27(Sun) 10:59:37)
  • アイコン魔界の仮面弁士様
    いつも素早いご回答ありがとうございます。


    > たとえば、使用例のところで
    >>    ExEx1 = New ExcelEx()
    >>    ExEx1.WorkBook = path & "Test.xlsx"
    >>    ExEx1.xlsApplication.WindowState = Excel.XlWindowState.xlMaximized
    >>    ExEx1.Sheets(1)
    >>    ExEx1.WorkBook = "Close"
    > と書かれていますが、Sheets メソッドの実装が不明瞭ですし。


    Sheetsの実装はこうなっております。
    ----------------------------------------------------------------------------------------------------
      Public Sub Sheets(strSheetName As String)
        xlsWorkSheet = xlsApplication.Sheets(strSheetName)
        If SheetVisible Then xlsWorkSheet.Select()
      End Sub
    ----------------------------------------------------------------------------------------------------


    >>Friend xlsApplication As Excel.Application = Nothing
    > これらの COM オブジェクトを非 Private として公開されていますが、
    > Friend で公開するにしても、ReadOnly な Property にしておくべきかと。
    >
    > ただし ReadOnly だとしても、ExcelEx 外から Open や Close 等、あるいは Cells 等への
    > アクセスは無制限に可能となってしまいますので、COM オブジェクトを直接公開すると、
    > ExcelEx だけでは、参照と解放の管理をまかないきれなくなることがあります。

    なるほどです…


    > 解放まですべてクラス側で管理するなら、委譲している Excel のオブジェクトを
    > 直接公開するのではなく、必要な操作のみを再公開するようにしたうえで、
    > IDisposable パターンも追加実装しておくのが安全です。実装量は爆発的に増えますけれどね。
    > https://learn.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-dispose
    > https://ufcpp.net/study/csharp/rm_disposable.html
    >
    > 逆に、そこまでの実装コストをかけられないというのであれば、
    > ヘルパーライブラリとしての実装に留めるという手もあろうかと。

    明記はしませんでしたが、IDisposableも実装しています。(実装パターンそのまま)
    ----------------------------------------------------------------------------------------------------
      Private disposedValue As Boolean
      Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposedValue Then
          If disposing Then
            ' TODO: マネージド状態を破棄します (マネージド オブジェクト)
            WorkBook_Close()
          End If

          ' TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
          ' TODO: 大きなフィールドを null に設定します
          disposedValue = True
        End If
      End Sub

      〜〜〜

    ----------------------------------------------------------------------------------------------------


    >>Private _WorkBookIsNew As Boolean = Nothing
    > = False と書いた方が素直ではありませんか?

    確かにそうですね(^^ゞ


    >>Public Property WorkBook As String
    > 今回の COM 解放の問題からは外れますが、こういったものはプロパティではなく、
    > String 引数を持ったメソッドとして実装する方が望ましいです。
    >
    > クラス設計的には、プロパティは "状態" を表すものであり、
    > "動作" を行うことを目的としたプロパティを実装すべきではありません。
    > "動作" を目的とするのはメソッドの役目ですよね。

    確かに…(^^ゞ



    >>Private Sub WorkBook_Open(Optional ByVal strFileName As String = Nothing)
    >>    'Excel アプリケーション起動
    >>    xlsApplication = New Excel.Application
    > WorkBook_Open が 2 回呼ばれると、xlsApplication 変数が
    > 以前に編集していたインスタンスを破棄することなく、
    > 新しい Application インスタンスを生成することになりそうですが、
    > 何らかの再入防止策が組み込まれていますか?

    2回呼ばない様にしています…が…(^^ゞ


    > インスタンス管理を考えると、Application の起動は
    > ExcelEx のコンストラクタで行った方が良いと思います。

    なるほどそうですね…(^^ゞ


    >>    MRComObject(xlsRange, True)
    > ExcelEx の他の部分の処理がどうなっているのかにもよりますが、
    > Range オブジェクトは複数使われることも多いので、
    > 自分ならコレクション管理するかな…?

    なるほど思いつきもしなかったです…
    Cellオブジェクトもコレクション化した方が良さそうですね…


    >>    If Not xlsWorkbook Is Nothing Then xlsWorkbook.Close()
    > 省略された部分のコードによって、この時点で Nothing になっていることがあるということですか?
    >
    > 提示された範囲のコードを見るだけでは、ここで Nothing になっているケースとは、外部から
    >  ExEx1.xlsWorkbook = Nothing
    > とされた場合か、あるいは WorkBook_Open がまだ呼ばれていない時ぐらいしかなさそうですが…。
    > (または、存在しないファイル名を指定するなどして、WorkBook_Open が失敗した場合とか)


    閉じないのでこんなコードを書いて実験していました…
    ----------------------------------------------------------------------------------------------------
      ExEx2.WorkBook = "Close"
      ExEx2.Dispose()
      ExEx2 = Nothing
    ----------------------------------------------------------------------------------------------------

    > あと、xlsWorkbook.Saved が False の時に Close を呼び出すと、
    > DisplayAlerts が True に戻されていた場合、保存確認のダイアログが表示されることになります。
    > そして保存確認でキャンセルされた場合、実際には Close されません。
    >
    > もしも終了時の保存が常に不要な場合は、Close 前に Saved プロパティを True にしておくか、
    > もしくは Close メソッドの SaveChanges 引数に False を指定するようにします。

    一番の問題はここそうです!
    開いたExcelファイルにSheetの初期化の為にシートごとコピーしてくるVBAコードがあります。

    (以下、Excel側VBAコード)
    ----------------------------------------------------------------------------------------------------
    Private Sub Workbook_Open()
      Dim row As Integer
      Dim work As String
      Dim No As String
      Dim FolderName As String
      Dim i As Integer

      On Error Resume Next

      If ActiveWorkbook.Name = "御見積書_原紙_1.xlsm" Or ActiveWorkbook.Name = "御見積書_原紙_2.xlsm" Then
        Check = True
        If Sheets("【設計用】見積書").Range("H1").Value <> "No." Then
          Sheets("【原紙】【設計用】材料一覧表").Select
          Cells.Select
          Selection.Copy
          Sheets("【設計用】材料一覧表").Select
          Range("A1").Select
          ActiveSheet.Paste
          Sheets("【原紙】【設計用】見積書").Select
          Cells.Select
          Selection.Copy
          Sheets("【設計用】見積書").Select
          Range("A1").Select
          ActiveSheet.Paste
        End If

        NewNo
        Check = False
      End If

      UserForm1.Show (False)

      Range("A5").Select
    End Sub
    ----------------------------------------------------------------------------------------------------

    上記を踏まえ、作り直します。
    ありがとうございます。
違反を報告
引用返信 削除キー/
■35244 / inTopicNo.13)  Re[1]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1486回)-(2022/11/27(Sun) 08:37:41)
  • アイコンNo35241に返信(たこさんの記事)
    > Excelの処理部分はクラスにまとめ処理していますが、タスクマネージャーで見ると閉じたはずのExcelが残っています。
    > いろいろやってみましたが解決できず、お知恵をお借りしたいと投稿致します。
    省略されている箇所に問題があるような気もしますね。

    たとえば、使用例のところで
    >     ExEx1 = New ExcelEx()
    >     ExEx1.WorkBook = path & "Test.xlsx"
    >     ExEx1.xlsApplication.WindowState = Excel.XlWindowState.xlMaximized
    >     ExEx1.Sheets(1)
    >     ExEx1.WorkBook = "Close"
    と書かれていますが、Sheets メソッドの実装が不明瞭ですし。



    > Friend xlsApplication As Excel.Application = Nothing
    これらの COM オブジェクトを非 Private として公開されていますが、
    Friend で公開するにしても、ReadOnly な Property にしておくべきかと。

    ただし ReadOnly だとしても、ExcelEx 外から Open や Close 等、あるいは Cells 等への
    アクセスは無制限に可能となってしまいますので、COM オブジェクトを直接公開すると、
    ExcelEx だけでは、参照と解放の管理をまかないきれなくなることがあります。


    解放まですべてクラス側で管理するなら、委譲している Excel のオブジェクトを
    直接公開するのではなく、必要な操作のみを再公開するようにしたうえで、
    IDisposable パターンも追加実装しておくのが安全です。実装量は爆発的に増えますけれどね。
    https://learn.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-dispose
    https://ufcpp.net/study/csharp/rm_disposable.html

    逆に、そこまでの実装コストをかけられないというのであれば、
    ヘルパーライブラリとしての実装に留めるという手もあろうかと。


    > Private _WorkBookIsNew As Boolean = Nothing
    = False と書いた方が素直ではありませんか?


    > Public Property WorkBook As String
    今回の COM 解放の問題からは外れますが、こういったものはプロパティではなく、
    String 引数を持ったメソッドとして実装する方が望ましいです。

    クラス設計的には、プロパティは "状態" を表すものであり、
    "動作" を行うことを目的としたプロパティを実装すべきではありません。
    "動作" を目的とするのはメソッドの役目ですよね。

    16bit 時代の Visual Basic ではプロパティで動作させていたりもしますがね…。
    (例:32bit 版では非推奨になった CommonDialog.Action プロパティなど)


    > Private Sub WorkBook_Open(Optional ByVal strFileName As String = Nothing)
    >     'Excel アプリケーション起動
    >     xlsApplication = New Excel.Application
    WorkBook_Open が 2 回呼ばれると、xlsApplication 変数が
    以前に編集していたインスタンスを破棄することなく、
    新しい Application インスタンスを生成することになりそうですが、
    何らかの再入防止策が組み込まれていますか?

    インスタンス管理を考えると、Application の起動は
    ExcelEx のコンストラクタで行った方が良いと思います。


    >     MRComObject(xlsRange, True)
    ExcelEx の他の部分の処理がどうなっているのかにもよりますが、
    Range オブジェクトは複数使われることも多いので、
    自分ならコレクション管理するかな…?

    >     If Not xlsWorkbook Is Nothing Then xlsWorkbook.Close()
    省略された部分のコードによって、この時点で Nothing になっていることがあるということですか?

    提示された範囲のコードを見るだけでは、ここで Nothing になっているケースとは、外部から
     ExEx1.xlsWorkbook = Nothing
    とされた場合か、あるいは WorkBook_Open がまだ呼ばれていない時ぐらいしかなさそうですが…。
    (または、存在しないファイル名を指定するなどして、WorkBook_Open が失敗した場合とか)

    あと、xlsWorkbook.Saved が False の時に Close を呼び出すと、
    DisplayAlerts が True に戻されていた場合、保存確認のダイアログが表示されることになります。
    そして保存確認でキャンセルされた場合、実際には Close されません。

    もしも終了時の保存が常に不要な場合は、Close 前に Saved プロパティを True にしておくか、
    もしくは Close メソッドの SaveChanges 引数に False を指定するようにします。


    > Excelを閉じた時にプロセスは終了・削除されると思っているのですが、認識が違うのでしょうか…
    .NET 側を解放したとしても、ReleaseComObject されねば COM 側は生き残りますが、
    その逆に COM 参照が残っている状態で Excel を閉じた場合、終了せずに非表示だけで留まることがあります。

    逆に、 COM 側だけが解放されたとしたら、解放済みのオブジェクトを操作するタイミングで、
    HRESULT: 0x80010108 (RPC_E_DISCONNECTED) に相当する例外
    たとえば「起動されたオブジェクトはクライアントから切断されました。」などが
    発生する可能性があります。
違反を報告
引用返信 削除キー/
■35241 / inTopicNo.14)  Excel Com オブジェクトの増殖
□投稿者/ たこ 一般人(15回)-(2022/11/27(Sun) 04:14:52)
  • アイコン環境/言語:[[Windows10 VB.NET .NET Framework 4.7.2 VS2019]] 
    分類:[.NET] 

    環境追記
    [Office 365 Excel バージョン2210]
    [Microsoft Excel 16.0 Object Library]


    いつもお世話になっております。

    今回Excelの既存データをまとめようと思いまして、
    VB.NETのExcel Comオブジェクトを使用して2枚のExcelを開いて処理しています。

    Excelの処理部分はクラスにまとめ処理していますが、タスクマネージャーで見ると閉じたはずのExcelが残っています。
    いろいろやってみましたが解決できず、お知恵をお借りしたいと投稿致します。


    <参考にしたサイト>
    https://hironimo.com/prog/vbnet/vb-net-excel/
    https://oreno-it2.info/archives/1043
    http://vbnettips.blog.shinobi.jp/file-folder/excel%20%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E6%93%8D%E4%BD%9C%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%EF%BC%88microsof
    http://hanatyan.sakura.ne.jp/vb2005/vb2013excel01.htm
    http://up-up-everyday.cocolog-nifty.com/wanco_programming/2015/07/vbnetexcel-81a4.html


    ExcelExクラス(抜粋)
    ----------------------------------------------------------------------------------------------------
      'Excel のアプリケーション参照用オブジェクト
      Friend xlsApplication As Excel.Application = Nothing
      'Excel の Workbooks 参照用オブジェクト (Workbook の Collection)
      Friend xlsWorkbooks As Excel.Workbooks = Nothing
      'Excel の Workbooks 内の1個の Workbook 参照用オブジェクト
      Friend xlsWorkbook As Excel.Workbook = Nothing
      'Excel の Workbook 内の Worksheets 参照用オブジェクト (Worksheet の Collection)
      Friend xlsWorkSheets As Excel.Sheets = Nothing
      'Excel の Sheets 内の1個の Worksheet 参照用オブジェクト
      Friend xlsWorkSheet As Excel.Worksheet = Nothing
      'Excel の Sheet 内の1個のセル Range 参照用オブジェクト
      Friend xlsRange As Excel.Range = Nothing

      Private _WorkBook As String = Nothing
      Private _WorkBookIsNew As Boolean = Nothing
      Public Property WorkBook As String
        Set(value As String)
          _WorkBook = value
          Select Case _WorkBook
            Case ""
              WorkBook_Open()
              _WorkBookIsNew = True
            Case "Close"
              _frgClose = True
              WorkBook_Close()
            Case "Save"
              xlsWorkbook.Save()
            Case "SaveAfterClose", "CloseBeforeSave"
              xlsWorkbook.Save()
              _frgClose = True
              WorkBook_Close()
            Case Else
              WorkBook_Open(_WorkBook)
              _WorkBookIsNew = False
          End Select
        End Set
        Get
          Return _WorkBook
        End Get
      End Property

      Private Sub WorkBook_Open(Optional ByVal strFileName As String = Nothing)
        'Excel アプリケーション起動
        xlsApplication = New Excel.Application
        'Excel の Workbooks 取得
        xlsWorkbooks = xlsApplication.Workbooks
        'Excel非表示
        xlsApplication.Visible = False
        xlsApplication.DisplayAlerts = False
        If IsNothing(strFileName) Then
          '新規 Excel ファイルを開く
          xlsWorkbook = xlsWorkbooks.Add()
        Else
          '既存 Excel ファイルを開く
          xlsWorkbook = xlsWorkbooks.Open(strFileName)
        End If
        'Excel の Worksheets 取得
        xlsWorkSheets = xlsWorkbook.Worksheets
        'Excel の Worksheet 取得
        xlsWorkSheet = xlsWorkSheets.Item(1)
        xlsWorkSheet.Select()
        xlsApplication.Visible = ExcelVisible
      End Sub

      Private Sub WorkBook_Close()
        '終了処理
        'xlsRange の解放
        MRComObject(xlsRange, True)
        'xlsWorkSheet の解放
        MRComObject(xlsWorkSheet, True)
        'xlsWorkSheets の解放
        MRComObject(xlsWorkSheets, True)
        'xlsWorkbookを閉じる
        If Not xlsWorkbook Is Nothing Then xlsWorkbook.Close()
        'xlsWorkbook の解放
        MRComObject(xlsWorkbook, True)
        'xlsWorkbooks の解放
        MRComObject(xlsWorkbooks, True)
        'Excelを閉じる
        If Not xlsApplication Is Nothing Then xlsApplication.Quit()
        'xlsApplication を解放
        MRComObject(xlsApplication, True)
      End Sub

      'COM オブジェクトへの参照を解放
      ''' <summary>
      ''' COMオブジェクトの参照カウントをデクリメントします。
      ''' </summary>
      ''' <typeparam name="T">(省略可能)</typeparam>
      ''' <param name="objCom">
      ''' COM オブジェクト持った変数を指定します。
      ''' このメソッドの呼出し後、この引数の内容は Nothing となります。
      ''' </param>
      ''' <param name="force">
      ''' すべての参照を強制解放する場合はTrue、現在の参照のみを減ずる場合はFalse。
      ''' </param>
      Friend Sub MRComObject(Of T As Class)(ByRef objCom As T, Optional ByVal force As Boolean = False)
        If objCom Is Nothing Then Return
        Try
          If System.Runtime.InteropServices.Marshal.IsComObject(objCom) Then
            If force Then
              System.Runtime.InteropServices.Marshal.FinalReleaseComObject(objCom)
            Else
              System.Runtime.InteropServices.Marshal.ReleaseComObject(objCom)
            End If
          End If
        Finally
          objCom = Nothing
        End Try
      End Sub
    ----------------------------------------------------------------------------------------------------

    上記ExcelExクラスがベースクラスで、子クラス、孫クラスがあります。
    子クラスで、Data As Microsoft.Office.Interop.Excel.Rangeと言うヶ所があるので、
    (必要無いとは思っているものの)MRComObject(Data)と入れてあります。

    ちょっと書いている途中で思いついたので、ベースクラスのみ呼び出して閉じる…
    ----------------------------------------------------------------------------------------------------
      Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ExEx1 = New ExcelEx()
        ExEx1.WorkBook = path & "Test.xlsx"
        ExEx1.xlsApplication.WindowState = Excel.XlWindowState.xlMaximized
        ExEx1.Sheets(1)
        ExEx1.WorkBook = "Close"
      End Sub
    ----------------------------------------------------------------------------------------------------
    を実行してもやはりゴミが残ります。

    Excelを閉じた時にプロセスは終了・削除されると思っているのですが、認識が違うのでしょうか…

マルチポストを報告
違反を報告
引用返信 削除キー/



トピック内ページ移動 / << 0 >>
Mode/  Pass/


- Child Tree -