#読むのが面倒な人へ
VBAからVBAの仕様書を作るVBAを書きました。(完成品 一時停止中です)
~~※たぶん大丈夫だとは思いますが、自己責任でお願いします。~~大丈夫じゃなかった。
#前回までのあらすじ
VBAでVBEを操作できるので、VBAのコードを解析してJavaDoc的なことができないか?
#設定
VBAでVBEを扱うためには参照設定が必要です。(参考サイト)
#参考サイト
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への出力
##主たるプロシージャ
実際にはこれをリボンから呼び出します。
また、実行制御のクラスや進捗管理のフォーム、結果管理のクラスなどがたくさんあるので、これ単体では動きません。
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/
Dim exe As Settings: Set exe = New Settings
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
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
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に追加しておきます。
Call 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
Dim s As ClsCallGraphShape: Set s = New ClsCallGraphShape
Set s.Initialize = .Item(.Count)
CallGraphShapes.Add s, s.CallGraphName
###プログレスバーの初期化
上記サイトを参考にプログレスバーのフォームを作成します。
進捗管理のためには走査する対象プロジェクトの数を最大値として取得しておきます。
'進捗バー初期化
Set pb = New FormProgressBar
pb.ShowModeless "実行します", wb
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メソッドを作成します。
こんなことの繰り返しです。
#次回予告
初期化するだけで結構な分量になってしまいました。解説が下手くそなのかもしれません。
思った以上に反響がありそうなので、少しずつ投稿していきたいと思います。
(次回へ続く)