【テクニカル・上級編】Shape.Sectionプロパティを使った動的ShapeSheet解析:未定義のシェイプデータやUser行を全探索してデバッグ表示するツール – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:未知のShapeSheetを丸裸にする動的セクション全探索エンジンの構築

長年、大規模なプラント図面、ネットワークトポロジ、そして複雑なワークフロー管理システムの裏側で、Visioのドキュメントと格闘してきたエンジニアなら誰しも一度は絶望したことがあるはずだ。

「この図形、いったいどこにデータを隠し持っているんだ?」

他人が構築した、あるいはサードパーティ製のステンシルからドロップされたブラックボックスのシェイプ。ShapeSheetを開き、プロパティウィンドウを覗いても見つからない。しかし、特定のマクロを実行すると何故か動的に挙動が変わる。その正体は、プログラムから動的に生成された、あるいは通常は隠蔽されている`Visio.Section`、`Visio.Row`、そして`Visio.Cell`の迷宮である。

GUIのShapeSheetエディタをポチポチと確認するデバッグ手法など、プロのエンジニアの取る手段ではない。今回は、Visioのオブジェクトモデルの深淵に潜り込み、未定義のシェイプデータ(Props)やユーザー定義セル(User)、さらには幾何学セクションに至るまで、存在するすべてのShapeSheet構造を動的に走査・列挙する極限のデバッグツールを授けよう。

1. Visioオブジェクトモデルの隠された罠とセクション走査の極意

Visio VBAにおける最大の悪夢は、「存在しないプロパティやインデックスへのアクセスによる実行時エラー(Error 9: インデックスが有効範囲にありません)」と、それに伴うメモリリーク(Comオブジェクトの解放漏れ)である。

特に`Shape.Section`コレクションは一筋縄ではいかない。WordやExcelのコレクションとは異なり、VisioのShapeSheetは「スパース(疎)配列」の極みだ。セクションインデックス(例: `visSectionProp`や`visSectionUser`など)は連続しておらず、図形が保持していないセクションにアクセスした瞬間、容赦なくVBAの実行時エラーが飛ぶ。

さらに、パフォーマンスの観点からも注意が必要だ。何千ものシェイプが配置されたページで、全セクション・全行・全セルを無思考にループさせれば、COMのクロスプロセス通信のオーバーヘッドにより、VBAの実行は数分間フリーズする。

これらを克服するためのアーキテクチャ上の要件は以下の3点だ。
1. 存在確認とエラーハンドリングの徹底:存在しないセクション・行を安全にスキップする堅牢性。
2. 定数とインデックスの動的解決:Visioの型ライブラリ(`VisSectionIndices`等)を利用した網羅的アプローチ。
3. イミディエイトウィンドウへの効率的なストリーミング出力:巨大なログでも一瞬で出力し切る文字列構築の最適化。

2. 実装:ShapeSheet動的全探索デバッグエンジン

以下のコードは、選択中のシェイプ(または指定したシェイプ)が持つすべてのShapeSheetセクションを強制的に列挙し、行名、セル名、そしてその数値をイミディエイトウィンドウへダンプする完全実用コードである。

標準モジュールに貼り付け、Visio上で任意のシェイプを選択した状態で `DumpShapeSheetAnatomy` を実行してほしい。

Option Explicit

‘ =================================================================================
‘ 伝説のチーフアーキテクトによる実装: Visio ShapeSheet 動的全探索エンジン
‘ =================================================================================

Public Sub DumpShapeSheetAnatomy()
Dim vsoShape As Visio.Shape

‘ 選択チェック
If ActiveWindow.Selection.Count = 0 Then
MsgBox “解析対象のシェイプを選択してください。”, vbExclamation, “ShapeSheet Explorer”
Exit Sub
End If

Set vsoShape = ActiveWindow.Selection(1)

