【テクニカル・上級編】【上級者向け】Visio VBAでのエラーハンドリング:ページ操作失敗時に「どのページで何が起きたか」を詳細に記録する – Visio VBA解析バイブル

スポンサーリンク
スポンサーリンク

堅牢性の設計思想:Visio VBAにおけるコンテキスト・アウェアなエラーハンドリング

大規模な図面自動生成、あるいは数千ページに及ぶ既存ドキュメントの再構築。エンジニアがVisioを単なる「作図ツール」ではなく「データビジュアライゼーション・エンジン」として扱う時、避けて通れない壁がある。それは、「どこで、何が、なぜ失敗したのか」を、実行時に完全に特定しなければならないという要請だ。

標準的な `On Error GoTo ErrorHandler` は、初学者が書くスクリプトには十分かもしれない。しかし、複雑なページ操作、レイヤーの動的制御、ページ設定の変更が絡み合うエンタープライズ級の自動化において、単に「エラー番号 1004」を吐き出して止まるコードは、ゴミに等しい。

本稿では、エラー発生時に「どのドキュメントの、どのページの、どのレイヤーの、どの操作で」失敗したかをスタックトレース風に記録し、致命的な停止を避けつつ、処理を継続するためのコンテキスト・アウェア(文脈依存型)なエラーハンドリング・アーキテクチャを詳解する。

1. 敗北の理由:なぜ標準のエラーハンドリングは無力なのか

Visio VBAにおけるページ操作は、メモリ上のオブジェクトツリーに対して極めて動的な操作を行う。
`Application.ActiveDocument.Pages(i).Shapes(j)…`
このような階層的なアクセスにおいて、エラーが発生した瞬間に我々が失うのは「エラーメッセージ」だけではない。「そのエラーがどの階層の、どのオブジェクトの状態において発生したか」というコンテキスト(文脈)である。

標準的なエラーハンドリングでは、`Err.Number` と `Err.Description` しか得られない。しかし、大規模ループの中で1ページだけ設定変更に失敗した場合、その原因が「ページ自体が破損しているのか」「特定のレイヤーがロックされているのか」「メモリ不足によるオブジェクトの生成失敗か」を判別するには、その時点のオブジェクトの属性情報をログに焼き付ける必要がある。

2. アーキテクチャ:コンテキスト・トラッカーの実装

スタックトレースを模倣するためには、現在実行中の「操作の階層」を管理する専用のクラスが必要だ。ここでは、操作の履歴をスタック(Stack)として保持する `CContextTracker` クラスを設計する。

CContextTracker クラス(Class Module)

' Class Module: CContextTracker
Option Explicit

Private pStack As Collection

Private Sub Class_Initialize()
    Set pStack = New Collection
End Sub

' 現在のコンテキストをスタックに積む
Public Sub Push(ByVal context As String)
    pStack.Add context, CStr(pStack.Count + 1)
End Sub

' コンテキストをポップする(正常終了時)
Public Sub Pop()
    If pStack.Count > 0 Then
        pStack.Remove pStack.Count
    End If
End Sub

' 現在のスタックを文字列として返す(エラーログ用)
Public Function GetStackTrace() As String
    Dim i As Long
    Dim trace As String
    For i = 1 To pStack.Count
        trace = trace & " -> [" & pStack(i) & "]"
    Next i
    GetStackTrace = trace
End Function

' メモリ解放の徹底
Private Sub Class_Terminate()
    Set pStack = Nothing
End Sub

3. 実践:堅牢なページ操作ループの実装

次に、このトラッカーを用いて、ページ操作(追加・削除・サイズ変更)をループで行うメインロジックを記述する。ここで重要なのは、「エラー発生時にオブジェクトを明示的に解放し、次のイテレーションへ安全に移行する」ことだ。

Main Module

Option Explicit

' Windows API: システムのアップタイムを取得してログの精度を上げる(高度なデバッグ用)

If VBA7 Then

Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long

Else

Declare Function GetTickCount Lib "kernel32" () As Long

End If

