Microsoft Accessで長期間運用されているデータベースシステムでは、レポート機能が業務上重要な役割を担っていることが多くあります。とくに紙ベースで運用されてきた現場などでは顕著でしょう。しかし、システムの改修やWebアプリケーションへの移行などを検討する際、これらのレポートの正確な仕様を把握することは大きな課題となります。ドキュメントが古くなっていたり、そもそも存在しなかったりする場合、手作業で各レポートのデザインやVBAコードを調査するには膨大な時間と労力が必要です。
つまり、これも大抵の場合はメチャクチャ大変ってことです_( _´ω`)_ペショ
このVBAスクリプトは、このような背景から既存のAccessデータベース内に存在するすべてのレポートオブジェクトの技術的な仕様を自動的に調査・分析し、その結果をMarkdown形式のレポートとして出力することを目的としています。
このコードは、これらの課題に対応するため、以下のニーズに応えることを目指しています。
今回のソースコードは、以下の環境で動作を確認しています。
Option Compare Database
Option Explicit
' 統合MDB分析ツール (全フォームのVBAコード分析対応)
' データベース構造の包括的な把握と、新システム移行のための機能分析を支援
Sub AnalyzeAllReportSpecificationsToMarkdown()
' プロジェクト内の全レポートの仕様を調査し、個別のMarkdownレポートを生成する
Dim rptObj As AccessObject
Dim reportName As String
Dim strFilePath As String
Dim intFileNum As Integer
Dim vbProj As Object ' As VBIDE.VBProject
Dim vbCrntProj As Object ' As Application.CurrentProject
Dim analysisCounter As Long: analysisCounter = 0
Dim errorCounter As Long: errorCounter = 0
Dim overallStatus As String: overallStatus = ""
' On Error GoTo ErrorHandler_Overall ' デバッグ中はコメントアウト推奨
' VBEプロジェクトの取得 (最初に一度だけ)
Set vbCrntProj = Application.CurrentProject
If vbCrntProj Is Nothing Then
MsgBox "VBAプロジェクトにアクセスできませんでした。参照設定やAccessのオプションを確認してください。", vbCritical, "初期化エラー"
Exit Sub
End If
If vbCrntProj.AllReports.count = 0 Then
MsgBox "分析対象のレポートが見つかりません。", vbInformation, "情報"
Exit Sub
End If
Set vbProj = Application.VBE.ActiveVBProject ' Application.CodeProject の方が堅牢な場合がある
' 全レポートをループ処理
For Each rptObj In vbCrntProj.AllReports
reportName = rptObj.Name
strFilePath = vbCrntProj.Path & "\" & ReplaceValidFileNameChars(reportName) & "_ReportSpec_Analysis.md"
Debug.Print "レポート仕様分析開始: " & reportName
intFileNum = FreeFile
Open strFilePath For Output As #intFileNum
' レポートヘッダー
Print #intFileNum, "# レポート仕様分析: `" & MDEscape(reportName) & "`"
Print #intFileNum, ""
Print #intFileNum, "**データベース:** `" & MDEscape(CurrentProject.Name) & "`"
Print #intFileNum, "**分析日時:** " & Now()
Print #intFileNum, ""
Print #intFileNum, "---"
Print #intFileNum, ""
' On Error GoTo ErrorHandler_ReportLevel ' レポートごとのエラー処理 (デバッグ中はコメントアウト推奨)
Dim rpt As Report ' Reportオブジェクト用
Dim designViewOpened As Boolean: designViewOpened = False
' レポートをデザインビューで開く (プロパティ取得のため)
' On Error Resume Next ' 個別のエラー処理に任せる
DoCmd.OpenReport reportName, acViewDesign, , , acHidden
If Err.Number = 0 Then
Set rpt = Reports(reportName)
designViewOpened = True
Else
Print #intFileNum, "## エラー"
Print #intFileNum, ""
Print #intFileNum, "- レポート `" & MDEscape(reportName) & "` をデザインビューで開けませんでした。エラー: " & MDEscape(Err.Description)
Err.Clear
errorCounter = errorCounter + 1
overallStatus = overallStatus & vbCrLf & "- " & reportName & ": デザインビューオープンエラー"
GoTo NextReportIteration
End If
' On Error GoTo ErrorHandler_ReportLevel ' 必要に応じて戻す
' --- 1. レポートの目的と利用者 (手動確認項目) ---
Print #intFileNum, "## 1. レポートの目的と利用者 (手動確認推奨)"
Print #intFileNum, ""
Print #intFileNum, "- **そのレポートが何のために作成され、誰が、どのような目的で利用しているか?** (ヒアリング等で確認)"
Print #intFileNum, "- **レポートからどのような情報を得ようとしているか?** (ヒアリング等で確認)"
Print #intFileNum, "- **レポートの利用頻度**(毎日、毎週、毎月、特定のイベント時など)。 (ヒアリング等で確認)"
Print #intFileNum, "- **レポートを参照した後、利用者は次にどのようなアクションを取るか?** (ヒアリング等で確認)"
Print #intFileNum, ""
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 2. データソースとデータ取得ロジック ---
AnalyzeReportDataSourceMD intFileNum, rpt, reportName, vbProj
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 3. レイアウトと表示項目 ---
AnalyzeReportLayoutMD intFileNum, rpt
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 4. VBAコードとマクロ ---
AnalyzeReportCodeAndMacroMD intFileNum, rpt, reportName, vbProj
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 5. 出力形式と用途 ---
AnalyzeReportOutputFormatMD intFileNum, rpt
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 6. パフォーマンスとデータ量 (手動確認項目) ---
Print #intFileNum, "## 6. パフォーマンスとデータ量 (手動確認推奨)"
Print #intFileNum, ""
Print #intFileNum, "- **レポートの表示や印刷にどの程度の時間がかかっているか?** (実測またはヒアリングで確認)"
Print #intFileNum, "- **対象となるデータ量はどの程度か?**(レコード数、期間など) (データソース調査と併せて確認)"
Print #intFileNum, "- **パフォーマンスに関するユーザーからの不満はあるか?** (ヒアリング等で確認)"
Print #intFileNum, ""
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 7. セキュリティとアクセス制御 (手動確認項目) ---
Print #intFileNum, "## 7. セキュリティとアクセス制御 (手動確認推奨)"
Print #intFileNum, ""
Print #intFileNum, "- **そのレポートを誰が閲覧できるべきか?**(特定の役職、部署、ユーザーなど) (ヒアリング等で確認)"
Print #intFileNum, "- **レポートの内容に、機密性の高い情報や個人情報は含まれているか?** (内容確認)"
Print #intFileNum, "- **Access側で、レポートへのアクセスに関して何らかの制御が行われているか?** (オブジェクトプロパティ、VBA、ユーザーレベルセキュリティ等の確認)"
Print #intFileNum, ""
Print #intFileNum, "---"
Print #intFileNum, ""
' --- 8. 改善要望と将来の拡張性 (手動確認項目) ---
Print #intFileNum, "## 8. 改善要望と将来の拡張性 (手動確認推奨)"
Print #intFileNum, ""
Print #intFileNum, "- **現在のレポートに関して、利用者からの改善要望や不満点はあるか?** (ヒアリング等で確認)"
Print #intFileNum, "- **将来的に、このレポートに関してどのような機能追加や変更が予想されるか?** (ヒアリング等で確認)"
Print #intFileNum, ""
analysisCounter = analysisCounter + 1
overallStatus = overallStatus & vbCrLf & "- " & reportName & ": 正常終了"
NextReportIteration:
If designViewOpened Then
On Error Resume Next ' 既に閉じている場合のエラーを無視
DoCmd.Close acReport, reportName, acSaveNo
On Error GoTo 0 ' エラーハンドラを全体に戻す前にリセット
End If
Set rpt = Nothing
If intFileNum > 0 Then Close #intFileNum
intFileNum = 0 ' ファイルハンドルをリセット
' On Error GoTo ErrorHandler_Overall ' エラーハンドラを全体に戻す (ループの最後に移動)
Next rptObj
On Error GoTo 0 ' メッセージ表示前のエラーハンドラ解除
' --- 全体処理の完了メッセージ ---
Dim summaryMsg As String
summaryMsg = "全レポートの仕様分析が完了しました。" & vbCrLf & vbCrLf & _
"処理済みレポート数: " & analysisCounter & vbCrLf & _
"エラー発生レポート数: " & errorCounter & vbCrLf & vbCrLf & _
"レポートは各レポート名のファイルとして、データベースと同じフォルダに保存されました。"
If errorCounter > 0 Then
summaryMsg = summaryMsg & vbCrLf & vbCrLf & "エラーが発生したレポートの詳細:" & overallStatus
MsgBox summaryMsg, vbExclamation, "分析完了 (一部エラーあり)"
Else
MsgBox summaryMsg, vbInformation, "分析完了"
End If
Exit Sub
ErrorHandler_ReportLevel:
Print #intFileNum, "## レポートレベルのエラー"
Print #intFileNum, ""
Print #intFileNum, "レポート `" & MDEscape(reportName) & "` の分析処理中に予期せぬエラーが発生しました。"
Print #intFileNum, "- **エラー番号:** " & Err.Number
Print #intFileNum, "- **エラー内容:** `" & MDEscape(Err.Description) & "`"
Debug.Print "レポート `" & MDEscape(reportName) & "` の分析処理中に予期せぬエラーが発生しました。"
Debug.Print "- **エラー内容:** `" & MDEscape(Err.Description) & "`"
errorCounter = errorCounter + 1
overallStatus = overallStatus & vbCrLf & "- " & reportName & ": 分析中エラー (" & Err.Number & ")"
Resume NextReportIteration ' エラーが発生したら次のレポートの処理へ
ErrorHandler_Overall:
MsgBox "分析処理全体で予期せぬエラーが発生しました。" & vbCrLf & _
"エラー番号: " & Err.Number & vbCrLf & _
"エラー内容: " & Err.Description, vbCritical, "重大なエラー"
If intFileNum > 0 Then Close #intFileNum ' 開いている可能性のあるファイルを閉じる
End Sub
' ファイル名として使用できない文字を置換するヘルパー関数
Function ReplaceValidFileNameChars(originalName As String) As String
Dim invalidChars As String
Dim i As Long
Dim char As String
Dim resultName As String
invalidChars = "\/:*?""<>|"
resultName = originalName
For i = 1 To Len(invalidChars)
char = Mid(invalidChars, i, 1)
resultName = Replace(resultName, char, "_")
Next i
ReplaceValidFileNameChars = resultName
End Function
Sub AnalyzeReportDataSourceMD(ByVal FileNum As Integer, ByVal rpt As Report, ByVal reportName As String, ByVal vbProj As Object)
' レポートのデータソース関連情報を分析
Print #FileNum, "## 2. データソースとデータ取得ロジック"
Print #FileNum, ""
If rpt Is Nothing Then
Print #FileNum, "- レポートオブジェクトにアクセスできませんでした。"
Exit Sub
End If
Print #FileNum, "- **レコードソース:**"
If Trim(rpt.RecordSource) = "" Then
Print #FileNum, " - `(設定なし)`"
Else
Print #FileNum, " ```sql"
Print #FileNum, Trim(rpt.RecordSource)
Print #FileNum, " ```"
' レコードソースがクエリ名の場合、そのSQLも表示しようと試みる
Dim qdf As DAO.QueryDef
On Error Resume Next
Set qdf = CurrentDb.QueryDefs(rpt.RecordSource)
If Err.Number = 0 And Not qdf Is Nothing Then
Print #FileNum, " - **上記レコードソースがクエリの場合のSQL定義:**"
Print #FileNum, " ```sql"
Print #FileNum, Trim(qdf.SQL)
Print #FileNum, " ```"
Else
Print #FileNum, " - (レコードソースはテーブル名、または直接SQL文の可能性があります)"
End If
Err.Clear
On Error GoTo 0 ' 通常のエラー処理に戻す
Set qdf = Nothing
End If
Print #FileNum, ""
' パラメータ (レポートのFilterプロパティや、レコードソースのクエリがパラメータクエリかなど)
' Filterプロパティ
If Trim(rpt.Filter) <> "" Then
Print #FileNum, "- **フィルタ (プロパティ):** `" & MDEscape(rpt.Filter) & "`"
Print #FileNum, "- **フィルタ適用中 (FilterOnプロパティ):** " & IIf(rpt.FilterOn, "はい", "いいえ")
Else
Print #FileNum, "- **フィルタ (プロパティ):** 設定なし"
End If
' パラメータクエリの検出は困難なので、レコードソースのSQLに [パラメータ名] が含まれるかなどで推測
If InStr(1, rpt.RecordSource, "[") > 0 And InStr(1, rpt.RecordSource, "]") > 0 Then
Print #FileNum, "- **レコードソースにパラメータが含まれる可能性あり。** (例: `[パラメータ名]`) SQL文を確認してください。"
End If
Print #FileNum, ""
' VBAやマクロによる動的変更の可能性 (VBAコード分析は別セクション)
Print #FileNum, "- **VBA/マクロによる動的変更:**"
If rpt.hasModule Then ' 小文字のh
Dim compName As String, codeMod As Object
compName = "Report_" & reportName
On Error Resume Next
Set codeMod = vbProj.VBComponents(compName).CodeModule ' Itemは不要な場合あり
If Err.Number = 0 And Not codeMod Is Nothing Then
If InStr(1, codeMod.Lines(1, codeMod.CountOfLines), ".RecordSource", vbTextCompare) > 0 Or _
InStr(1, codeMod.Lines(1, codeMod.CountOfLines), ".Filter", vbTextCompare) > 0 Or _
InStr(1, codeMod.Lines(1, codeMod.CountOfLines), ".FilterOn", vbTextCompare) > 0 Then
Print #FileNum, " - VBAコード内で RecordSource や Filter プロパティを操作している可能性があります。詳細はVBAコード分析セクションを参照。"
Else
Print #FileNum, " - VBAコードは存在しますが、RecordSource/Filter操作は見つかりませんでした(簡易チェック)。"
End If
Else
Print #FileNum, " - VBAモジュールはありますが、コードにアクセスできませんでした。エラー: " & Err.Description
End If
Err.Clear
On Error GoTo 0 ' 通常のエラー処理に戻す
Set codeMod = Nothing
Else
Print #FileNum, " - VBAモジュールなし。"
End If
If Trim(rpt.OnOpen) <> "" Or Trim(rpt.OnNoData) <> "" Then
Print #FileNum, " - OnOpenまたはOnNoDataイベントに処理が設定されています(マクロまたはVBAの可能性)。"
End If
Print #FileNum, ""
' リンクテーブルや外部データソースの参照は、レコードソースのSQLから推測
Print #FileNum, "- **外部データソース参照の可能性:** レコードソースのSQL文を確認し、リンクテーブルや外部DB名が含まれていないか調査してください。"
Print #FileNum, ""
End Sub
Sub AnalyzeReportLayoutMD(ByVal FileNum As Integer, ByVal rpt As Report)
' レポートのレイアウトと表示項目を分析
' グループ化に関するロジックは削除
Dim ctl As Control
Dim sec As Section
Print #FileNum, "## 3. レイアウトと表示項目"
Print #FileNum, ""
If rpt Is Nothing Then
Print #FileNum, "- レポートオブジェクトにアクセスできませんでした。"
Exit Sub
End If
Print #FileNum, "### 表示コントロール一覧 (主要プロパティ)"
Print #FileNum, ""
If rpt.Controls.count > 0 Then
For Each ctl In rpt.Controls
Print #FileNum, "- **コントロール名:** `" & MDEscape(ctl.Name) & "`"
Print #FileNum, " - **種類:** " & MDEscape(TypeName(ctl)) & " (ControlType: " & ctl.ControlType & ")"
If ctl.ControlType = acLabel Then
Print #FileNum, " - **キャプション:** `" & MDEscape(ctl.Caption) & "`"
ElseIf ctl.ControlType = acTextBox Or ctl.ControlType = acComboBox Or ctl.ControlType = acListBox Then
If Trim(ctl.ControlSource) <> "" Then Print #FileNum, " - **コントロールソース:** `" & MDEscape(ctl.ControlSource) & "`"
If Trim(ctl.Format) <> "" Then Print #FileNum, " - **書式:** `" & MDEscape(ctl.Format) & "`"
If Trim(ctl.InputMask) <> "" Then Print #FileNum, " - **入力マスク:** `" & MDEscape(ctl.InputMask) & "`"
ElseIf ctl.ControlType = acImage Then
If Trim(ctl.Picture) <> "" Then Print #FileNum, " - **ピクチャ:** `" & MDEscape(ctl.Picture) & "`"
ElseIf ctl.ControlType = acSubform Or ctl.ControlType = acSubReport Then ' サブレポートも考慮
Print #FileNum, " - **ソースオブジェクト:** `" & MDEscape(ctl.SourceObject) & "`"
End If
If Trim(ctl.Tag) <> "" Then Print #FileNum, " - **タグ:** `" & MDEscape(ctl.Tag) & "`"
Print #FileNum, " - **セクション:** " & SectionCodeToString(ctl.Section)
Print #FileNum, ""
Next ctl
Else
Print #FileNum, "*表示コントロールはありません。*" & vbCrLf
End If
Print #FileNum, "### グループ化と並べ替え"
Print #FileNum, ""
Print #FileNum, "- **グループ化:** 手動でレポートデザインを確認してください。" ' グループ化情報を手動確認するよう促す
If Trim(rpt.OrderBy) <> "" Then
Print #FileNum, "- **並べ替え (OrderByプロパティ):** `" & MDEscape(rpt.OrderBy) & "`"
Print #FileNum, "- **並べ替え適用中 (OrderByOnプロパティ):** " & IIf(rpt.OrderByOn, "はい", "いいえ")
Else
Print #FileNum, "- **並べ替え (OrderByプロパティ):** 設定なし"
End If
Print #FileNum, ""
Print #FileNum, "### セクション情報"
Print #FileNum, ""
On Error Resume Next ' Sectionコレクションへのアクセスエラー対策
Dim i As Integer
' レポートセクションのインデックスは acDetail から始まる定数で直接指定する方が確実
Dim sectionTypes As Variant
sectionTypes = Array(acDetail, acHeader, acFooter, acPageHeader, acPageFooter, _
acGroupLevel1Footer, acGroupLevel1Header, _
acGroupLevel2Footer, acGroupLevel2Header) ' 他のグループも必要なら追加
For Each i In sectionTypes
On Error Resume Next ' 特定のセクションが存在しない場合のエラーをハンドル
Set sec = rpt.Section(i)
If Err.Number = 0 And Not sec Is Nothing Then
Print #FileNum, "- **セクション:** " & MDEscape(sec.Name) & " (" & SectionCodeToString(sec.SectionType) & ")"
Print #FileNum, " - **表示:** " & IIf(sec.Visible, "はい", "いいえ")
Print #FileNum, " - **高さ:** " & sec.Height
If sec.SectionType = acGroupLevel1Header Or sec.SectionType = acGroupLevel1Footer Or _
sec.SectionType = acGroupLevel2Header Or sec.SectionType = acGroupLevel2Footer Then
If Trim(sec.Tag) <> "" Then Print #FileNum, " - **タグ:** `" & MDEscape(sec.Tag) & "`"
End If
ElseIf Err.Number <> 0 And Err.Number <> 2459 Then ' 2459: セクションが存在しない
Print #FileNum, "- (セクション " & SectionCodeToString(i) & " の情報取得エラー: " & Err.Description & ")"
End If
Err.Clear
Set sec = Nothing
On Error GoTo 0 ' 通常のエラー処理に戻す
Next i
If Err.Number <> 0 Then Print #FileNum, "- (セクション情報の一部取得で予期せぬエラー発生)": Err.Clear
On Error GoTo 0
Print #FileNum, ""
Print #FileNum, "- **条件付き書式の有無:** 手動でレポートデザインを確認してください。"
Print #FileNum, "- **画像/OLEオブジェクト/グラフの有無:** 上記コントロール一覧とレポートデザインを手動で確認してください。"
Print #FileNum, ""
End Sub
Sub AnalyzeReportCodeAndMacroMD(ByVal FileNum As Integer, ByVal rpt As Report, ByVal reportName As String, ByVal vbProj As Object)
' レポートのVBAコードとマクロを分析
Print #FileNum, "## 4. VBAコードとマクロ"
Print #FileNum, ""
If rpt Is Nothing Then
Print #FileNum, "- レポートオブジェクトにアクセスできませんでした。"
Exit Sub
End If
Print #FileNum, "- **VBAモジュールの有無:** " & IIf(rpt.hasModule, "**あり**", "なし") ' 小文字のh
If rpt.hasModule Then
Dim vbComp As Object, codeMod As Object
Dim compName As String: compName = "Report_" & reportName
On Error Resume Next
Set vbComp = vbProj.VBComponents(compName) ' Itemは不要な場合あり
If vbComp Is Nothing Then Set vbComp = vbProj.VBComponents(reportName) ' Itemなしも試す
If Err.Number = 0 And Not vbComp Is Nothing Then
Set codeMod = vbComp.CodeModule
If Not (codeMod Is Nothing) And codeMod.CountOfLines > 1 Then ' 宣言行のみは除く
Print #FileNum, " - **VBAコード総行数:** " & codeMod.CountOfLines
Print #FileNum, " - **注:** 詳細なVBAコード分析は、フォーム分析と同様のツール/手法で別途実施することを推奨します。"
' イベントプロシージャの存在チェック (簡易)
Dim eventProcList As String: eventProcList = ""
Dim procInfo As Variant ' 配列を想定
Dim allProcs As Collection
Set allProcs = GetAllProceduresFromCodeModule(codeMod) ' 既存関数を再利用
For Each procInfo In allProcs
If Left(LCase(procInfo(0)), 7) = "report_" Or InStr(1, LCase(procInfo(0)), "_format") > 0 Or InStr(1, LCase(procInfo(0)), "_print") > 0 Or InStr(1, LCase(procInfo(0)), "_retreat") > 0 Or InStr(1, LCase(procInfo(0)), "_nodata") > 0 Then
eventProcList = eventProcList & "`" & MDEscape(procInfo(0)) & "`, "
End If
Next procInfo
If eventProcList <> "" Then
eventProcList = Left(eventProcList, Len(eventProcList) - 2)
Print #FileNum, " - **主要イベントプロシージャ候補:** " & eventProcList
End If
Else
Print #FileNum, " - VBAモジュールはありますが、実質的なコードは少ないか、アクセスできませんでした。"
End If
Else
Print #FileNum, " - VBAコンポーネントにアクセスできませんでした。エラー: " & Err.Description
End If
Err.Clear
On Error GoTo 0 ' 通常のエラー処理に戻す
Set codeMod = Nothing: Set vbComp = Nothing
End If
Print #FileNum, ""
' 主要イベントプロパティのマクロ設定確認
Dim eventProps As Variant
Dim prop As Variant
eventProps = Array("OnOpen", "OnClose", "OnActivate", "OnDeactivate", "OnError", "OnNoData", "OnPage")
Dim macroFound As Boolean: macroFound = False
For Each prop In eventProps
On Error Resume Next ' プロパティが存在しない場合もある
Dim propValue As String
propValue = rpt.Properties(prop).Value
If Err.Number = 0 And Trim(propValue) <> "" And Left(Trim(propValue), 1) <> "[" Then ' "["で始まらないのはマクロ名
Print #FileNum, "- **イベントプロパティ `" & MDEscape(CStr(prop)) & "` にマクロ設定あり:** `" & MDEscape(propValue) & "`"
macroFound = True
End If
Err.Clear
On Error GoTo 0 ' 通常のエラー処理に戻す
Next prop
If Not macroFound Then
Print #FileNum, "- **主要イベントプロパティにマクロ設定は見つかりませんでした(簡易チェック)。**"
End If
Print #FileNum, ""
End Sub
Sub AnalyzeReportOutputFormatMD(ByVal FileNum As Integer, ByVal rpt As Report)
' レポートの出力形式関連情報を分析
Print #FileNum, "## 5. 出力形式と用途"
Print #FileNum, ""
If rpt Is Nothing Then
Print #FileNum, "- レポートオブジェクトにアクセスできませんでした。"
Exit Sub
End If
Print #FileNum, "- **主に画面プレビューか印刷か、エクスポート利用か:** (ヒアリング等で確認)"
Print #FileNum, ""
Print #FileNum, "### ページ設定 (一部抜粋)"
Print #FileNum, ""
On Error Resume Next ' Printerオブジェクトがない場合がある
If Not rpt.Printer Is Nothing Then
With rpt.Printer
Print #FileNum, "- **用紙サイズ:** " & PaperSizeToString(.PaperSize) & " (" & .PaperSize & ")"
Print #FileNum, "- **向き:** " & IIf(.Orientation = acPRORLandscape, "横", "縦") & " (" & .Orientation & ")"
Print #FileNum, "- **左余白:** " & Format(.LeftMargin / 1440, "0.00") & "インチ (" & .LeftMargin & " twips)" ' 1 inch = 1440 twips
Print #FileNum, "- **右余白:** " & Format(.RightMargin / 1440, "0.00") & "インチ (" & .RightMargin & " twips)"
Print #FileNum, "- **上余白:** " & Format(.TopMargin / 1440, "0.00") & "インチ (" & .TopMargin & " twips)"
Print #FileNum, "- **下余白:** " & Format(.BottomMargin / 1440, "0.00") & "インチ (" & .BottomMargin & " twips)"
End With
Else
Print #FileNum, "- プリンタオブジェクトにアクセスできませんでした。ページ設定は手動で確認してください。"
End If
If Err.Number <> 0 Then Print #FileNum, "- ページ設定情報取得エラー: " & MDEscape(Err.Description): Err.Clear
On Error GoTo 0 ' 通常のエラー処理に戻す
Print #FileNum, ""
Print #FileNum, "- **定期的な自動印刷/エクスポートの有無:** (バッチ処理やタスクスケジューラの確認)"
Print #FileNum, ""
End Sub
' --- ユーティリティ関数群 (一部新規・変更あり) ---
Function ReplaceValidFileNameChars(originalName As String) As String
Dim invalidChars As String, i As Long, char As String, resultName As String
invalidChars = "\/:*?""<>|": resultName = originalName
For i = 1 To Len(invalidChars)
char = Mid(invalidChars, i, 1): resultName = Replace(resultName, char, "_")
Next i
ReplaceValidFileNameChars = resultName
End Function
Function GetAllProceduresFromCodeModule(ByVal codeMod As Object) As Collection
Dim procs As New Collection, procName As String, procKind As Long, currentLine As Long
Dim lastProcName As String: lastProcName = "###INITIAL_VALUE###"
Dim tempProcInfo(0 To 1) As Variant
If codeMod Is Nothing Or codeMod.CountOfLines = 0 Then Set GetAllProceduresFromCodeModule = procs: Exit Function
On Error GoTo ErrorHandler_GetAllProcs
currentLine = 1
Do While currentLine <= codeMod.CountOfLines
Dim procNameAtLine As String, procKindAtLine As Long
On Error Resume Next
procNameAtLine = codeMod.ProcOfLine(currentLine, procKindAtLine)
If Err.Number <> 0 Then procNameAtLine = "": Err.Clear
On Error GoTo ErrorHandler_GetAllProcs
If procNameAtLine <> "" Then
If procNameAtLine <> lastProcName Then
tempProcInfo(0) = procNameAtLine: tempProcInfo(1) = procKindAtLine
On Error Resume Next
procs.Add Item:=tempProcInfo, Key:=CStr(procNameAtLine)
If Err.Number <> 0 Then Err.Clear
On Error GoTo ErrorHandler_GetAllProcs
lastProcName = procNameAtLine
End If
Dim procStart As Long, procLinesCount As Long
procStart = codeMod.ProcStartLine(procNameAtLine, procKindAtLine)
procLinesCount = codeMod.ProcCountLines(procNameAtLine, procKindAtLine)
If procLinesCount > 0 Then currentLine = procStart + procLinesCount Else currentLine = currentLine + 1
Else
currentLine = currentLine + 1
End If
Loop
Set GetAllProceduresFromCodeModule = procs
Exit Function
ErrorHandler_GetAllProcs:
Debug.Print "GetAllProceduresFromCodeModule Error: " & Err.Number & " - " & Err.Description
Set GetAllProceduresFromCodeModule = procs
End Function
Function GetFormControlNames(objectName As String, Optional isReport As Boolean = True) As Collection
' レポートまたはフォーム上のコントロール名一覧を取得する
Dim ctl As Control, col As New Collection, objOwner As Object, designViewOpened As Boolean
Dim objectType As AcObjectType
Set col = New Collection ' 毎回新しいコレクションを生成
If isReport Then objectType = acReport Else objectType = acForm
On Error Resume Next
' オブジェクトが既に開いているか、どのビューで開いているかを確認
Dim currentState As Long
currentState = SysCmd(acSysCmdGetObjectState, objectType, objectName)
designViewOpened = False ' 初期化
If currentState <> 0 Then ' 何らかの形で開いている
If isReport Then
Set objOwner = Reports(objectName)
If objOwner.CurrentView <> acViewDesign Then ' デザインビューでなければ開きなおす
DoCmd.Close objectType, objectName, acSaveNo ' 一旦閉じる
DoCmd.OpenReport objectName, acViewDesign, , , acHidden
Set objOwner = Reports(objectName)
designViewOpened = True ' 新しく開いたフラグ
End If
Else ' フォームの場合
Set objOwner = Forms(objectName)
If objOwner.CurrentView <> acDesign Then
DoCmd.Close objectType, objectName, acSaveNo
DoCmd.OpenForm objectName, acDesign, , , , acHidden
Set objOwner = Forms(objectName)
designViewOpened = True
End If
End If
Else ' 開いていない場合
If isReport Then
DoCmd.OpenReport objectName, acViewDesign, , , acHidden
Set objOwner = Reports(objectName)
Else
DoCmd.OpenForm objectName, acDesign, , , , acHidden
Set objOwner = Forms(objectName)
End If
designViewOpened = True
End If
If Err.Number <> 0 Then
Debug.Print "GetFormControlNames: オブジェクト '" & objectName & "' のオープン/参照エラー: " & Err.Description
Err.Clear
Set GetFormControlNames = col ' 空のコレクションを返す
Exit Function
End If
On Error GoTo 0
If Not objOwner Is Nothing Then
On Error Resume Next ' コントロールアクセスエラー対策
For Each ctl In objOwner.Controls
col.Add ctl.Name, ctl.Name ' キーも同じ名前で追加(重複回避のため)
Next ctl
If Err.Number <> 0 Then
Debug.Print "GetFormControlNames: コントロール取得エラー in '" & objectName & "': " & Err.Description
Err.Clear
End If
On Error GoTo 0
If designViewOpened And Not objOwner Is Nothing Then ' 新しくデザインビューで開いた場合のみ閉じる
On Error Resume Next ' 閉じる際のエラーは無視
DoCmd.Close objectType, objectName, acSaveNo
On Error GoTo 0
End If
Else
Debug.Print "GetFormControlNames: オブジェクト '" & objectName & "' にアクセスできませんでした。"
End If
Set GetFormControlNames = col
End Function
Function GetEventProcedureInfo(procName As String, controlNames As Collection, Optional objectTypeName As String = "Report") As String
' プロシージャ名からイベント情報を推測する
Dim parts() As String, objectName As String, eventName As String, ctlName As Variant
GetEventProcedureInfo = ""
If InStr(procName, "_") = 0 Then Exit Function
parts = Split(procName, "_")
If UBound(parts) < 1 Then Exit Function
objectName = parts(0): eventName = parts(1)
If LCase(objectName) = LCase(objectTypeName) Then
GetEventProcedureInfo = MDEscape(objectTypeName) & " の " & MDEscape(eventName) & " イベント"
Exit Function
End If
On Error Resume Next
For Each ctlName In controlNames
If LCase(CStr(ctlName)) = LCase(objectName) Then
GetEventProcedureInfo = "コントロール `" & MDEscape(objectName) & "` の " & MDEscape(eventName) & " イベント"
Exit Function
End If
Next ctlName
On Error GoTo 0
End Function
Function ProcKindToString(Kind As Long) As String
Select Case Kind
Case 0: ProcKindToString = "Sub/Function (vbext_pk_Proc)"
Case 1: ProcKindToString = "Property Let (vbext_pk_Let)"
Case 2: ProcKindToString = "Property Set (vbext_pk_Set)"
Case 3: ProcKindToString = "Property Get (vbext_pk_Get)"
Case Else: ProcKindToString = "不明な種類 (" & Kind & ")"
End Select
End Function
Function MDEscape(TextToEscape As String) As String
Dim EscapedText As String, parts() As String, i As Long, inBackticks As Boolean
EscapedText = "": If IsNull(TextToEscape) Then MDEscape = "(Null)": Exit Function
parts = Split(TextToEscape, "`"): inBackticks = False
For i = 0 To UBound(parts)
If inBackticks Then
EscapedText = EscapedText & "`" & parts(i)
If i < UBound(parts) Then EscapedText = EscapedText & "`" ElseIf TextToEscape Like "*`" Then EscapedText = EscapedText & "`"
Else
Dim tempPart As String: tempPart = parts(i)
tempPart = Replace(tempPart, "\", "\\"): tempPart = Replace(tempPart, "*", "\*")
tempPart = Replace(tempPart, "_", "\_"): tempPart = Replace(tempPart, "{", "\{")
tempPart = Replace(tempPart, "}", "\}"): tempPart = Replace(tempPart, "[", "\[")
tempPart = Replace(tempPart, "]", "\]"): tempPart = Replace(tempPart, "(", "\(")
tempPart = Replace(tempPart, ")", "\)"): tempPart = Replace(tempPart, "#", "\#")
tempPart = Replace(tempPart, "+", "\+"): tempPart = Replace(tempPart, "-", "\-")
tempPart = Replace(tempPart, ".", "\."): tempPart = Replace(tempPart, "!", "\!")
tempPart = Replace(tempPart, "|", "\|")
EscapedText = EscapedText & tempPart
End If
inBackticks = Not inBackticks
Next i
MDEscape = EscapedText
End Function
Function SectionCodeToString(sectionCode As Integer) As String
' セクションコードを文字列に変換
Select Case sectionCode
Case acDetail: SectionCodeToString = "詳細 (acDetail)"
Case acHeader: SectionCodeToString = "レポートヘッダー (acHeader)"
Case acFooter: SectionCodeToString = "レポートフッター (acFooter)"
Case acPageHeader: SectionCodeToString = "ページヘッダー (acPageHeader)"
Case acPageFooter: SectionCodeToString = "ページフッター (acPageFooter)"
Case acGroupLevel1Header: SectionCodeToString = "グループヘッダー1 (acGroupLevel1Header)"
Case acGroupLevel1Footer: SectionCodeToString = "グループフッター1 (acGroupLevel1Footer)"
Case acGroupLevel2Header: SectionCodeToString = "グループヘッダー2 (acGroupLevel2Header)"
Case acGroupLevel2Footer: SectionCodeToString = "グループフッター2 (acGroupLevel2Footer)"
' 他のグループレベルも必要に応じて追加
Case Else: SectionCodeToString = "不明なセクション (" & sectionCode & ")"
End Select
End Function
Function PaperSizeToString(paperSizeCode As Integer) As String
' 用紙サイズコードを文字列に変換 (主要なもののみ)
Select Case paperSizeCode
Case acPRPSLetter: PaperSizeToString = "レター (Letter)"
Case acPRPSLegal: PaperSizeToString = "リーガル (Legal)"
Case acPRPSExecutive: PaperSizeToString = "エグゼクティブ (Executive)"
Case acPRPSA3: PaperSizeToString = "A3"
Case acPRPSA4: PaperSizeToString = "A4"
Case acPRPSA5: PaperSizeToString = "A5"
Case acPRPSB4: PaperSizeToString = "B4 (JIS)"
Case acPRPSB5: PaperSizeToString = "B5 (JIS)"
' 他の用紙サイズも必要に応じて追加 (詳細はAccessのヘルプ参照)
Case Else: PaperSizeToString = "その他/ユーザー定義 (" & paperSizeCode & ")"
End Select
End Function
Function IsUserDefinedProcedureName(procNameToCheck As String) As Boolean
IsUserDefinedProcedureName = True
If procNameToCheck = "" Then IsUserDefinedProcedureName = False: Exit Function
If Left(LCase(procNameToCheck), 4) = "docmd." Or _
Left(LCase(procNameToCheck), 7) = "screen." Or _
Left(LCase(procNameToCheck), 3) = "me." Or _
Left(LCase(procNameToCheck), 6) = "forms!" Or _
Left(LCase(procNameToCheck), 8) = "reports!" Or _
LCase(procNameToCheck) = "currentdb" Or _
LCase(procNameToCheck) = "currentproject" Then IsUserDefinedProcedureName = False: Exit Function
If IsVBAKeywordOrBuiltIn(procNameToCheck) Then IsUserDefinedProcedureName = False: Exit Function
End Function
Function IsVBAKeywordOrBuiltIn(word As String) As Boolean
Dim keywords As Variant, i As Long, lowerWord As String
keywords = Array("Sub", "Function", "Property", "Get", "Let", "Set", "End", "If", "Then", "Else", "ElseIf", _
"Select", "Case", "For", "To", "Next", "Do", "While", "Loop", "Until", "Wend", "Exit", _
"Call", "Dim", "ReDim", "Const", "Public", "Private", "Friend", "Static", "With", "As", _
"New", "Me", "Nothing", "Empty", "Null", "True", "False", "Not", "And", "Or", "Xor", "Eqv", "Imp", "Is", _
"Like", "Option", "Explicit", "Compare", "Database", "Binary", "Text", "GoTo", "On", "Error", "Resume", _
"Type", "Enum", "Event", "RaiseEvent", "Implements", "Declare", "Lib", "Alias", "AddressOf", "ByVal", "ByRef", "Optional", "ParamArray", _
"Array", "MsgBox", "InputBox", "Chr", "Asc", "Str", "Val", "CStr", "CBool", "CByte", "CCur", "CDate", "CDbl", "CDec", "CInt", "CLng", "CSng", "CVar", _
"Format", "Date", "Time", "Now", "Timer", "DateAdd", "DateDiff", "DatePart", "DateSerial", "DateValue", "Day", "Month", "Year", "Hour", "Minute", "Second", "Weekday", _
"Left", "Right", "Mid", "Len", "InStr", "InStrRev", "Replace", "Split", "Join", "LCase", "UCase", "Trim", "LTrim", "RTrim", "Space", "String", _
"Abs", "Sgn", "Sqr", "Int", "Fix", "Round", "Rnd", _
"RGB", "QBColor", _
"TypeName", "VarType", _
"IsEmpty", "IsNull", "IsDate", "IsNumeric", "IsObject", "IsArray", "IsError", _
"CreateObject", "GetObject", _
"Err", "Erl", _
"Nz", "Choose", "Switch", "IIf", _
"Environ", "Command", "Shell", _
"Beep", "SendKeys", "AppActivate", _
"DoEvents", "EOF", "LOF", "FileLen", "GetAttr", "SetAttr", "Dir", "FreeFile", "Input", "Print", "Write", "Open", "Close", "Seek", "Lock", "Unlock", "Name", "Kill", "FileCopy", "MkDir", "RmDir", "ChDir", "ChDrive")
IsVBAKeywordOrBuiltIn = False: lowerWord = LCase(Trim(word))
For i = LBound(keywords) To UBound(keywords)
If lowerWord = LCase(keywords(i)) Then IsVBAKeywordOrBuiltIn = True: Exit Function
Next i
If IsNumeric(word) Then IsVBAKeywordOrBuiltIn = True: Exit Function
If (Left(word, 1) = """" And Right(word, 1) = """") Or (Left(word, 1) = "'" And Right(word, 1) = "'") Then IsVBAKeywordOrBuiltIn = True: Exit Function
End Function
Sub AnalyzeKeywordsAndSyntaxMD(ByVal FileNum As Integer, ByVal codeMod As Object)
Dim i As Long, lineText As String, evidences As Collection, k As Long, kw As Variant
Dim counts() As Long, keywordSets(0 To 9) As Variant, sectionTitles(0 To 9) As String
Print #FileNum, "## 3. 特定のキーワード・構文の使用状況"
Print #FileNum, ""
If codeMod.CountOfLines <= 1 Then Print #FileNum, "*分析可能なコードがほとんどありません。*" & vbCrLf: Exit Sub
keywordSets(0) = Array("DoCmd.", "Forms!", "Reports!", "Me.", "Screen.ActiveForm", "Screen.ActiveControl", "CurrentDb(", "CurrentProject."): sectionTitles(0) = "Accessオブジェクト/メソッド"
keywordSets(1) = Array("DAO.Database", "DAO.Recordset", "db.OpenRecordset", "ADODB.Connection", "ADODB.Command", "ADODB.Recordset", "cn.Execute", "rs.Open"): sectionTitles(1) = "DAO/ADOオブジェクト"
keywordSets(2) = Array("On Error GoTo", "On Error Resume Next", "Resume Next", "Resume", "Err.Raise", "Err.Clear"): sectionTitles(2) = "エラー処理構文"
keywordSets(3) = Array("Declare Sub", "Declare Function"): sectionTitles(3) = "API関数 (宣言)"
keywordSets(4) = Array("Open ", "Close #", "Print #", "Input #", "Line Input #", "FreeFile", "Kill ", "FileCopy ", "MkDir ", "Scripting.FileSystemObject"): sectionTitles(4) = "ファイル操作"
keywordSets(5) = Array("For ", "Next", "Do ", "Loop", "While ", "Wend", "For Each"): sectionTitles(5) = "ループ構文 (出現回数)"
keywordSets(6) = Array("If ", "Then", "ElseIf ", "Else", "End If", "Select Case", "Case ", "End Select"): sectionTitles(6) = "条件分岐構文 (出現回数)"
keywordSets(7) = Array("CreateObject(", "GetObject("): sectionTitles(7) = "CreateObject/GetObject (外部オブジェクト連携)"
keywordSets(8) = Array("GoTo "): sectionTitles(8) = "GoToステートメント"
keywordSets(9) = Array("SELECT """, "INSERT INTO """, "UPDATE """, "DELETE FROM """): sectionTitles(9) = "SQL文字列埋め込みの可能性"
ReDim counts(LBound(keywordSets) To UBound(keywordSets))
For k = LBound(keywordSets) To UBound(keywordSets)
Print #FileNum, "### " & MDEscape(sectionTitles(k)): Print #FileNum, ""
Set evidences = New Collection: counts(k) = 0
For i = 1 To codeMod.CountOfLines
lineText = codeMod.Lines(i, 1)
If Trim(lineText) = "" Or Left(Trim(lineText), 1) = "'" Then GoTo NextLine_ReportSpec
For Each kw In keywordSets(k)
If InStr(1, lineText, CStr(kw), vbTextCompare) > 0 Then
If k = 5 Or k = 6 Then counts(k) = counts(k) + 1 Else On Error Resume Next: evidences.Add "**行 " & i & ":** `" & MDEscape(Trim(lineText)) & "`": On Error GoTo 0: counts(k) = counts(k) + 1: Exit For
End If
Next kw
NextLine_ReportSpec:
Next i
If k = 5 Or k = 6 Then Print #FileNum, "- **総出現回数 (簡易カウント):** " & counts(k) Else If evidences.count > 0 Then Print #FileNum, "- **検出された箇所 (" & evidences.count & "件):**": For Each kw In evidences: Print #FileNum, " - " & kw: Next kw Else Print #FileNum, "*該当するキーワード・構文は見つかりませんでした。*"
Print #FileNum, ""
Next k
Dim declaredAPIs As New Collection, apiName As String, apiCallFound As Boolean
For i = 1 To codeMod.CountOfLines
lineText = codeMod.Lines(i, 1)
If InStr(1, lineText, "Declare", vbTextCompare) > 0 Then apiName = ExtractApiName(lineText): If apiName <> "" Then On Error Resume Next: declaredAPIs.Add apiName, apiName: On Error GoTo 0
Next i
Print #FileNum, "### API関数 (呼び出し)": Print #FileNum, ""
If declaredAPIs.count > 0 Then
Set evidences = New Collection: apiCallFound = False
For Each kw In declaredAPIs
For i = 1 To codeMod.CountOfLines
lineText = codeMod.Lines(i, 1)
If Trim(lineText) = "" Or Left(Trim(lineText), 1) = "'" Then GoTo NextApiLine_ReportSpec
If InStr(1, lineText, CStr(kw), vbTextCompare) > 0 And InStr(1, lineText, "Declare", vbTextCompare) = 0 Then On Error Resume Next: evidences.Add "**行 " & i & " (`" & MDEscape(CStr(kw)) & "`):** `" & MDEscape(Trim(lineText)) & "`": apiCallFound = True: On Error GoTo 0
NextApiLine_ReportSpec:
Next i
Next kw
If evidences.count > 0 Then Print #FileNum, "- **呼び出しが検出された箇所 (" & evidences.count & "件):**": For Each kw In evidences: Print #FileNum, " - " & kw: Next kw Else Print #FileNum, "*宣言されたAPI関数の呼び出しは見つかりませんでした。*"
Else Print #FileNum, "*呼び出しをチェックする宣言済みAPI関数が見つかりませんでした。*"
Print #FileNum, ""
End Sub
Function ExtractApiName(declarationLine As String) As String
Dim namePart As String
ExtractApiName = ""
If InStr(1, declarationLine, "Function ", vbTextCompare) > 0 Then namePart = Mid(declarationLine, InStr(1, declarationLine, "Function ", vbTextCompare) + Len("Function ")) Else If InStr(1, declarationLine, "Sub ", vbTextCompare) > 0 Then namePart = Mid(declarationLine, InStr(1, declarationLine, "Sub ", vbTextCompare) + Len("Sub ")) Else Exit Function
If InStr(1, namePart, "(") > 0 Then ExtractApiName = Trim(Left(namePart, InStr(1, namePart, "(") - 1)) Else ExtractApiName = Trim(namePart)
End Function
このVBAスクリプトは、Microsoft Accessデータベース内に存在するすべてのレポートオブジェクトを対象とし、それぞれの技術的な仕様(データソース、レイアウト、表示項目、VBAコードやマクロの利用状況、ページ設定など)を自動的に調査・分析します。分析結果は、レポートごとに個別のMarkdownファイルとして、データベースファイルと同じフォルダに出力されます。
このツールの主な目的は、既存のAccessレポートの機能を新しいシステム(例: Webアプリケーション)へ移行する際や、大規模な改修を行う際に必要となる初期の仕様情報を効率的に収集し、ドキュメント化することです。レポートの「目的」や「利用者」といった意味的な情報は自動分析が困難なため、これらの項目については手動での確認を促すメッセージがレポートに含まれます。
スクリプトは、VBE (Visual Basic Editor) オブジェクトモデルやDAO (Data Access Objects) を活用して、レポートのプロパティや関連するVBAコードの情報を取得し、Markdown形式で整形して出力します。これにより、開発者は各レポートの技術的な詳細を迅速に把握し、移行計画や改修作業の基礎資料として利用できます。
Sub AnalyzeAllReportSpecificationsToMarkdown()
[レポート名]_ReportSpec_Analysis.md
という名前で個別のMarkdownファイルを作成し、分析結果を書き込みます。AnalyzeReportDataSourceMD
: データソースとデータ取得ロジックを分析。AnalyzeReportLayoutMD
: レイアウトと表示項目を分析。AnalyzeReportCodeAndMacroMD
: VBAコードとマクロの利用状況を分析。AnalyzeReportOutputFormatMD
: 出力形式とページ設定関連情報を分析。Sub AnalyzeReportDataSourceMD(ByVal FileNum As Integer, ByVal rpt As Report, ByVal reportName As String, ByVal vbProj As Object)
rpt
) のデータソースとデータ取得ロジックに関連する情報を分析し、指定されたファイル番号 (FileNum
) のMarkdownレポートに出力します。RecordSource
プロパティの値(テーブル名、クエリ名、またはSQL文そのもの)を出力します。レコードソースがクエリ名の場合は、DAOを使用してそのクエリのSQL定義も取得し、併記します。Filter
プロパティと FilterOn
プロパティの値を出力します。また、レコードソース文字列に [
と ]
が含まれている場合、パラメータクエリである可能性を示唆します。.RecordSource
や .Filter
といったプロパティへのアクセスが見られる場合、VBAによってデータソースが動的に操作されている可能性を指摘します。同様に、レポートの OnOpen
や OnNoData
イベントに何らかの設定があれば、マクロやVBAによる処理の存在を示唆します。Sub AnalyzeReportLayoutMD(ByVal FileNum As Integer, ByVal rpt As Report)
rpt
) のレイアウト構成と表示されている項目に関する情報を分析し、Markdownレポートに出力します。TypeName
とControlType
)、および主要なプロパティ(コントロールソース、書式、キャプション、ピクチャパス、ソースオブジェクト、タグなど)、配置されているセクション名を出力します。OrderBy
プロパティ(並べ替え条件)と OrderByOn
プロパティ(並べ替えの有効/無効)の値を出力します。Sub AnalyzeReportCodeAndMacroMD(ByVal FileNum As Integer, ByVal rpt As Report, ByVal reportName As String, ByVal vbProj As Object)
rpt
) に関連付けられているVBAコードとマクロの設定状況を分析し、Markdownレポートに出力します。HasModule
プロパティ) が存在するかどうか、存在する場合はVBAコードの総行数 (CodeModule.CountOfLines
) を出力します。さらに、GetAllProceduresFromCodeModule
関数を利用して、モジュール内に定義されているプロシージャの中から、レポートの主要なイベント(例: Report_Open
, Detail_Format
, GroupHeader0_Print
など)に関連する可能性のあるプロシージャ名を候補として簡易的にリストアップします。OnOpen
, OnClose
, OnActivate
, OnDeactivate
, OnError
, OnNoData
, OnPage
)にマクロ名が直接設定されているかどうかを調べ、設定されていればそのマクロ名を出力します。([イベント プロシージャ]
となっている場合はVBAが使われていることを示します)。Format
イベントや Print
イベントに記述されたVBAコードは、複雑な動的表示制御を行っている可能性があり、詳細な分析対象となります。Sub AnalyzeReportOutputFormatMD(ByVal FileNum As Integer, ByVal rpt As Report)
rpt
) の出力形式やページ設定に関連する一部の情報を分析し、Markdownレポートに出力します。Printer
オブジェクトを通じて、用紙サイズ、印刷の向き、上下左右の余白といったページ設定情報を取得し、インチ単位とtwips単位で出力します。この情報は、プリンタドライバが正しく設定され、Printer
オブジェクトが利用可能な場合に取得できます。ReplaceValidFileNameChars(originalName As String) As String
:
\
, /
, :
, *
, ?
, "
, <
, >
, |
)をアンダースコア _
に一括で置換します。GetAllProceduresFromCodeModule(ByVal codeMod As Object) As Collection
:
CodeModule
) から、定義されている全てのプロシージャの名前とその種類(Sub, Function, Property Get/Let/Set)を抽出し、重複なくCollection
オブジェクトとして返します。コレクションの各アイテムは、プロシージャ名と種類コードを格納した2要素の配列です。AnalyzeReportCodeAndMacroMD
関数内で、レポートのVBAモジュールに含まれるイベントプロシージャの候補を効率的にリストアップするために使用されます。コードモジュール全体を1行ずつスキャンしてプロシージャの境界を探すよりも効率的な方法です。GetFormControlNames(objectName As String, Optional isReport As Boolean = True) As Collection
:
isReport
引数で指定)を、必要であれば非表示のデザインビューで一時的に開き、そのオブジェクト上に配置されている全てのコントロールの名前を取得して、Collection
オブジェクトとして返します。GetEventProcedureInfo
関数が、イベントプロシージャがどのコントロールに関連付けられているかを特定するために必要な、オブジェクト上のコントロール名の一覧を提供します。GetEventProcedureInfo(procName As String, controlNames As Collection, Optional objectTypeName As String = "Report") As String
:
Detail_Format
)と、対象オブジェクトの種類名(デフォルトは “Report”)、およびそのオブジェクト上のコントロール名リストを基に、そのプロシージャがどのオブジェクト(レポート自身、特定のセクション、または特定のコントロール)のどのイベントに対応するものかを推測し、説明的な文字列(例: “セクション Detail
の Format
イベント”、”コントロール Text1
の Click
イベント”)を返します。IsUserDefinedProcedureName(procNameToCheck As String) As Boolean
:
True
を返す簡易的な判定関数です。これは、Call
ステートメントの対象がユーザー定義のプロシージャである可能性が高いかどうかを大まかに推測するために使用されます。この関数は、プロジェクト内で実際にその名前のプロシージャが定義されているかまでは検証しません。IsVBAKeywordOrBuiltIn(word As String) As Boolean
:
IsUserDefinedProcedureName
関数の内部で使用され、プロシージャ名候補が予約語や組み込み関数である可能性を除外することで、ユーザー定義プロシージャ名の誤判定を減らすことを目的としています。このキーワードリストは完全ではないため、必要に応じて拡張が推奨されます。ExtractApiName(declarationLine As String) As String
:
Declare Sub
または Declare Function
といったAPI宣言の行文字列から、宣言されているAPI関数の名前部分を抽出します。ProcKindToString(Kind As Long) As String
:
vbext_ProcKind
列挙型に対応)を、人間が読んで理解しやすい説明文字列(例: “Sub/Function (vbext_pk_Proc)“, “Property Get (vbext_pk_Get)“)に変換します。MDEscape(TextToEscape As String) As String
:
*
, _
, #
, [
, ]
など)を、バックスラッシュ \
を使ってエスケープします。ただし、インラインコードを示すバッククォート `
で囲まれた部分については、エスケープ処理の対象外とします。SectionCodeToString(sectionCode As Integer) As String
:
acDetail
, acPageHeader
)を、人間が読んで理解しやすい説明文字列(例: “詳細 (acDetail)”, “ページヘッダー (acPageHeader)“)に変換します。PaperSizeToString(paperSizeCode As Integer) As String
:
acPRPSA4
)を、人間が読んで理解しやすい説明文字列(例: “A4”)に変換します(主要な用紙サイズのみ対応)。解決できる課題(ユーティリティ関数群全体として): これらのヘルパー関数は、メインの分析プロシージャのロジックを整理し、特定の共通処理を再利用可能な形でカプセル化することで、コード全体の構造を明確にし、可読性、保守性、拡張性を向上させます。特にMDEscape
関数は、Markdown形式での正確なレポート出力に不可欠です。
Alt
+ F11
キーを押すか、Accessのリボンメニューから「データベースツール」タブ → 「Visual Basic」を選択します。AnalyzeAllReportSpecificationsToMarkdown
プロシージャと、それに付随する全てのサブプロシージャおよびユーティリティ関数)を、作成した標準モジュールのコードウィンドウにコピー&ペーストします。Microsoft Office XX.0 Access database engine Object Library
(XX.0はOfficeのバージョンによって変わります。)Microsoft Visual Basic for Applications Extensibility 5.3
AnalyzeAllReportSpecificationsToMarkdown
プロシージャ内のどこかにカーソルを置きます。F5
キーを押すか、ツールバーの実行ボタン(緑色の右向き三角)をクリックしてマクロを実行します。[レポート名]_ReportSpec_Analysis.md
という名前のMarkdownファイルが複数作成されていることを確認します。VBE.ActiveVBProject
の使用: コード内でVBEプロジェクトを取得する際に Set vbProj = VBE.ActiveVBProject
と記述されています。これはVBAエディタで現在アクティブになっているプロジェクトを参照しますが、複数のデータベースファイルやアドインが同時に開かれている状況などでは、意図しないプロジェクトを参照してしまう可能性があります。より堅牢にカレントデータベースのVBAプロジェクトを参照するには Set vbProj = Application.CodeProject
を使用することを推奨します。Open
イベントなどがトリガーされる可能性があります。通常は大きな問題を引き起こしませんが、これらのイベントに排他制御や重要な初期化処理などが記述されている場合は、予期せぬ動作の可能性も考慮に入れる必要があります。処理後は保存せずに閉じています。Debug.Print
出力)のエラー情報を確認してください。Printer
オブジェクトに依存します。実行環境に適切なプリンタドライバがインストールされていない、またはデフォルトプリンタが正しく設定されていない場合、これらの情報が正確に取得できないか、エラーが発生する可能性があります。MDEscape
関数によって主要なMarkdown特殊文字はエスケープ処理されますが、最終的なレポートの表示は使用するMarkdownビューアやパーサの実装に依存します。特定のビューアでは意図した通りに表示されない可能性もゼロではありません。