【実務・中級編】ShapeSheetの全セクションを動的走査:Shape.Sectionプロパティを使った未知のプロパティ構造解析 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:ShapeSheet全セクション動的走査による「ブラックボックス」構造解析

開発現場でこんな絶望を味わったことはないだろうか。
「サードパーティ製のステンシルから配置した複雑なグループ図形。独自のカスタムプロパティや隠し数式が組み込まれているが、UIからは全体像が全く見えない。VBAで自動化しようにも、どのセルに何のデータが入っているのか皆目検討がつかない……」

Visioの真のパワーは、すべてのシェイプが内包する「ShapeSheet」という超高機能なスプレッドシート構造にある。しかし、このShapeSheetは多層構造(Section → Row → Cell)を極めており、愚直にプロパティをハードコーディングしようものなら、メンテナンス不可能なスパゲッティコードの完成だ。

今回は、未知のシェイプが持つすべての数式、値、プロパティを完全網羅し、ログとして丸裸にする「ShapeSheet動的走査エンジン」の設計思想と実装を授けよう。

1. なぜ「静的な参照」では実務で破綻するのか

初学者や素人がやりがちなミスがこれだ。

‘ 【アンチパターン】ハードコーディングされたセル参照
Dim shp As Visio.Shape
Set shp = ActivePage.Shapes(1)

‘ サードパーティ製シェイプでは、このセルのインデックスが存在しないか、
‘ 意図しない別のカスタムプロパティを指していてエラーになる
Dim propVal As String
propVal = shp.CellsU(“Prop.MyCustomData”).FormulaU

このアプローチが実務で必ず破綻する理由は3つある。
1. 構造の不定性: ベンダーや作成者によって、セクションや行のインデックス、名前(NameU)がバラバラである。
2. 存在しないセルへのアクセス: `CellsU`は、該当するセルが存在しない場合に容赦なく実行時エラー(Error 1004など)を吐き散らす。
3. パフォーマンスの劣化: 無駄な例外処理や固定値の総当たりは、数千個のシェイプを持つ図面において致命的な速度低下を招く。

我々が目指すべきは、「どのような構造であっても、オブジェクトモデルの階層を再帰的かつ安全にハントし尽くす」ジェネリックな設計だ。

2. ShapeSheetオブジェクトモデルの深淵

Visioのオブジェクト階層は、以下のピラミッド構造をしている。

Application
└─ Document
└─ Page
└─ Shape (単体 or グループ)
└─ Section (Visio.Section / visSecXxxx)
└─ Row (Visio.Row)
└─ Cell (Visio.Cell)

ここで重要なのは、すべてのシェイプがすべてのセクションを持っているわけではないという点だ。例えば、通常の四角形には「Shape Data(旧カスタムプロパティ)」セクション自体が存在しない場合がある。そのため、存在確認(Countのチェックやエラーハンドリング)を挟みながら、有効なセクションだけを動的にイテレートする必要がある。

3. 【プロダクションコード】ShapeSheet完全走査エンジン

以下のコードは、選択されたシェイプ(グループの場合は再帰的に子シェイプも含む)のShapeSheetを隅々まで舐め尽くし、イミディエイトウィンドウへ構造・数式・現在値を出力する実用モジュールだ。

実務のログ基盤やデータベース連携の事前調査(インスペクター)として、そのままプロジェクトに組み込んで即戦力として利用できる。

Option Explicit

‘ =========================================================================
‘ 模範解答:ShapeSheet全セクション動的走査・解析エンジン
‘ =========================================================================
Public Sub InspectSelectedShapeSheet()
Dim vsoSelection As Visio.Selection
Set vsoSelection = ActiveWindow.Selection

If vsoSelection.Count = 0 Then
MsgBox “解析対象のシェイプを選択してください。”, vbExclamation, “構造解析”
Exit Sub
End If

Debug.Print “=========================================”
Debug.Print ” ShapeSheet Dynamic Inspection Start”
Debug.Print “=========================================”

Dim shp As Visio.Shape
For Each shp in vsoSelection
Call TraverseShape(shp, 0)
Next shp

Debug.Print “=========================================”
Debug.Print ” Inspection Completed.”
Debug.Print “=========================================”
MsgBox “ShapeSheetの解析が完了しました。イミディエイトウィンドウを確認してください。”, vbInformation, “完了”
End Sub

‘ シェイプを再帰的に走査(グループ図形対応)
Private Sub TraverseShape(ByVal shp As Visio.Shape, ByVal indentLevel As Long)
Dim indent As String
indent = String(indentLevel 2, ” “)

Debug.Print indent & “[Shape] Name: ” & shp.Name & ” (ID: ” & shp.ID & “, Type: ” & GetShapeTypeName(shp.Type) & “)”

‘ 1. すべてのセクションを走査
Dim secIndex As Integer
Dim vsoSection As Visio.Section

For secIndex = 0 To shp.Sections.Count – 1
Set vsoSection = shp.Sections(secIndex)

‘ セクションが有効(削除されていない等)かチェック
If Not vsoSection Is Nothing Then
Dim secName As String
secName = GetSectionName(vsoSection.Index)

Debug.Print indent & ” └─ [Section] ” & secName & ” (Index: ” & vsoSection.Index & “, Rows: ” & vsoSection.Rows.Count & “)”

‘ 2. セクション内の行を走査
Dim rowIndex As Integer
Dim vsoRow As Visio.Row

