The Document of VBA, by VBA, for VBA（その1）

#読むのが面倒な人へ
　VBAからVBAの仕様書を作るVBAを書きました。（~~完成品~~　一時停止中です）
~~※たぶん大丈夫だとは思いますが、自己責任でお願いします。~~大丈夫じゃなかった。

#前回までのあらすじ
　VBAでVBEを操作できるので、VBAのコードを解析してJavaDoc的なことができないか？

#設定
　VBAでVBEを扱うためには参照設定が必要です。（[参考サイト](http://officetanaka.net/excel/vba/vbe/01.htm)）

#参考サイト
　https://excel-ubara.com/excelvba5/EXCELVBA269.html
　VBAを使ってVBAに記載されているコードの一覧を作成するサンプル。
　もうほとんどゴールなので、これのリファクタリングと、次の機能を追加しています。

1.Wordへの書き出し
　公務員のドキュメントといえばWord。目次が使えるので悪くはないかなぁと思います。
　Wordというだけで毛嫌いさされたりしますが、個人的には能力を引き出せていないユーザと、能力を引き出さないとその辺のメモ帳に勝てないユーザビリティの低い設計のMicrosoftが悪いと思う。
　
2.呼び出し関係
　そのプロシージャから呼び出されるプロシージャ・クラス・モジュール情報の取得。
　JavaDocを~~パク~~参考にするなら必須と思い、開発しましたが、めちゃくちゃ大変でした。
　どれくらい大変だったかというと、大変すぎて呼び出される関係も取得しようと思いましたが断念しました。
　アルゴリズムを解説するのでもっといい案があったら誰か教えてください。

3.コールグラフ（呼び出し関係図）
　上記2.の関係性を線でつないだもの。作ったはいいですが列挙するだけでは結局よくわからなかったので、急遽追加（これでバージョンが2になりました）。
　作ってからいわゆるコールグラフと呼ばれると知りましたが、コールグラフにはすべての関係を列挙する静的なコールグラフと特定の機能の関係のみを列挙した動的なコールグラフとがあるようです。
　静的コールグラフだけだと見づらすぎてやっぱりよくわからなかったので、動的コールグラフも作れるようにしました。
　これも正解がわからないので誰かいい案があったら教えてください。

4.進捗管理
　上記2.のせいで実行時間が果てしなく増えたので、ユーザビリティの向上のために追加。(参考サイト：https://excel-ubara.com/excelvba3/EXCELFORM026.html ）

#コード解説
概ね以下の流れです。
0.初期化
1.コードの走査
2.コールグラフを作成
3.走査結果をシートへ出力
4.Wordへの出力

##主たるプロシージャ
　実際にはこれをリボンから呼び出します。
　また、実行制御のクラスや進捗管理のフォーム、結果管理のクラスなどがたくさんあるので、これ単体では動きません。

```VB:MainModule.MakeVBADoc()
Option Explicit

Public Sub MakeVBADoc()
  Dim exe As Settings:  Set exe = New Settings
  
  Dim fileName As String: fileName = openVBAFile

  #If DebugMode Then
    If fileName = "" Then fileName = ThisWorkbook.Name
  #Else
    If fileName = "" Then Exit Sub
  #End If
  
  Dim wb As Workbook: Set wb = Workbooks(fileName)
  Dim dicProcInfo As Object: Set dicProcInfo = CreateObject("Scripting.Dictionary")
  Set CallGraphShapes = New Collection

  #If DebugMode Then
    Dim VBCom As VBComponent
  #Else
    Dim VBCom As Object
  #End If
  'コールグラフ初期化
  Call ResetCallGraph
  
  '進捗バー初期化
  Set pb = New FormProgressBar
  pb.ShowModeless "実行します", wb
  
  For Each VBCom In wb.VBProject.VBComponents
    With VBCom.CodeModule
      If .CountOfDeclarationLines <> .CountOfLines Or _
         VBCom.Type = vbext_ct_ClassModule Or _
         VBCom.Type = vbext_ct_MSForm Then
        Dim cnt As Long: cnt = cnt + 1
        Call AddComponent2CallGraph(VBCom, cnt)
        Call getProcInfo(dicProcInfo, VBCom.CodeModule)
      End If
    End With
  Next VBCom

  If dicProcInfo.Count > 0 Then
    Call MakeCallGraph(dicProcInfo)
    Call addResult2Worksheet(dicProcInfo)
    If IsMakeWord Then Call makeDocument(fileName, dicProcInfo)
  End If
    
  pb.SelfClose
  If fileName <> ThisWorkbook.Name Then Workbooks(fileName).Close False
  
  CallGraph.Activate
  
  If dicProcInfo.Count = 0 Then Exit Sub
  
  If IsMakeWord Then
    MsgBox "このマクロと同じフォルダに仕様書を出力しました"
    Exit Sub
  End If
  
  MsgBox "結果を出力しました"
End Sub
```

##0.初期化
　初期化というか初期設定？

###VBA挙動管理
　Application.ScreenUpdatingを始めとしたVBAの実行時間を遅くする原因を止めます。以下のサイトを参考にクラスとプロパティで制御します。
　参考サイト：http://dev-clips.com/clip/vba/improve-performance-property/

```VB:画面の更新等の停止
  Dim exe As Settings:  Set exe = New Settings
```

```VB:Settings.cls
Option Explicit

Private Sub Class_Initialize()
  ExecuteMode = True
End Sub

Private Sub Class_Terminate()
  ExecuteMode = False
End Sub

Private Property Let ExecuteMode(ByVal mode As Boolean)
  With Application
    .ScreenUpdating = Not mode
    .EnableEvents = Not mode
    .Calculation = IIf(mode, xlCalculationManual, xlCalculationAutomatic)
  End With
End Property
```

###対象ファイルの取得
　対象となるファイル名を取得し、新規ブックとしてオープンします。`Application.GetOpenFilename`は特筆することはありませんが、AccessやWordのVBAも解析するのであれば然るべき処理が必要です。またVBADocを開きながらVBADocを開くわけにはいかないので、条件付きコンパイル（頭に#がついている行）でキャンセル時にVBADocのドキュメントを作成するデバッグモードを用意しています。
　余談ですが、この条件付きコンパイルは非常に便利な機能で、例えば参照設定で活躍します。
　外部オブジェクトは参照設定をオンにしてアーリーバインディングするかCreateObject()を使ってレイトバインディングしないと使用できません。（今回であれば、VBAでVBEを操作するための「Microsoft Visual Basic for Applications Extensibility」）
　アーリーバインディングしないとインテリセンス（Ctrl + Spaceで出るメンバー候補一覧）が使えないので、コーディングでは不便ですが、他人に渡したときに参照設定をオンにしないと使えないというデメリットがあります。
　条件付きコンパイルを使うことでパラメータを一つ更新するだけで挙動をコントロールできます。
　参考サイト：http://www.asahi-net.or.jp/~ef2o-inue/vba_o/sub05_800_500.html

```VB:対象ファイルの取得
  Dim fileName As String: fileName = openVBAFile

  #If DebugMode Then
    If fileName = "" Then fileName = ThisWorkbook.Name
  #Else
    If fileName = "" Then Exit Sub
  #End If

  Dim wb As Workbook: Set wb = Workbooks(fileName)
  Dim dicProcInfo As Object: Set dicProcInfo = CreateObject("Scripting.Dictionary")
  Set CallGraphShapes = New Collection

  #If DebugMode Then
    Dim VBCom As VBComponent
  #Else
    Dim VBCom As Object
  #End If
```

```VB:openVBAFile
Private Function openVBAFile() As String
  Dim targetFileName As String: targetFileName = _
      Application.GetOpenFilename("マクロ有効 Excelブック,*.xlsm")
  If targetFileName = "False" Then Exit Function
  
  Workbooks.Open targetFileName
  ActiveWindow.Visible = False
  openVBAFile = Mid(targetFileName, InStrRev(targetFileName, "\") + 1)
End Function
```


###コールグラフの初期化
　コールグラフの初期化。消すだけなら単純ですが、凡例を作成するところまでを実行します。
　オートシェイプの設置自体はsetShapeメソッドを別途作成し丸投げ。このように一定のまとまった処理はモジュール化して取り出していくのがよいでしょう。
　なお、各枠のマージン・幅・高さは、PL PT PW PH CL CT CW CHとパラメータを別途定数として宣言しています。
　また、配置については行列を指定して設置できるよう`getRowPosition() getColPosition()`で管理します。
　オートシェイプ設置時にあとでコールグラフを作成するために、コールグラフ取り扱い用のクラスを別途作り、Collectionに追加しておきます。

```VB:コールグラフの初期化
  Call ResetCallGraph
```

```VB:CallGraphModule.ResetCallGraph
Public Sub ResetCallGraph()
  With CallGraph
    .Activate
    .Unprotect
    .Shapes.SelectAll
  End With
  On Error Resume Next
  Selection.Delete
  On Error GoTo 0
  
  '凡例枠（親）
  Call setShape("", C_Component, 0, S_Parent, PL, PT, PW, PH, "凡例")
  With CallGraph.Shapes
    .Item(.Count).Line.DashStyle = msoLineDash
  End With

  '中身（子）
  Call setShape("全て表示", C_Component, 0, S_Child, _
                getColPosition(1), getRowPosition(1), CW, CH, "凡例_モード")
  CallGraph.Shapes("凡例_モード").ShapeStyle = msoShapeStylePreset10
  Call setShape("モジュール", C_Component, 0, S_Parent, _
                getColPosition(2), getRowPosition(1), CW, CH, "凡例_モジュール")
  Call setShape("フォーム", C_Component, -0.2, S_Parent, _
                getColPosition(3), getRowPosition(1), CW, CH, "凡例_フォーム")
  Call setShape("クラス", C_Component, -0.4, S_Parent, _
                getColPosition(4), getRowPosition(1), CW, CH, "凡例_クラス")
  Call setShape("太赤枠には呼び出し" & vbNewLine & "関係がありません", C_Component, 0, S_Parent, _
                getColPosition(5), getRowPosition(1), CW, CH, "凡例_注釈", vbWhite)
  
  Call setShape("Sub", C_Sub, 0.6, S_Child, _
                getColPosition(1), getRowPosition(2), CW, CH, "凡例_Sub")
  Call setShape("Function", C_Function, 0.6, S_Child, _
                getColPosition(2), getRowPosition(2), CW, CH, "凡例_Function")
  Call setShape("Property Let", C_Let, 0.6, S_Child, _
                getColPosition(3), getRowPosition(2), CW, CH, "凡例_Let")
  Call setShape("Property Set", C_Set, 0.6, S_Child, _
                getColPosition(4), getRowPosition(2), CW, CH, "凡例_Set")
  Call setShape("Property Get", C_Get, 0.6, S_Child, _
                getColPosition(5), getRowPosition(2), CW, CH, "凡例_Get")
            
  Call GroupingShapes("凡例")
End Sub

Private Function getColPosition(ByVal col_num As Long) As Single
  getColPosition = PL + CL + (CL + CW) * (col_num - 1)
End Function

Private Function getRowPosition(ByVal row_num As Long) As Single
  getRowPosition = PT + CT + (CT + CH) * (row_num - 1)
End Function

Private Sub setShape(ByVal proc_name As String, ByVal theme_color As Long, _
                     ByVal proc_brightness As Single, ByVal proc_shapetype As Long, _
                     ByVal proc_left As Single, ByVal proc_top As Single, _
                     ByVal proc_width As Single, ByVal proc_height As Single, _
                     Optional ByVal shape_name As String = "", _
                     Optional ByVal proc_line As Long = vbBlack)
  With CallGraph.Shapes.AddShape(proc_shapetype, proc_left, proc_top, proc_width, proc_height)
    With .Fill.ForeColor
      .ObjectThemeColor = theme_color
      .Brightness = proc_brightness
      .TintAndShade = 0
    End With
    
    With .Line
      .ForeColor.RGB = proc_line
      .Transparency = 0
    End With
    
    With .TextFrame
      .HorizontalOverflow = xlOartHorizontalOverflowOverflow
      .VerticalOverflow = xlOartVerticalOverflowOverflow
    End With
    
    With .TextFrame2
      .WordWrap = msoFalse
      .VerticalAnchor = msoAnchorMiddle
      
      With .TextRange
        .Text = proc_name
        .ParagraphFormat.Alignment = IIf(proc_line = vbWhite, msoAlignLeft, msoAlignCenter)
        .Font.Fill.ForeColor.RGB = vbBlack
      End With
    End With
    .OnAction = "MakeActiveCallGraph"
  End With
  
  With CallGraph.Shapes
    .Item(.Count).Name = IIf(shape_name = "", proc_name, shape_name)
    .Item(.Count).ZOrder msoBringToFront
    .Item(.Count).Select False
    
    Dim s As ClsCallGraphShape: Set s = New ClsCallGraphShape
    Set s.Initialize = .Item(.Count)
    CallGraphShapes.Add s, s.CallGraphName
  End With
End Sub
```

```VB:コールグラフを管理する処理
    Dim s As ClsCallGraphShape: Set s = New ClsCallGraphShape
    Set s.Initialize = .Item(.Count)
    CallGraphShapes.Add s, s.CallGraphName
```

###プログレスバーの初期化
　上記サイトを参考にプログレスバーのフォームを作成します。
　進捗管理のためには走査する対象プロジェクトの数を最大値として取得しておきます。


```VB:プログレスバーの初期化
  '進捗バー初期化
  Set pb = New FormProgressBar
  pb.ShowModeless "実行します", wb
```

```VB:FormProgressBar.ShowModeless
Public Sub ShowModeless(Optional ByVal title As String = "", _
                        Optional ByRef wb As Workbook)
  'ラベルコントロール追加
  Set progressBar_ = Me.FrameProgressBar.Controls.Add("Forms.Label.1", "lblProgress")
  If barColor_ = 0 Then barColor_ = RGB(0, 0, 128)
  With progressBar_
    .Width = 0
    .Height = Me.FrameProgressBar.Height
    .BackColor = barColor_
  End With

  'プログレスバーの背景をへこませる
  Me.FrameProgressBar.SpecialEffect = fmSpecialEffectSunken

  '割込み拒否の設定
  If interactive_ = False Then
    Me.Enabled = False                           'これは好みで
    Application.Interactive = False
    Application.EnableCancelKey = xlDisabled
  End If

  '最大値の設定
  Dim VBCom As Object
  For Each VBCom In wb.VBProject.VBComponents
    maxValue_ = maxValue_ + CountModule(VBCom.CodeModule)
  Next VBCom

  Me.Caption = title
  Me.Show vbModeless
End Sub

Private Function CountModule(code_module As Object) As Long
  Dim i As Long: i = code_module.CountOfDeclarationLines + 1
  Do Until i > code_module.CountOfLines
    Dim tmpProc As String, tmpKind As Long
    tmpProc = code_module.ProcOfLine(i, tmpKind)
    Dim tmp As Long: tmp = tmp + 1
    i = i + code_module.ProcCountLines(tmpProc, tmpKind)
  Loop
  CountModule = tmp
End Function
```

　この辺からVBEを操作するVBAの始まりです。
　Workbookオブジェクトの配下のVBProjectオブジェクトの配下にVBComponent(s)が格納されています。
　このコンポーネントがプロジェクトエクスプローラー（VBEの左側）に表示されている標準モジュール、クラス、フォーム、シートモジュール等に対応しています。
　このVBComponentのさらに配下にCodeModuleオブジェクトがおり、こいつがコードを操作するメソッドを持っています。
　（リファレンス：https://docs.microsoft.com/ja-jp/office/vba/api/access.module）
　VBAでVBEが操作できるとはいいましたが、調べれば調べるほど碌なメソッドがなく、ほんとに欲しいものは自作しなくてはいけないという大変さ。
　例えばコンポーネント内のモジュール（プロシージャ数）を数えるメソッドはありません。
　そのため、「宣言部分の行数を取得するメソッド」「コードの何行目がどのプロシージャに含まれているかを取得するメソッド」「対象のプロシージャの行数を取得するメソッド」を使用して「宣言部の次の行から、その行が何のプロシージャの一部化を調べカウントし、そのプロシージャの行数だけ次へ読み飛ばすことを末尾まで繰り返す」という力業なCountModuleメソッドを作成します。
　こんなことの繰り返しです。

#次回予告
　初期化するだけで結構な分量になってしまいました。解説が下手くそなのかもしれません。
　思った以上に反響がありそうなので、少しずつ投稿していきたいと思います。
　（[次回](https://qiita.com/ando_yu/items/dd6a15d863b098bb0b2a)へ続く）