【実務・中級編】Shape.Delete実行後のShapeSheet参照エラー(#REF!)の自動検知と数式リカバリマクロ – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:Shape.Deleteの呪縛を断つ!ShapeSheetの「#REF!」自動検知・リカバリエンジン

こんにちは。開発プロジェクトの現場を預かるチーフアーキテクトの私だ。
日々、複雑なダイアグラムの自動生成や、大規模なレイアウト管理ツールをVBAで構築している者なら、一度はあの「悪夢」に直面したことがあるはずだ。

そう、`Shape.Delete` を実行した瞬間に、他の図形のShapeSheet内でひっそりと爆発する「#REF!」エラーである。

Visioのオブジェクトモデルにおいて、図形間の数式バインド(ガードやScratchセル、ピンの位置参照など)は強力な反面、参照元のシェイプが消滅した瞬間に容赦なく崩壊する。そして、その腐敗した数式は、図形の移動や再計算のたびにVBAのランタイムエラーを引き起こすか、最悪の場合、ドキュメント全体の破損へと繋がる。

今回は、このVisio VBAにおける最大のトラップの一つである「#REF!エラーの連鎖」を完全に無力化し、プロダクション環境で耐えうる堅牢なフェイルセーフ・リカバリシステムをあなたに授けよう。

なぜ「#REF!」エラーの放置は致命傷なのか?

アマチュアのVBAプログラマは、「図形を消すんだから、参照エラーが出るのは仕方がない。エラーが出たらその都度手動で直せばいい」と考える。
しかし、大規模なフローチャートやネットワーク図を動的生成・更新するシステムにおいて、これはシステム死への片道切符だ。

1. 連鎖的な腐敗: 1つのシェイプを消したことで発生した`#REF!`を別のシェイプがさらに参照している場合、エラーはドキュメント全体に伝播する。
2. 遅延エラーの恐怖: エラーが発生した瞬間ではなく、ユーザーが後からその図形をダブルクリックしたり、別のマクロでプロパティを読み込もうとした瞬間にVBAがクラッシュする。
3. ShapeSheetの型汚染: 数式セルに文字列としての`#REF!`が入り込むことで、数値型を期待する処理で型ミスマッチ(エラー13)が頻発する。

これを防ぐには、図形削除の前後、あるいは定期的なメンテナンスバッチとして、「ドキュメント全域のShapeSheet数式スキャンと安全なデフォルト値への強制上書き」を自動化する以外に道はない。

堅牢な設計思想:何をどうスキャンし、どう修復すべきか

今回のリカバリエンジンを実装するにあたり、以下のアーキテクチャ上の原則を定めた。

  • イミュータブルな視点での走査: ページ内の全シェイプ、さらにはマスターシェイプやグループの内部(子シェイプ)まで再帰的に潜り込むこと。
  • 網羅的なセルチェック: すべてのセルを舐めるのはパフォーマンスの観点から重すぎるため、「数式(FormulaU)」に `”#REF!”` という文字列が含まれているかを高速に判定する。
  • スマートなデフォルト値フォールバック: 単にエラーを消すだけでなく、そのセルが持つべき本来のデータ型(数値、文字列、座標など)に応じた安全な数式へフォールバックさせる。

プロダクションコード:ShapeSheetリカバリエンジン

以下のコードは、実務の現場ですぐに組み込めるように設計された完全版のモジュールだ。エラーハンドリングを徹底し、ドキュメント全体をクリーンに保つ。

Option Explicit

‘ ==============================================================================
‘ ódulo名: clsShapeSheetRecoveryEngine
‘ 概要: アクティブドキュメント内の全ShapeSheetを走査し、#REF! エラーを検知・修復する
‘ ==============================================================================

Public Sub RunFullRecovery()
Dim startTime As Double
startTime = Timer

Dim vsoPage As Visio.Page
Dim fixedCount As Long
fixedCount = 0

‘ 画面描画とイベントを停止し、爆速化と安定性を確保
Application.ScreenUpdating = False
Application.EventsEnabled = False

On Error GoTo ErrorHandler

Debug.Print “=== ShapeSheet #REF! リカバリ処理を開始 ===”

‘ ドキュメント内の全ページを走査
For Each vsoPage In ActiveDocument.Pages
‘ バックグラウンドページは除外する場合のガード(必要に応じて変更)
If Not vsoPage.Background Then
Debug.Print “スキャン中: ページ [” & vsoPage.Name & “]”
Call ScanShapesRecursive(vsoPage.Shapes, fixedCount)
End If
Next vsoPage

Debug.Print “=== リカバリ完了: 合計 ” & fixedCount & ” 個の破損セルを修復しました (” & Format(Timer – startTime, “0.00”) & “秒) ===”

CleanUp:
Application.ScreenUpdating = True
Application.EventsEnabled = True
Exit Sub

ErrorHandler:
MsgBox “リカバリ処理中に予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

‘ ——————————————————————————
‘ 再帰的にグループの内側も含めてシェイプを走査する
‘ ——————————————————————————
Private Sub ScanShapesRecursive(ByRef vsoShapes As Visio.Shapes, ByRef fixedCount As Long)
Dim vsoShape As Visio.Shape
Dim i As Long

‘ コレクションの逆順ループ(安全性の確保)
For i = vsoShapes.Count To 1 Step -1
Set vsoShape = vsoShapes(i)

‘ シェイプ自体のセルをチェック
If InspectAndFixShape(vsoShape) Then
fixedCount = fixedCount + 1
End If

‘ グループシェイプの場合は再帰的に内部を走査
If vsoShape.Type = visTypeGroup Then
Call ScanShapesRecursive(vsoShape.Shapes, fixedCount)
End If
Next i
End Sub

‘ ——————————————————————————
‘ 個別シェイプの全セルを走査し、#REF! を検出・修復する
‘ ——————————————————————————
Private Function InspectAndFixShape(ByRef vsoShape As Visio.Shape) As Boolean
Dim vsoCell As Visio.Cell
Dim isFixed As Boolean
isFixed = False

On Error GoTo CellError

‘ シェイプ内のすべてのセクション・行・セルを走査
‘ 注: Shape.CellsU を直接回すのは一部のセルが取得できない場合があるため、
‘ 主要なプロパティやイテレーションを行う。ここではパフォーマンスを考慮し
‘ 主要なジオメトリ/位置セル、およびエラーが出やすい数式セルに絞るか、
‘ 全セルイテレーションを行う。

For Each vsoCell in vsoShape.CellsU
‘ 数式内に “#REF!” が含まれているか判定
If InStr(1, vsoCell.FormulaU, “#REF!”, vbTextCompare) > 0 Then
Debug.Print ” [検出] 破損シェイプ ID: ” & vsoShape.ID & ” (” & vsoShape.Name & “) / セル: ” & vsoCell.Name
Debug.Print ” 破損数式: ” & vsoCell.FormulaU

‘ 安全なデフォルト値へのフォールバック
‘ セルの種類(幅、高さ、ピン位置など)に応じた修復
Call ApplySafeFallback(vsoCell)

isFixed = True
End If
Next vsoCell

InspectAndFixShape = isFixed
Exit Function

CellError:
‘ 読み取り専用セルや特殊セルでエラーが出る場合はスキップ
Resume Next
End Function

‘ ——————————————————————————
‘ セルのコンテキストに応じた安全なフォールバック値を適用
‘ ——————————————————————————
Private Sub ApplySafeFallback(ByRef vsoCell As Visio.Cell)
Dim cellName As String
cellName = LCase(vsoCell.Name)

On Error GoTo ForceDefault

‘ セル名に応じたスマート修復
Select Case True
Case InStr(cellName, “width”) > 0
vsoCell.FormulaU = “2 इंच” ‘ または “50mm”等、環境に応じたデフォルト
Case InStr(cellName, “height”) > 0
vsoCell.FormulaU = “1 इंच”
Case InStr(cellName, “pinx”), InStr(cellName, “piny”)
vsoCell.FormulaU = “0”
Case Else
‘ 汎用的なフォールバック:直前の数値結果を強制代入、または 0
vsoCell.ResultIU = vsoCell.ResultIU
End Select
Exit Sub

ForceDefault:
‘ どうしてもダメな場合は強制的に “0” をブチ込む
vsoCell.FormulaU = “0”
End Sub

現場のエンジニアへ送る、実装上の重要な注意点

1. パフォーマンスの罠 (`ScreenUpdating`)
Visio VBAで最もやってはいけないのが、画面描画(`ScreenUpdating = True`)のまま数千個のセルを書き換えることだ。セルを1つ書き換えるたびにVisioは再計算と画面再描画を走りたがる。必ず前後のトグル処理を忘れないこと。
2. `CellsU` と `FormulaU` の使用
ローカライズ(日本語版、英語版など)の差異によるバグを防ぐため、セル名やプロパティは必ずUniversal名(末尾に `U` がつくもの)を使用すること。`Formula` ではなく `FormulaU` を使うのがプロの流儀だ。
3. トランザクション的運用
図形を削除するバッチ処理を組むときは、削除した「直後」にこのリカバリ関数を呼ぶのではなく、一連の削除・生成バッチがすべて完了した最後に1回だけ走らせる方が、無駄な負荷がかからず圧倒的に効率が良い。

結びにかえて

APIや外部データベースからデータを取得して図形をダイナミックに再構築するシステムにおいて、エラーハンドリングの甘さはそのままプロダクトの信用失墜に直結する。

「エラーが出たら手で直す」という昭和の思考法は今日で捨てよう。
万全のフェイルセーフをコードで担保し、「何度図形を消して再生成しても絶対に壊れない、強靭なVisio自動化アーキテクチャ」を君の手で構築してほしい。

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