Sub ExecuteRobustPageOperations() Dim doc As Visio.Document Dim pg As Visio.Page Dim tracker As CContextTracker Dim i As Long Dim targetPageCount As Long ' 初期化 On Error GoTo FatalError Set doc = Visio.Application.ActiveDocument Set tracker = New CContextTracker tracker.Push "Document: " & doc.Name targetPageCount = 10 ' 例として10ページ分操作 ' メインループ For i = 1 To targetPageCount ' --- エラーハンドリングの境界線 --- On Error GoTo PageErrorHandler tracker.Push "Iteration: " & i ' 1. ページの追加(または取得) tracker.Push "Action: Add/Get Page" If i <= doc.Pages.Count Then Set pg = doc.Pages(i) Else Set pg = doc.Pages.Add End If tracker.Pop ' Action: Add/Get Page 終了 tracker.Push "Action: Page Settings" ' 2. ページ設定の変更(ここで失敗するケースが多い) ' 例: ページサイズを極端な値に設定しようとしてエラーを誘発させるシミュレーション If i = 5 Then ' 意図的なエラー:存在しないプロパティへのアクセス pg.PageSetup.PageWidth = -1 End If pg.PageSetup.PageWidth = 20 ' cm pg.PageSetup.PageHeight = 30 ' cm tracker.Pop ' Action: Page Settings 終了 tracker.Push "Action: Layer Management" ' 3. レイヤー操作(複雑な階層操作) ' ここで失敗しても、次のページへ進める必要がある ' (実装省略) tracker.Pop ' Action: Layer Management 終了 ' 正常終了時のスタッククリア tracker.Pop ' Iteration: i 終了 ' メモリ最適化: オブジェクトの明示的解放 Set pg = Nothing Next i MsgBox "全プロセスが完了しました。", vbInformation Exit Sub ' --- エラーハンドリング・セクション --- PageErrorHandler: ' エラー発生時の詳細ログ出力 Call LogError(tracker, Err) ' 重要:メモリリークを防ぐため、失敗したオブジェクトを確実に解放 Set pg = Nothing ' スタックの整合性を保つためのPop ' (エラーがどこで起きたかによってPopの回数が変わるため、 ' 設計思想として「エラー時はスタックをリセットするか、Pushした分だけ確実にPopする」必要がある) ' ここでは簡略化のため、次のループのためにスタックを整理する Do While tracker.GetStackTrace <> "Document: " & doc.Name tracker.Pop Loop ' 次のページへスキップ Resume Next FatalError: ' 致命的なエラー(ドキュメントが開けない等) MsgBox "致命的なシステムエラーが発生しました。処理を中断します。" & vbCrLf & _ "Trace: " & tracker.GetStackTrace, vbCritical Set tracker = Nothing End Sub ' 高精度ログ出力ルーチン Private Sub LogError(tracker As CContextTracker, errObj As Err) Dim logMsg As String Dim logFile As String logFile = ThisDocument.Path & "\error_log.txt" logMsg = "--------------------------------------------------" & vbCrLf & _ "Timestamp: " & Now & " (Tick: " & GetTickCount & ")" & vbCrLf & _ "Error Number: " & errObj.Number & vbCrLf & _ "Description: " & errObj.Description & vbCrLf & _ "Context Trace: " & tracker.GetStackTrace & vbCrLf & _ "--------------------------------------------------" & vbCrLf ' デバッグ出力 Debug.Print logMsg ' ファイルへの永続化(Appendモード) Dim fso As Object Set fso = CreateObject("Scripting.FileSystemObject") Dim ts As Object Set ts = fso.OpenTextFile(logFile, 8, True) ' 8 = ForAppending ts.WriteLine logMsg ts.Close Set ts = Nothing Set fso = Nothing End Sub

4. 伝説のエンジニアが教える「極限の知見」

このコードを単なる「エラー回避策」として見てはいけない。これは「システムの観測可能性(Observability)」を高めるための設計である。

オブジェクトのライフサイクルとメモリ管理

Visio VBAにおいて、`Set pg = Nothing` をエラーハンドラ内で忘れることは、メモリリークを許容することと同義だ。特に、ループ内でページを追加・削除する際、参照が残っていると、Visioプロセス自体が不安定になり、最終的に `Out of Memory` でクラッシュする。エラーハンドラは、単なるエラーの記録場所ではなく、「システムの整合性を回復させるためのクリーンアップ・ポイント」でなければならない。

Windows API の活用

`GetTickCount` をログに含めた理由は、マルチスレッド環境や、外部システムから非同期でVisioを操作している場合、ログの発生順序をミリ秒単位の精度で検証するためだ。レガシーな環境では、システムクロックの精度が低いことがあり、APIによる直接的なカウントは、デバッグにおける「真実」を導き出す。

堅牢なループ設計(Skip vs Abort)

「エラーが出たら止める」のは、スクリプトだ。「エラーが出ても、可能な限り完遂し、失敗した箇所だけを報告する」のが、システムだ。本実装のように `Resume Next` を戦略的に使い、かつ `tracker` を使って「何が失敗したか」を完全に捕捉することで、運用担当者は「どのページを修正すればよいか」を即座に判断できる。

結論

Visio VBAの自動化において、真のプロフェッショナルとアマチュアを分けるのは、コードの短さではなく、「失敗した時の挙動の美しさ」だ。

コンテキストをスタックし、オブジェクトを管理し、エラーをデータとして扱う。この規律こそが、レガシーな環境であっても、大規模なシステムであっても、揺るぎない信頼性をもたらす唯一の道である。

タイトルとURLをコピーしました