【テクニカル・上級編】Shape.Cast的アプローチ:Dictionaryクラスを活用したShapeオブジェクトのプロパティ拡張とメタデータ一時保持パターン – Visio VBA解析バイブル

スポンサーリンク

Shape.Cast的アプローチ:Dictionaryクラスを活用したShapeオブジェクトのプロパティ拡張とメタデータ一時保持パターン

Visio VBAにおける最大のボトルネック、それは「ShapeSheetの肥大化」と「データの永続性と一時性の混同」にある。

実務で高度な図面自動生成や外部システム連携(CADやBIM、あるいは社内資産管理DBとの同期)を行うとき、すべてのデータをShapeSheetの「ユーザー定義セル(User-defined cells)」や「図形データ(Shape Data)」に書き込もうとする設計者は二流だ。不要なセル追加はファイルサイズを膨らませ、何よりVisioの再計算エンジン(Calc)を無駄に揺さぶり、巨大図面の描画パフォーマンスを致命的に破壊する。

さらに、VBAには強力な型システムやオブジェクト指向における「ダウンキャスト(C#の `as` や `Cast`)」のような動的なプロパティ拡張メカニズムが存在しない。`Visio.Shape` オブジェクトに独自のメソッドを生やしたり、複雑な構造体や別インスタンスを直接ぶら下げたりすることは、標準のオブジェクトモデルでは不可能である。

このアーキテクチャの限界を突破するため、`Scripting.Dictionary` を用いたメモリ内メタデータ管理層(レイヤー) を構築する。Shapeの不変の識別子である `ID` をキーとして、揮発性の複雑なデータ構造やクラスインスタンスを完全分離して管理する、シニアエンジニア必携のデザインパターンを授けよう。

1. なぜShapeSheetへの直接書き込みでは破綻するのか?

多くのVBAプログラマは、図形に何らかの状態を持たせたい時、即座に以下のようなコードを書く。

‘ 悪臭を放つアンチパターン:ShapeSheetの乱用
vShape.CellsU(“Prop.MyCustomState”).FormulaU = “””Processing”””

このアプローチには、エンジニアリングの観点から3つの致命的な欠陥がある。

1. パフォーマンスの劣化: ShapeSheetへの書き込み・読み込みは、COMの境界を跨ぐコストに加えて、Visioの数式再評価エンジンをトリガーする。数千個のシェイプを持つ図面でこれをやると、処理が数分単位でフリーズする。
2. 型制約の壁: ShapeSheetが保持できるのは文字列、数値、数式、Booleanのみである。配列、Dictionary、あるいは独自の業務ロジックを持つVBAのクラスインスタンス(Collectionや独自Classなど)を保持することは物理的に不可能である。
3. トランザクション管理の欠如: 処理の途中でエラーが発生した場合、ShapeSheetに書き込まれた中途半端な状態(汚染データ)が図面ファイル内に残存し、手動でクリーンアップしなければならなくなる。

我々が求めるべきは、「図面ファイル(物理)と、実行時メモリ(論理)の完全な関心事の分離」 である。

2. 設計思想:Shape.Cast的アプローチ(Dictionaryによるプロパティ拡張)

概念は極めてシンプルだ。
Visioの `Shape` オブジェクト自体は「描画と基本幾何情報の器」としてのみ扱い、業務上のメタデータや処理状態、関連オブジェクトへの参照は、すべてVBAのメモリ上に保持した `Scripting.Dictionary` で管理する。

ここでキーとなるのが、Visioのドキュメントスコープ内における `Shape.ID` である。

  • `Shape.ID` は、同一ページ内において一意(Unique)であり、図形が生存している限り不変である。
  • これをDictionaryのキーに採用することで、O(1) の極めて高速なルックアップを実現する。

[ Visio Page ] ──────> [ Shape (ID: 5) ]

▼ (ID: 5 をキーとして紐付け)
[ Scripting.Dictionary ] ──> [ Custom Metadata Class / Array ]

3. 実装:堅牢なメタデータ管理クラスの構築

それでは、実戦投入に耐えうるコードを提示する。
ここでは、単なるプリミティブ型の保持に留まらず、「任意のカスタムクラス(状態・配管情報・外部DBキー等)」 をShapeにキャストするかのように紐付けるマネージャーモジュールを設計する。

準備:カスタムデータ用クラス (`C_ShapeMetaData`)

まず、図形に持たせたいリッチなデータを格納するクラスを作成する(クラスモジュール名: `C_ShapeMetaData`)。

‘ ==========================================
‘ クラスモジュール: C_ShapeMetaData
‘ ==========================================
Option Explicit

Public EntityID As String
Public ProcessStatus As String
Public RelatedNodeIDs() As String
Public LastUpdated As Date

‘ 初期化処理
Private Sub Class_Initialize()
LastUpdated = Now
ReDim RelatedNodeIDs(0)
End Sub

コア:メタデータ管理マネージャー (`M_ShapeManager`)

次に、Dictionaryをカプセル化し、メモリリークを防ぐためのライフサイクル管理を行う標準モジュール(またはクラス)を実装する。

‘ ==========================================
‘ 標準モジュール: M_ShapeManager
‘ ==========================================
Option Explicit

‘ ページごとのShape IDをキーに、カスタムメタデータを保持するDictionary
‘ Key: Shape.ID (Long), Item: C_ShapeMetaData
Private m_MetaStore As Scripting.Dictionary

‘ — 初期化 —
Public Sub InitializeManager()
Set m_MetaStore = New Scripting.Dictionary
m_MetaStore.CompareMode = BinaryCompare
End Sub

‘ — メタデータのバインド(キャスト的アプローチ) —
Public Sub AttachMetadata(ByVal targetShape As Visio.Shape, ByVal meta As C_ShapeMetaData)
If m_MetaStore Is Nothing Then InitializeManager()

Dim shpID As Long
shpID = targetShape.ID

‘ 既に存在する場合は上書き、なければ追加
If m_MetaStore.Exists(shpID) Then
Set m_MetaStore(shpID) = meta
Else
m_MetaStore.Add shpID, meta
End If
End Sub

‘ — メタデータの取得(キャストの代替) —
Public Function GetMetadata(ByVal targetShape As Visio.Shape) As C_ShapeMetaData
If m_MetaStore Is Nothing Then
Set GetMetadata = Nothing
Exit Function
End If

Dim shpID As Long
shpID = targetShape.ID

If m_MetaStore.Exists(shpID) Then
Set GetMetadata = m_MetaStore(shpID)
Else
Set GetMetadata = Nothing
End If
End Function

‘ — 終了処理・メモリ解放 —
Public Sub TerminateManager()
If Not m_MetaStore Is Nothing Then
m_MetaStore.RemoveAll
Set m_MetaStore = Nothing
End If
End Sub

4. 応用:バッチ処理におけるパフォーマンスとメモリ安全性の極限追求

実際の業務システム連携では、数千の図形をループ処理し、外部APIやDBとの同期を行う。このパターンを用いた実用的なメインプロシージャの記述例を示す。

‘ ==========================================
‘ メイン実行プロシージャ
‘ ==========================================
Public Sub ExecuteHighSpeedProcessing()
‘ エラーハンドリングによる確実なメモリ解放の担保
On Error GoTo ErrorHandler

‘ マネージャーの初期化
InitializeManager

Dim vPage As Visio.Page
Set vPage = ActivePage

Dim vShape As Visio.Shape
Dim targetCount As Long
targetCount = vPage.Shapes.Count

Debug.Print “=== 処理開始: 対象図形数 = ” & targetCount & ” ===”

Dim startTime As Double
startTime = Timer

‘ 1. メタデータの生成とアタッチ(高速インメモリ処理)
Dim i As Long
For i = 1 To targetCount
Set vShape = vPage.Shapes(i)

‘ 特定のレイヤーやマスターシェイプのフィルタリング
If vShape.Master IsNot Nothing Then
‘ カスタムメタデータインスタンスの生成
Dim meta As C_ShapeMetaData
Set meta = New C_ShapeMetaData

meta.EntityID = “ENT-” & Format(vShape.ID, “00000”)
meta.ProcessStatus = “Pending”
ReDim meta.RelatedNodeIDs(1)
meta.RelatedNodeIDs(0) = “NODE-A”
meta.RelatedNodeIDs(1) = “NODE-B”

‘ メモリ上のDictionaryに紐付け
AttachMetadata vShape, meta
End If
Next i

Debug.Print “メタデータアタッチ完了: ” & Format(Timer – startTime, “0.00秒”)

‘ 2. 紐付けられたメタデータを用いた並行・順次ビジネスロジックの実行
For i = 1 To targetCount
Set vShape = vPage.Shapes(i)

‘ キャスト的アプローチでメタデータを取得
Dim currentMeta As C_ShapeMetaData
Set currentMeta = GetMetadata(vShape)

If Not currentMeta Is Nothing Then
‘ ShapeSheetを汚さず、メモリ上で高速にステータス変更
currentMeta.ProcessStatus = “Synced”

‘ 必要最低限の描画反映のみをShapeSheetに行う(最小限のCOM往復)
If currentMeta.ProcessStatus = “Synced” Then
‘ 例: 処理済みなら枠線を緑にする等
vShape.CellsU(“LineColor”).FormulaU = “RGB(0,180,0)”
End If
End If
Next i

Debug.Print “全処理完了 経過時間: ” & Format(Timer – startTime, “0.00秒”)

CleanUp:
‘ 3. 明示的なメモリ解放
TerminateManager
Exit Sub

ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

5. シニアアーキテクトが知るべき「罠」と極意

このアーキテクチャを導入するにあたり、VBAの裏側にあるメモリ管理の闇と制約について知っておくべきことがある。

1. Shape.IDの罠(図形の削除とコピー)

Visioにおいて、一度アタッチした `Shape.ID` は、図形が削除されると再利用される(Recycleされる)ことがある。また、図形を「コピー&ペースト」すると、新しく生成されたシェイプには「全く新しいID」が採番される。
そのため、長期間にわたるセッションや、ユーザーが手動で図形をコピー&ペーストするようなインタラクティブな編集環境下では、Dictionaryのキーの整合性が崩れるリスクがある。

  • 対策: このパターンは、「一連のバッチ処理(インポート、レイアウト自動計算、エクスポートなど)のトランザクション内」で完結させる揮発性データ管理として用いるのが最も堅牢である。永続化が必要なデータは、最終的なコミットの段階で初めてShapeSheetのカスタムプロパティや外部DBへ書き出すこと。

2. オブジェクト参照の循環とメモリリーク

VBAの `Scripting.Dictionary` は、オブジェクトの参照(`Set` による代入)を保持する場合、Dictionary自体を破棄する前に内部のアイテムを適切に解放(`Nothing`代入や`RemoveAll`)しないと、COMオブジェクトの参照カウントが残存し、Visioプロセスがメモリリークを引き起こす。
上記のコードで `TerminateManager` 内にて `m_MetaStore.RemoveAll` を呼び出しているのは、このメモリリークを完全に防ぐための定石である。

総括

VBAはレガシーな言語と揶揄されることが多いが、それは開発者がオブジェクトモデルの制約に屈し、場当たり的なコードを書き続けてきた結果に過ぎない。

ShapeSheetという「遅くて制限の多い永続化ストレージ」に複雑なロジックや一時データを押し込める悪癖を断ち切り、`Scripting.Dictionary` を用いたインメモリ・レイヤーによるプロパティ拡張(Shape.Cast的アプローチ)を採用せよ。それこそが、数千・数万のシェイプを軽快に操る、プロフェッショナルなVisio自動化システムの唯一無二の解である。

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