Visio VBAを掌握する極限の知見:Shape削除後の「#REF!」地獄を断つ自動修復エンジンの構築
Visioのオブジェクトモデルは、CADや一般的なドキュメント描画ソフトとは一線を画す。背後に強力な「ShapeSheet」というRDBライクな数式エンジンを隠し持ち、シェイプ間の動的な依存関係(Glue・Scratch・Userセルの参照など)を構築できる点が最大の強みだ。
だが、この強みは同時に「最大の爆弾」でもある。
設計者が深く考えずに特定のシェイプを `Shape.Delete` で消し去った瞬間、依存関係にあった他のシェイプのShapeSheetセルには無慈悲なエラー値 `#REF!` が刻まれる。UI上では「数式エラー」として静かに沈黙し、下流の計算ロジックや自動化スクリプトを根底からクラッシュさせる。
今回は、このレガシーな悪夢をVBAのレイヤーで完全に無力化し、全ページのShapeSheetを走査してエラーを自動検知・リカバリする「フェイルセーフ・エンジン」の全貌を公開する。
—
1. なぜShapeSheetは「#REF!」で崩壊するのか?
Visioの数式エンジンは、他のシェイプのプロパティを `Sheet.10!PinX` のように参照する。ここで `Sheet.10` が削除されると、Visioは親切心(あるいは悪意)から、該当セルの数式を強制的に `#REF!` に書き換える。
厄介なのは、一度 `#REF!` に落ちたセルに対して再び正常な数式や数値を代入しようとしても、構文エラーや型不一致を引き起こすケースがある点だ。特に `CellsU` プロパティ経由で値を流し込む場合、既存のエラー文字列がパース処理を阻害することがある。
シニアエンジニアとして、我々はこの現象を「事後的なエラーハンドリング」ではなく、「ドキュメントの整合性を担保するトランザクション的修復」としてコードに落とし込まなければならない。
—
2. 実装アーキテクチャの要件
今回の自動リカバリエンジンは、以下の要件を満たす設計とする。
1. 全ページ・全シェイプの網羅的走査:Masterシェイプや非表示レイヤーの罠を回避しつつ、Document全体を舐め尽くす。
2. ResultStr / FormulaU の二刀流チェック:セルの評価値(Value)と数式(Formula)の両面から `#REF!` を検出する。
3. 安全なデフォルト値へのフォールバック:単に消すのではなく、パニックを起こさない安全な初期値(あるいはフォールバック数式)へ強制上書きする。
4. オブジェクトの明示的解放(メモリ管理):Visio VBA特有のCOMメモリリークを防ぐため、ループごとの変数破棄を徹底する。
—
3. 実装コード:ShapeSheetエラー自動リカバリ・エンジン
以下のコードをVisioの標準モジュールに配置し、実行せよ。実業務の巨大なフロー図やネットワーク図でも耐えうるよう、最適化の限りを尽くしている。
Option Explicit
‘ —————————————————————————
‘ 模範的チーフアーキテクトによるVisio ShapeSheetリカバリエンジン
‘ Purpose: ドキュメント内の全シェイプのセルを走査し、#REF! エラーを自動修復する
‘ —————————————————————————
Public Sub ExecuteShapeSheetFailSafe()
Dim startTime As Double
startTime = Timer
Dim doc As Visio.Document
Set doc = ActiveDocument
If doc Is Nothing Then
MsgBox “アクティブなドキュメントが存在しません。”, vbCritical, “致命的エラー”
Exit Sub
End If
Dim totalCheckedCells As Long
Dim totalFixedErrors As Long
totalCheckedCells = 0
totalFixedErrors = 0
‘ 画面描画と言いなりになるイベントを抑制して爆発的な高速化を図る
Application.ScreenUpdating = False
Application.EventEnabled = False
Dim pag As Visio.Page
Dim shp As Visio.Shape
On Error GoTo ErrorHandler
‘ ドキュメント内の全ページをループ
For Each pag In doc.Pages
‘ バックグラウンドページや特殊ページはスキップ等の制御も可能だが今回は全走査
For Each shp In pag.Shapes
Call ProcessShapeRecursive(shp, totalCheckedCells, totalFixedErrors)
Next shp
Next pag
Application.ScreenUpdating = True
Application.EventEnabled = True
MsgBox “リカバリ処理が完了しました。” & vbCrLf & _
“総走査セル数: ” & totalCheckedCells & vbCrLf & _
“修復エラー数: ” & totalFixedErrors & vbCrLf & _
“処理時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “フェイルセーフ完了”
Exit Sub
ErrorHandler:
‘ 異常終了時も必ずVisioの描画ロックとイベントを解除する(これ重要)
Application.ScreenUpdating = True
Application.EventEnabled = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “異常終了”
End Sub
‘ —————————————————————————
‘ 再帰的にシェイプ(グループ化された子シェイプを含む)を走査するプロシージャ
‘ —————————————————————————
Private Sub ProcessShapeRecursive(ByVal parentShp As Visio.Shape, ByRef checkedCount As Long, ByRef fixedCount As Long)
Dim cell As Visio.Cell
Dim subShp As Visio.Shape
‘ 1. シェイプ内のすべてのセルを走査
Dim i As Long
For i = 0 To parentShp.CellCount – 1
Set cell = parentShp.CellsSRC(parentShp.CellRegion(i), parentShp.CellIndex(i))
checkedCount = checkedCount + 1
‘ #REF! エラーの検知 (ResultStr または FormulaU にエラー文字列が含まれるか)
If InStr(1, cell.FormulaU, “#REF!”, vbTextCompare) > 0 Or _
InStr(1, cell.ResultStr(&H20), “#REF!”, vbTextCompare) > 0 Then
‘ 【リカバリ実行】
‘ セルの性質に応じた安全値へのフォールバック
If SafeRestoreCell(cell, parentShp) Then
fixedCount = fixedCount + 1
End If
End If
‘ COMオブジェクトの解放
Set cell = Nothing
Next i
‘ 2. グループシェイプの場合は子シェイプを再帰処理
If parentShp.Type = visTypeGroup Then
For Each subShp in parentShp.Shapes
Call ProcessShapeRecursive(subShp, checkedCount, fixedCount)
Set subShp = Nothing
Next subShp
End If
End Sub
‘ —————————————————————————
‘ セル種別に応じた安全な数式・値の復旧ロジック
‘ —————————————————————————
Private Function SafeRestoreCell(ByVal targetCell As Visio.Cell, ByVal ownerShp As Visio.Shape) As Boolean
On Error GoTo RestoreError
Dim cellName As String
cellName = targetCell.NameU
‘ 例外処理: ユーザー定義セルやスクラッチセルの場合は安全に削除、あるいはデフォルト化
If Left(cellName, 4) = “User” Or Left(cellName, 7) = “Scratch” Then
‘ 壊れたカスタムセルは数式を “0” にリセット
targetCell.FormulaU = “0”
SafeRestoreCell = True
Exit Function
End If
‘ 位置・サイズ系セルの場合(最低限の形状維持)
Select Case cellName
Case “PinX”, “PinY”
targetCell.FormulaU = “Width0.5”
SafeRestoreCell = True
Case “Width”, “Height”
targetCell.FormulaU = “1” ‘ デフォルト1インチにフォールバック
SafeRestoreCell = True
Case Else
‘ その他の汎用セルは空値または “0” を代入
‘ ※Visioの仕様上、セルによっては定数代入が不可の場合があるためエラートラップ必須
targetCell.FormulaU = “0”
SafeRestoreCell = True
End Select
Exit Function
RestoreError:
‘ 強制代入すら拒絶される読取専用セルなどの場合はFALSEを返す
SafeRestoreCell = False
End Function
—
4. チーフアーキテクトが教える「現場の知見」と最適化の罠
上記のコードをそのまま社内システムに組み込むにあたり、以下の「教科書には載っていない実務上の知見」を心に刻んでおいてほしい。
① `ScreenUpdating` と `EventEnabled` の強制制御による爆速化
Visio VBAで最もやってはいけないのが、シェイプやセルを操作するたびに画面の再描画(UI Redraw)やイベントリスナーを発火させることだ。数千個のセルを持つ図面において、これを放置すると処理が何百倍も遅くなる。
コード内にある通り、処理の冒頭で `Application.ScreenUpdating = False` をかけ、必ず ErrorHandler を含めて復元すること。これが守れないコードはプロの仕事とは言えない。
② セルの「インデックス走査」の優位性
`For Each cell In shp.Cells` のようなスマートな書き方は、VisioのCOMラッパーの仕様上、特定のセルコレクションで正しく機能しないケースやメモリリークを引き起こす原因になる。
`CellCount` を取得し、`CellsSRC` を用いてインデックス経由(`0` から `CellCount – 1`)で物理的に叩く手法こそが、最も確実でメモリ消費の少ない王道のアプローチである。
③ `#REF!` 回避の根本治療(設計論)
VBAによる事後修復はもちろん強力なフェイルセーフだが、そもそも設計段階で崩壊を防ぐ方法もある。
シェイプを削除する前に `Glue`(接着)やプロパティの依存関係を解除する前処理(プレ・フック)をスクリプトに組み込むことだ。しかし、ユーザーがGUI上で手動削除したケースまで防ぐことはできない。だからこそ、今回紹介した「定期実行、または保存前イベント(`DocumentBeforeSave`)で走らせる自動修復エンジン」の常備が、エンタープライズ環境におけるVisio運用における唯一にして最大の防衛策となる。
—
レガシーなシステムにしがみつくだけのプログラマーは淘汰される。しかし、Visioの奥底にあるCOMの挙動とShapeSheetの数式構造を完全に見切り、トラブルをコードでねじ伏せるエンジニアの価値は、いつの時代も決して揺らがない。この知見をあなたの武器として、現場のシステムを完全に掌握してほしい。