Debug.Print “=================================================================”
Debug.Print ” [START] ShapeSheet Anatomy Dump”
Debug.Print ” Target Shape ID: ” & vsoShape.ID & ” | Name: ” & vsoShape.Name
Debug.Print “=================================================================”

‘ メインの走査処理を実行
Call TraverseAllSections(vsoShape)

Debug.Print “=================================================================”
Debug.Print ” [END] Dump Completed Successfully.”
Debug.Print “=================================================================”
End Sub

Private Sub TraverseAllSections(ByRef vsoShape As Visio.Shape)
Dim secIdx As Long
Dim rowIdx As Long
Dim cellIdx As Long

Dim vsoSec As Visio.Section
Dim vsoRow As Visio.Row
Dim vsoCell As Visio.Cell

On Error GoTo ErrorHandler

‘ Visioが定義する主要なセクションインデックスの範囲を走査
‘ 注: セクション定数はVisioのバージョンにより拡張されるため、代表的な範囲を網羅する
For secIdx = 0 To biMaxSectionIndex()

‘ セクションが存在するかどうかを安全にチェックするためエラー捕捉を一時有効化
On Error Resume Next
Set vsoSec = vsoShape.Section(secIdx)
Dim errNum As Long: errNum = Err.Number
On Error GoTo ErrorHandler

If errNum = 0 And Not vsoSec Is Nothing Then
Debug.Print “————————————————–”
Debug.Print ” Section Found: Index = ” & secIdx & ” | Name/Type = ” & GetSectionName(secIdx)
Debug.Print “————————————————–”

‘ 行の走査
For rowIdx = 0 To vsoSec.Count – 1
On Error Resume Next
Set vsoRow = vsoSec.Row(rowIdx)
errNum = Err.Number
On Error GoTo ErrorHandler

If errNum = 0 And Not vsoRow Is Nothing Then
Dim rowName As String
On Error Resume Next
rowName = vsoRow.Name
If Err.Number <> 0 Then rowName = “[Row-” & rowIdx & “]”
On Error GoTo ErrorHandler

‘ セルの走査
For cellIdx = 0 To vsoRow.Count – 1
On Error Resume Next
Set vsoCell = vsoRow.Cell(cellIdx)
errNum = Err.Number
On Error GoTo ErrorHandler

If errNum = 0 And Not vsoCell Is Nothing Then
Dim cellName As String, cellFormula As String, cellResult As String
On Error Resume Next
cellName = vsoCell.Name
cellFormula = vsoCell.FormulaU
cellResult = vsoCell.ResultStr(“”)
On Error GoTo ErrorHandler

‘ 出力フォーマット: [セクション] 行名.セル名 = 数式 (評価値)
Debug.Print ” -> Row[” & rowIdx & “:” & rowName & “] Cell[” & cellIdx & “:” & cellName & “] ” & _
“Formula: ” & cellFormula & ” | Value: ” & cellResult

Set vsoCell = Nothing
End If
Next cellIdx
Set vsoRow = Nothing
End If
Next rowIdx
Set vsoSec = Nothing
End If
Next secIdx

Exit Sub

ErrorHandler:
‘ 予期せぬCOMエラーはログに残して継続(堅牢性の担保)
Debug.Print “[Warning] Error encountered at Sec:” & secIdx & ” Row:” & rowIdx & ” Cell:” & cellIdx & ” -> ” & Err.Description
Resume Next
End Sub

