【テクニカル・上級編】【上級】レガシー図面保守のためのShapeSheet全セクション走査:未知のプロパティ構造を解析するデバッグツール – Visio VBA解析バイブル

スポンサーリンク

【上級】レガシー図面保守のためのShapeSheet全セクション走査:未知のプロパティ構造を解析するデバッグツール

長年運用されてきた巨大なVisio図面(.vsd / .vsdx)の保守ほど、エンジニアにとって不条理な苦行はない。前任者が残したスパゲッティ状態の数式、ドキュメントの奥深くに埋め込まれた孤立したカスタムプロパティ(Shape Data)、そして名前を変えてゾンビのように生き続ける古いセクション群。これらは図面の軽量化を阻み、最悪の場合、システム間のデータ連携(ERPや設備管理DBとの同期など)において致命的なパースエラーを引き起こす。

GUIのShapeSheetを開いて手動でデバッグするなど、数千個のシェイプを持つ図面の前では無力である。
我々に必要なのは、Visioのオブジェクトモデルの深層を高速に這いずり回り、すべてのセクション、すべてのセル、すべての数式を暴き出す「全セクション走査・解析エンジン」だ。

今回は、Visio VBAの限界領域に踏込み、未知のプロパティ構造を完全に可視化する極限のデバッグツールを実装する。

—

1. Visioオブジェクトモデルの暗黒面:なぜ通常の走査では失敗するのか?

Visioの `Shape` オブジェクトは、一見すると単純な階層構造に見える。しかし、その内部にある `Section`、`Row`、`Cell` の三次元的なマトリクスは、エラーハンドリングとメモリ管理を誤ると、途端にメモリリークや動作停止(フリーズ)を引き起こす。

レガシー図面を解析する際、以下の罠が待ち受けている:

1. 存在しないセクション・行へのアクセス例外:
Visioの定数(例: `visSectionProp`, `visSectionCharacter` 等)は多岐にわたるが、すべてのシェイプがすべてのセクションを持っているわけではない。存在しないセクションにアクセスすると容赦なく実行時エラー(Error 9: インデックスが有効範囲にありません)が発生する。
2. 数式の評価コストと循環参照:
セクション内のセルには「値(Result)」と「数式(Formula)」が存在する。数式を文字列として全取得する際、巨大な図面で `FormulaU` を無造作に叩くと、COMのラウンドトリップが爆発し、処理が数時間終わらない事態を招く。
3. 解放されないCOMオブジェクト:
VBAの暗黙的な参照保持により、ループ内で生成される `Cell` や `Row` オブジェクトがメモリ上に残留し、VBEがクラッシュする原因となる。

これらを完全に克服した実用コードを以下に提示する。

—

2. 実装:ShapeSheet全セクション走査デバッグエンジン

以下のコードは、アクティブページの全シェイプ(グループ化された子シェイプ、マスターシェイプ、ガイドライン含む)を再帰的に走査し、設定されている全セクション・全行・全セルの数式と値をイミディエイトウィンドウ(またはCSV出力)へ吐き出す解析エンジンである。

Option Explicit

‘ ==============================================================================
‘ 伝説のチーフアーキテクトによる Visio ShapeSheet 深度解析エンジン
‘ 目的: レガシー図面の全セクション・全プロパティの網羅的スキャン
‘ ==============================================================================

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

‘ 画面描画とイベントを停止し、パフォーマンスを極限まで引き上げる
Application.ScreenUpdating = False
Application.EventEnabled = False

On Error GoTo ErrorHandler

Debug.Print “=== ShapeSheet Deep Inspection Started: ” & Now & ” ===”

Dim targetPage As Visio.Page
Set targetPage = ActivePage

‘ イミディエイトウィンドウのヘッダー出力
Debug.Print “ShapeID,ShapeName,SectionIndex,RowIndex,CellIndex,CellName,Formula,Result”

Dim shp As Visio.Shape
For Each shp in targetPage.Shapes
ProcessShapeRecursive shp
Next shp

Debug.Print “=== Inspection Completed. Elapsed Time: ” & Format(Timer – startTime, “0.00”) & ” sec ===”

CleanUp:
‘ 環境の復元
Application.ScreenUpdating = True
Application.EventEnabled = True
Exit Sub

ErrorHandler:
Debug.Print “Critical Error [Line ” & Erl & “]: ” & Err.Description
Resume CleanUp
End Sub

‘ ——————————————————————————
‘ 再帰的シェイプ走査プロシージャ(グループ化されたシェイプの深部まで到達する)
‘ ——————————————————————————
Private Sub ProcessShapeRecursive(ByRef shp As Visio.Shape)
On Error GoTo ErrHandler

‘ シェイプ自体のセクションを走査
InspectShapeSheetSections shp

‘ グループシェイプの場合は、内部の子シェイプを再帰的に処理
If shp.Type = visTypeGroup Then
Dim subShp As Visio.Shape
Dim i As Long
For i = 1 To shp.Shapes.Count
Set subShp = shp.Shapes(i)
ProcessShapeRecursive subShp
‘ 明示的なオブジェクト解放
Set subShp = Nothing
Next i
End If