For rowIndex = 0 To vsoSection.Rows.Count – 1
‘ 一部の特殊な行は削除されている場合があるためエラー対策
On Error Resume Next
Set vsoRow = vsoSection.Rows(rowIndex)
If Err.Number = 0 And Not vsoRow Is Nothing Then

Debug.Print indent & ” └─ [Row] Index: ” & vsoRow.Index & ” (Name: ” & vsoRow.Name & “)”

‘ 3. 行内のセルを走査
Dim cellIndex As Integer
Dim vsoCell As Visio.Cell

For cellIndex = 0 To vsoRow.Cells.Count – 1
Set vsoCell = vsoRow.Cells(cellIndex)
If Err.Number = 0 And Not vsoCell Is Nothing Then
‘ 数値や数式、ローカル名を取得
Dim cellName As String
Dim cellFormula As String
Dim cellResult As String

cellName = vsoCell.Name
cellFormula = vsoCell.FormulaU Invariant(U)形式の数式
cellResult = vsoCell.ResultStr(“”) ‘ 文字列としての評価値

‘ デフォルト値や空っぽの数式を省いてノイズを減らすロジックも有効だが、
‘ 全解析のため今回はすべて出力
Debug.Print indent & ” └─ [Cell] ” & cellName & ” = ” & cellFormula & ” [Value: ” & cellResult & “]”
End If
Err.Clear
Next cellIndex
End If
Err.Clear
On Error GoTo 0
Next rowIndex
End If
Next secIndex

‘ グループシェイプの場合は子シェイプを再帰処理
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)
Call TraverseShape(subShp, indentLevel + 1)
Next i
End If
End Sub

‘ セクション定数から可読性の高い名称を返すヘルパー関数
Private Function GetSectionName(ByVal secIndex As Integer) As String
Select Case secIndex
Case Visio.VisSectionIndices.visSectionObject: GetSectionName = “Shape Transform (Object)”
Case Visio.VisSectionIndices.visSectionFirstComponent To Visio.VisSectionIndices.visSectionLastComponent: GetSectionName = “Geometry”
Case Visio.VisSectionIndices.visSectionCharacter: GetSectionName = “Character”
Case Visio.VisSectionIndices.visSectionParagraph: GetSectionName = “Paragraph”
Case Visio.VisSectionIndices.visSectionTabs: GetSectionName = “Tabs”
Case Visio.VisSectionIndices.visSectionScratch: GetSectionName = “Scratchpad”
Case Visio.VisSectionIndices.visSectionConnectionPts: GetSectionName = “Connection Points”
Case Visio.VisSectionIndices.visSectionTextField: GetSectionName = “TextFields”
Case Visio.VisSectionIndices.visSectionControls: GetSectionName = “Controls”
Case Visio.VisSectionIndices.visSectionProp: GetSectionName = “Shape Data (Properties)”
Case Visio.VisSectionIndices.visSectionAction: GetSectionName = “Actions”
Case Visio.VisSectionIndices.visSectionLayer: GetSectionName = “Layers”
Case Visio.VisSectionIndices.visSectionUser: GetSectionName = “User-defined Cells”
Case Visio.VisSectionIndices.visSectionStatus: GetSectionName = “Status”
Case Else: GetSectionName = “Section_” & secIndex
End Select
End Function

‘ シェイプタイプの文字列表現
Private Function GetShapeTypeName(ByVal typeVal As Integer) As String
Select Case typeVal
Case Visio.VisShapeTypes.visTypeGroup: GetShapeTypeName = “Group”
Case Visio.VisShapeTypes.visTypeForeign: GetShapeTypeName = “Foreign (CAD/Bitmap)”
Case Visio.VisShapeTypes.visTypeInk: GetShapeTypeName = “Ink”
Case Visio.VisShapeTypes.visTypeShape: GetShapeTypeName = “Shape”
Case Else: GetShapeTypeName = “Unknown(” & typeVal & “)”
End Select
End Function

4. プロジェクト運用上の重要な知見(データベース連携への布石)

このコードを実行すると、イミディエイトウィンドウには膨大なテキストデータが出力される。実務でこのデータをどう活かすべきか、チーフアーキテクトとしての視点を共有しておこう。

① `FormulaU` と `Formula` の使い分け

国際化(多言語対応)されたVisio環境において、日本語ローカライズ版の`Formula`プロパティを使用すると、関数名(`GUARD`や`IF`など)が日本語化されてしまい、VBAの再代入時や外部システムとの連携時にパースエラーを起こす。
必ずユニバーサル名(Uが付くプロパティ:`FormulaU`, `CellsU`)を使用すること。 これがプロの鉄則だ。

② データベースやCSVへの書き出しへの拡張

上記コードの `Debug.Print` 部分を、ADO(ActiveX Data Objects)を使ったSQLite/SQL Serverへのバルクインサート、あるいはCSVファイル出力ストリームに置き換えるだけで、「社内製図面アセットの自動インベントリシステム」が完成する。
「どの図面に、どんなカスタムプロパティ(`Prop.xxx`)を持つシェイプがいくつ配置されているか」を夜間バッチ等で自動収集し、DBで一元管理したい現場では、この走査ロジックがそのままコアエンジンとなる。

総括

Visio VBAにおける最大の罠は、「見えている図形」と「背後にあるShapeSheet構造」のギャップに躓くことだ。オブジェクトモデルの隅々まで自ら手を伸ばし、動的に全構造をハントする仕組みを手に入れたあなたにもはや「ブラックボックス」は存在しない。

この知見を武器に、手作業による泥臭い図面チェックや属人化したメンテナンス作業を根絶やしにしてほしい。

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