‘ =================================================================================
‘ ヘルパー関数: セクションインデックスから人間が読める名称を返す
‘ =================================================================================
Private Function GetSectionName(ByVal secIndex As Long) As String
Select Case secIndex
Case VisSectionIndices.visSectionTransform: GetSectionName = “Shape Transform”
Case VisSectionIndices.visSectionShapeInsp: GetSectionName = “Shape Inspector”
Case VisSectionIndices.visSectionFirstComponent: GetSectionName = “First Component”
Case VisSectionIndices.visSectionConnectionPts: GetSectionName = “Connection Points”
Case VisSectionIndices.visSectionTextField: GetSectionName = “Text Field”
Case VisSectionIndices.visSectionControls: GetSectionName = “Controls”
Case VisSectionIndices.visSectionProp: GetSectionName = “Shape Data (Properties)”
Case VisSectionIndices.visSectionHelp: GetSectionName = “Help”
Case VisSectionIndices.visSectionScratchpad: GetSectionName = “Scratchpad”
Case VisSectionIndices.visSectionAction: GetSectionName = “Actions”
Case VisSectionIndices.visSectionLayer: GetSectionName = “Layers”
Case VisSectionIndices.visSectionUser: GetSectionName = “User-defined Cells”
Case VisSectionIndices.visSectionSmartTag: GetSectionName = “Smart Tags”
Case Else: GetSectionName = “Custom / Undefined Section (” & secIndex & “)”
End Select
End Function

‘ 探索上限のセクションインデックスを返す
Private Function biMaxSectionIndex() As Long
‘ Visio 2010以降で定義されている主要な最大セクションインデックスをカバー
biMaxSectionIndex = 255
End Function

3. チーフアーキテクトの眼:このコードが持つ技術的優位性

上記のコードは一見して単純なループに見えるかもしれないが、レガシーシステムを保守するシニアエンジニアの知見が随所に組み込まれている。

① `On Error Resume Next` の戦略的運用

VBAにおけるエラーハンドリングのタブー視されがちな「ゴリ押し」だが、スパースなCOMコレクションを総当たりでハントする文脈においては唯一無二の正解となる。存在しないセクションや行番号に対して、事前に存在チェック用のAPIを叩くよりも、エラーをトラップして高速に無視していく方が、COMの往復回数を減らし結果的に圧倒的なパフォーマンスを発揮する。

② `FormulaU` と `ResultStr` の使い分け

ShapeSheetの解析において、生データ(数式)を見るべきか、計算結果を見るべきかはデバッグの成否を分ける。

  • `FormulaU`:国際標準(Universal)の数式文字列を取得。他のシェイプへの参照や数式バグを暴くのに必須。
  • `ResultStr(“”)`:単位系を考慮した最終的な評価値を文字列として取得。

この両方を同時にダンプすることで、「数式が壊れていないか」「意図した値がパースされているか」を一目で判断できる。

③ オブジェクトの明示的解放(メモリ管理の作法)

VBAのガベージコレクションは頼りにならない。特にCOMオブジェクト(`Visio.Section`, `Visio.Row`, `Visio.Cell`)をループ内で大量に生成・破棄する場合、明示的に `Set vsoCell = Nothing` を行わないと、Visioのプロセス内にCOM参照が残存し、マクロ終了後もメモリリークや最悪の場合はVisio自体のクラッシュを引き起こす。このコードでは、ループのスコープごとにオブジェクトを確実に解放している。

4. 現場で使える応用テクニック:未定義シェイプデータの自動検出と一括削除

この動的全探索エンジンの応用として、現場で重宝する「ゴミデータ(誰も参照していないUser行やProp行)の自動クレンジング」への展開について触れておこう。

例えば、過去の開発者が残した不要な`Prop.Row_1`などが図形の肥大化を招いている場合、`TraverseAllSections` を拡張し、特定の条件(例:数式が固定値かつ空文字、特定のプレフィックスを持つ)に合致するセルや行を `vsoSec.DeleteRow(rowIdx)` で動的に削除するスクリプトへと昇華させることができる。

レガシーなVisio図面資産をデジタル変革(DX)し、Web APIやRPAと連携させる前段階として、このような「図面の構造化・正常化スクリプト」を仕込むことが、シニアエンジニアに求められる真の腕の見せ所である。

図面の闇に怯えるな。オブジェクトモデルを掌握し、Visioのすべてをコードの支配下に置け。

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