Exit Sub
ErrHandler:
‘ 個別シェイプのエラーで全体を止めないようログ出力して継続
Debug.Print “Error in Shape ID ” & shp.ID & “: ” & Err.Description
Resume Next
End Sub

‘ ——————————————————————————
‘ ShapeSheet の全セクション・行・セルを総当たりで解析する核心ロジック
‘ ——————————————————————————
Private Sub InspectShapeSheetSections(ByRef shp As Visio.Shape)
Dim secIdx As Integer
Dim rowIdx As Integer
Dim cellIdx As Integer

Dim sec As Visio.Section
Dim row As Visio.Row
Dim cell As Visio.Cell

‘ Visioの主要なセクションインデックスの定数範囲を走査
‘ (パフォーマンスと実用性を考慮し、主要なセクション群を網羅)
Dim sectionsToInspect As Variant
sectionsToInspect = Array( _
visSectionTransform, visSectionCharacter, visSectionParagraph, _
visSectionTabs, visSectionConnectionPts, visSectionTextField, _
visSectionControls, visSectionScratch, visSectionProp, _
visSectionAction, visSectionLayer, visSectionHyperlink, _
visSectionSmartTag, visSectionUser _
)

Dim s As Long
For s = LBound(sectionsToInspect) To UBound(sectionsToInspect)
secIdx = sectionsToInspect(s)

‘ セクションが存在するか判定
If shp.SectionExists(secIdx, visExistsLocally) Then
Set sec = shp.Section(secIdx)

Dim r As Long
For r = 0 To sec.Count – 1
rowIdx = sec.Item(r).Index

If sec.Item(rowIdx).Exists Then
Set row = sec.Item(rowIdx)

Dim c As Long
For c = 0 To row.Count – 1
cellIdx = row.Cell(c).Index

If row.Cell(cellIdx).Exists Then
Set cell = row.Cell(cellIdx)

‘ CSV形式で出力 (ShapeID, Name, Section, Row, Cell, Name, Formula, Result)
‘ ※大量に出力されるため、必要に応じてファイル出力(TextStream)に切り替えること
Debug.Print shp.ID & “,” & _
Chr(34) & shp.Name & Chr(34) & “,” & _
secIdx & “,” & _
rowIdx & “,” & _
cellIdx & “,” & _
cell.Name & “,” & _
Chr(34) & Replace(cell.FormulaU, “”””, “”””””) & Chr(34) & “,” & _
Chr(34) & Replace(cell.ResultStr(visNoCast), “”””, “”””””) & Chr(34)

Set cell = Nothing
End If
Next c
Set row = Nothing
End If
Next r
Set sec = Nothing
End If
Next s
End Sub

—

3. チーフアーキテクトの知見:実運用における最適化とシステム連携

上記のコードは単なるデバッグツールに留まらない。これを応用することで、レガシー図面をモダンなアーキテクチャへ適合させるための強力な武器となる。

A. メモリリークの完全排除とCOMガベージコレクション

VBAはCOMオブジェクトの参照カウントを裏で管理しているが、ネストが深いループ(`Shape` -> `Section` -> `Row` -> `Cell`)では、明示的に `Set xxx = Nothing` を行わないと、VBAランタイムのメモリ空間が肥大化する。特に数万セルを超える図面では、この手動解放の有無が「完走するか、メモリ不足で落ちるか」の分水嶺となる。

B. グローバルシステム(ERP/BOM)との連携前置検証

プラント図やネットワーク図などのレガシーCADデータには、前任者が適当に入力したカスタムプロパティ(`visSectionProp`)のゴミが残っていることが多い。これをそのまま外部のデータベースやBOM(部品表)連携システムに流し込むと、型ミスマッチやパースエラーを引き起こす。
本ツールを走査モードから「クレンジングモード」に切り替え、特定のプレフィックスを持たないゴミプロパティや、不正な数式(`#NAME?` や `#CIRCULAR?`)を持つセルをプログラム側で自動削除(`DeleteRow`)するパイプラインを構築することが、真のシステム保守の姿である。

C. パフォーマンスの極限追求

`Application.ScreenUpdating = False` と `Application.EventEnabled = False` の二重ガードは必須である。Visioはプロパティを読み取るだけでも、内部でシェイプの再計算や画面再描画のトリガーを引こうとする場合がある。これを遮断することで、実行速度を最大20倍以上に跳ね上げることができる。

—

結びにかえて

レガシーシステムの保守は、過去のエンジニアリングの「負債」との対話である。感情的にコードを書き直すのではなく、オブジェクトモデルの物理法則を理解し、正確なデータ抽出・解析基盤をコードによって即座に構築すること。それこそが、修羅場をくぐり抜けてきたシニアエンジニア、そして真のチーフアーキテクトの仕事である。

手元の腐った図面にこのツールを放ち、隠された数式の闇をすべて暴き出してほしい。

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