虚無への境界線を統べよ:Visio VBAにおける「存在しないオブジェクト」との決別
Visio VBAの世界において、凡庸なプログラマと熟練のアーキテクトを分かつ境界線は、コードの「正常系」ではなく「異常系」への向き合い方に現れる。
VisioはExcelやWordとは一線を画す。それは単なるドキュメントではなく、複雑にリンクし合う「ShapeSheet」という動的なデータベースの集合体だからだ。削除されたShape IDへの参照、存在しないセルの取得、そして予期せぬタイミングでのユーザー操作。これらはマクロを容易に粉砕する。
本稿では、レガシーシステムの保守から大規模な自動化までを統括してきた私の経験に基づき、Visio VBAにおいて「虚無(エラー)」をいかにして制御し、堅牢なシステムを構築するか、その極限の知見を伝授する。
—
1. Shape IDの揮発性と「幽霊オブジェクト」の正体
VisioのShape IDは、そのドキュメントのライフサイクル内で一意だが、永続的ではない。Shapeが削除されればそのIDは欠番となり、存在しないIDへのアクセスは `91: Object variable or With block variable not set` ではなく、しばしば `-2032465768 (Invalid Sheet Index)` というVisio固有の実行時エラーを叩き出す。
存在確認の鉄則:`ItemFromID` の罠
多くの開発者が `Page.Shapes.ItemFromID(ID)` を不用意に呼び出すが、これは危険だ。存在しないIDを渡した瞬間に例外がスローされる。
‘ — 悪い例:存在を信じ切っている —
Set shp = vsoPage.Shapes.ItemFromID(nID) ‘ ここで落ちる
‘ — プロの流儀:存在を検証し、例外を封じ込める —
Public Function GetSafeShape(ByRef vsoPage As Visio.Page, ByVal nID As Long) As Visio.Shape
On Error Resume Next
Dim shp As Visio.Shape
Set shp = vsoPage.Shapes.ItemFromID(nID)
On Error GoTo 0
If Not shp Is Nothing Then
Set GetSafeShape = shp
End If
End Function
2. ShapeSheet操作における「CellExists」の絶対守護
Visio開発の核心はShapeSheetにある。しかし、`shp.Cells(“Prop.Value”).ResultStr(“”)` と書いたとき、そのShapeに “Prop.Value” という行が確実に存在すると断言できるだろうか?
ユーザーが手動でシェイプデータを削除した、あるいはマスターシェイプが更新された――そんな些細なことでシステムは沈黙する。
CellExistsU によるユニバーサル名の検証
ローカル名(日本語環境なら日本語のセル名)に依存してはならない。多言語環境や将来の互換性を考慮し、常にユニバーサル名を使用し、かつ存在確認を先行させる。
Public Sub UpdateShapeData(ByRef shp As Visio.Shape, ByVal cellName As String, ByVal value As String)
‘ CellExistsU の第2引数は 0 (False) で「セルの継承を含めて確認」を意味する
If shp.CellExistsU(cellName, Visio.VisExistsFlags.visExistsAnywhere) <> 0 Then
shp.CellsU(cellName).FormulaForceU = Chr(34) & value & Chr(34)
Else
‘ ここでログ出力や代替処理を行う。黙って無視するのは二流だ。
Debug.Print “Warning: Cell ” & cellName & ” not found in Shape ID ” & shp.ID
End If
End Sub
3. Windows APIによる高速化と「サイレント・フェイル」の構築
大規模な図面処理において、VBAの標準的なエラーダイアログはユーザーを混乱させる。また、画面更新やイベントの連鎖はパフォーマンスを著しく低下させるだけでなく、予期せぬ再入(Re-entrancy)によるスタックオーバーフローを招く。
私は、クリティカルなセクションではWindows APIを用いてメッセージボックスを抑制し、処理の原子性を確保する。
‘ Windows API: メッセージループを制御し、画面更新を完全に凍結する
Private Declare PtrSafe Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hWnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
Private Const WM_SETREDRAW As Long = &HB
Public Sub ExecuteHeavyProcess()
On Error GoTo ErrorHandler
‘ VisioのUI更新とイベントを停止
Application.ScreenUpdating = False
Application.EventsEnabled = False
Application.DeferRecalc = True
‘ — 高負荷なロジック —
‘ ここで数千のShapeを走査する
CleanExit:
‘ 確実に元の状態に戻す(これがなければVisioが死ぬ)
Application.DeferRecalc = False
Application.EventsEnabled = True
Application.ScreenUpdating = True
Exit Sub
ErrorHandler:
‘ ログ記録とリソース解放
LogError Err.Number, Err.Description, “ExecuteHeavyProcess”
Resume CleanExit
End Sub
4. COMオブジェクトのライフサイクル管理
Visio VBAはCOM(Component Object Model)の上で動いている。ループ内で `Shape` や `Cell` オブジェクトを大量に生成し、明示的に解放しない場合、メモリリークや「ゴースト・プロセス(Visioを閉じてもプロセスが残る)」の原因となる。
Public Sub IterateShapes(ByRef vsoPage As Visio.Page)
Dim i As Integer
Dim shp As Visio.Shape
For i = 1 To vsoPage.Shapes.Count
Set shp = vsoPage.Shapes.Item(i)
‘ 処理
‘ ループの終端で明示的に参照を切り離す
Set shp = Nothing
Next i
End Sub
5. 究極の堅牢性を実現する「Try-Get」パターン
最後に、私が設計するエンタープライズ・システムで必ず採用するパターンを紹介する。これは、エラーハンドリングを「フロー制御」から分離し、呼び出し側に安全なインターフェースを提供するものだ。
”’
”’
”’
Public Function TryGetShapeData(ByRef shp As Visio.Shape, ByVal rowName As String, ByRef outValue As String) As Boolean
TryGetShapeData = False
outValue = “”
If shp Is Nothing Then Exit Function
‘ ユニバーサル名の接頭辞を検証
Dim fullCellName As String
fullCellName = “Prop.” & rowName
If shp.CellExistsU(fullCellName, visExistsAnywhere) <> 0 Then
‘ ResultStr は型変換エラーを防ぐために非常に有用
outValue = shp.CellsU(fullCellName).ResultStr(Visio.VisUnitCodes.visNoCast)
TryGetShapeData = True
End If
End Function
結論:アーキテクトとしての矜持
Visio VBAにおけるエラーハンドリングとは、単なる `On Error` 文の羅列ではない。それは、「オブジェクトの生存期間を完全に把握し、不確実なデータ構造に対して常に懐疑的であること」と同義である。
存在しないものを参照しない。開けられない扉は叩かない。
この原則を徹底することで、あなたのマクロは「動けばラッキーなスクリプト」から「止まることのない基幹システム」へと昇華する。
技術の深淵を覗く者は、常に最悪の事態を想定し、それをコードでねじ伏せる準備ができている。諸君の健闘を祈る。
