Visio VBAにおけるMaster参照の完全解剖:シェイプアイデンティティの正確な特定とMasterの動的再同期アーキテクチャ
大規模なプラント設計図解、ネットワーク構成図、またはエンタープライズの業務プロセスフロー(BPMN)を長年運用している現場において、Visioドキュメントの「腐朽(Rot)」は避けて通れない問題です。
特に問題となるのが、図面上のシェイプ(Shape)と、その定義元であるマスターシェイプ(Master)の乖離です。
ステンシル(`.vss` / `.vssx`)のマスターを改修・最適化しても、既存のドキュメント上にドラッグ&ドロップされたシェイプには自動的に反映されません。結果として、旧バージョンのバグを含んだプロパティ構成や古いShapeSheet数式を保持したままのシェイプが、数千ものドキュメント内に「レガシーの遺物」として取り残されることになります。
本記事では、Visio VBAの内部オブジェクトモデルに深く切り込み、図面上のシェイプがどのマスターから派生したのかをミリ秒単位のオーバーヘッドもなく正確に特定し、最新のマスター定義へと動的再同期(Update)する決定版のアーキテクチャを解説します。
—
1. Visioマスター参照のメカニズムと隠された罠
Visioのオブジェクトモデルを正しく理解していない開発者が最も陥りやすい罠、それは 「`Shape.Master` は外部ステンシルのマスターを直指ししているわけではない」 という点です。
ドキュメントステンシル(Document Stencil)のキャッシュ構造
マスターシェイプを外部ステンシルからページ上に配置した瞬間、Visioは対象のドキュメント内部に存在する隠されたステンシル(ドキュメントステンシル:`Document.Masters`)へ、そのマスターのローカルコピーを作成します。
[外部ステンシル (.vssx)]
│ (Drag & Drop)
▼
[ドキュメントステンシル (Document.Masters)] ──<参照>── [ページ上のShape]
▲
└── ※ Shape.Master が指しているのは「ここ」であり、外部ステンシルではない!
したがって、`Shape.Master` プロパティを参照した際に返されるのは、ドキュメント内部にキャッシュされた `Visio.Master` オブジェクト です。
判定を難しくする3大要因
1. `Shape.Master` が `Nothing` である可能性:
ユーザーが作図ツール(四角形や直線)で直接描画したシェイプや、グループ化によって新たに生成された最上位シェイプの場合、`Shape.Master` は `Nothing` を返します。ぬるぽ(Null Reference)ガードなしのアクセスは一発で実行時エラーを発起させます。
2. マスター名(`Name` と `NameU`)の揺らぎ:
VisioのUI言語環境(日本語/英語など)によって `Master.Name` は変動します。堅牢なシステムを構築するためには、言語に依存しないユニバーサル名(`Master.NameU`)の検証が絶対条件となります。
3. ドキュメントステンシル内のコピー重複(`[Master Name].XX`):
外部ステンシルから同名のマスターが複数回、異なるタイミングで引き込まれると、ドキュメントステンシル内で `Process.12` のようにサフィックスが付与され、単純な文字列比較 (`Master.NameU = “Process”`) が破綻します。
—
2. 解決策:多角的一元特定(Multi-Layer Validation)アーキテクチャ
単なる `Shape.Master.NameU` の比較に頼るのではなく、現場で耐えうるシステムを構築するには3層の検証機構を実装します。
1. 第1層:Master Pointer Check
`Shape.Master` が存在するか(`Is Nothing` でないか)の高速境界判定。
2. 第2層:Base Name / Unique Identification Check
`Master.NameU` からサフィックスを取り除いた正規化判定、あるいはマスター作成時に注入した `User.SourceGUID` などの特定Cellの評価。
3. 第3層:ShapeSheet User-Defined Cell Verification
マスターシェイプ側で定義された一意の識別子(`User.MasterVersion` 等)を直接走査。
—
3. 実装:堅牢なMaster参照検証と自動アップデート・エンジン
以下に挙げるコードは、エンタープライズのレガシー環境(Visio 2010〜2021/365 Desktop)で動作することを保証した製品レベルのVBAモジュールです。
高精度タイマーによるパフォーマンス計測(Win32 API利用)、描画パイプラインの停止、明示的COMメモリ解放、データの引継ぎロジックをすべて完備しています。
標準モジュール:`mod_MasterUpdater.bas`
Attribute VB_Name = “mod_MasterUpdater”
Option Explicit
‘ ==============================================================================
‘ Win32 API 宣言(高精度パフォーマンス計測用)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” (lpFrequency As Currency) As Long
End If
‘ ==============================================================================
‘ 定数定義
‘ ==============================================================================
Private Const USER_CELL_MASTER_ID As String = “User.MasterGUID”
Private Const USER_CELL_VERSION As String = “User.SchemaVersion”
‘ ——————————————————————————
‘ メイン処理:ページ内の全シェイプを検証し、旧マスターを最新に差し替える
‘ ——————————————————————————
Public Sub Execute_Master_Synchronization()
Dim vsoPage As Visio.Page
Dim vsoTargetStencil As Visio.Document
Dim startTime As Currency, endTime As Currency, freq As Currency
Dim updatedCount As Long
Dim errCount As Long
‘ 精密タイマー開始
QueryPerformanceFrequency freq
QueryPerformanceCounter startTime
‘ パフォーマンス最適化:グラフィック描画およびイベント発火の全停止
Dim originalEventsState As Boolean
Dim originalScreenState As Long
originalEventsState = Visio.Application.EventsEnabled
originalScreenState = Visio.Application.ScreenUpdating
Visio.Application.EventsEnabled = False
Visio.Application.ScreenUpdating = 0
Visio.Application.DeferRecalc = True
On Error GoTo ErrorHandler
Set vsoPage = Visio.ActivePage
If vsoPage Is Nothing Then
MsgBox “有効なアクティブページが存在しません。”, vbCritical
GoTo CleanUp
End If
‘ 最新のマスターが格納されているソースステンシルをReadOnlyで開く
‘ ※実業務では絶対パス、またはシステム環境変数から取得することを推奨
Dim stencilPath As String
stencilPath = Visio.Application.ActiveDocument.Path & “Core_Equipment_Stencil.vssx”
On Error Resume Next
Set vsoTargetStencil = Visio.Application.Documents.OpenEx(stencilPath, visOpenDocked Or visOpenRO Or visOpenHidden)
On Error GoTo ErrorHandler
If vsoTargetStencil Is Nothing Then
MsgBox “最新の定義ステンシルを開けませんでした: ” & stencilPath, vbCritical
GoTo CleanUp
End If
‘ ページ内シェイプの再帰走査と更新を実行
updatedCount = 0
errCount = 0
Call ProcessShapesCollection(vsoPage.Shapes, vsoTargetStencil, updatedCount, errCount)
QueryPerformanceCounter endTime
Dim elapsedTimeMs As Double
elapsedTimeMs = ((endTime – startTime) / freq) 1000
MsgBox “同期処理が完了しました。” & vbCrLf & _
“・更新成功シェイプ数: ” & updatedCount & ” 件” & vbCrLf & _
“・スキップ/エラー数: ” & errCount & ” 件” & vbCrLf & _
“・総処理時間: ” & Format(elapsedTimeMs, “0.00”) & ” ms”, vbInformation, “処理結果”
CleanUp:
‘ 状態の復元と明示的COMオブジェクト解放
Visio.Application.DeferRecalc = False
Visio.Application.ScreenUpdating = originalScreenState
Visio.Application.EventsEnabled = originalEventsState
If Not vsoTargetStencil Is Nothing Then
vsoTargetStencil.Close
Set vsoTargetStencil = Nothing
End If
Set vsoPage = Nothing
Exit Sub
ErrorHandler:
errCount = errCount + 1
Debug.Print “[ERROR] Master Update Failed: ” & Err.Number & ” – ” & Err.Description
Resume Next
End Sub
‘ ——————————————————————————
‘ シェイプコレクションの安全な再帰走査
‘ ——————————————————————————
Private Sub ProcessShapesCollection(ByRef vsoShapes As Visio.Shapes, _
ByRef vsoTargetStencil As Visio.Document, _
ByRef refUpdatedCount As Long, _
ByRef refErrCount As Long)
Dim i As Long
Dim currentShape As Visio.Shape
Dim masterName As String
Dim isNeedsUpdate As Boolean
‘ 逆順ループ(削除・置換に伴うコレクションインデックスの狂いを防ぐため)
For i = vsoShapes.Count To 1 Step -1
Set currentShape = vsoShapes.Item(i)
‘ グループシェイプの場合は内部を再帰走査
If currentShape.Type = visTypeGroup And currentShape.Master Is Nothing Then
Call ProcessShapesCollection(currentShape.Shapes, vsoTargetStencil, refUpdatedCount, refErrCount)
Else
‘ マスター派生図形かの正確な特定
If IsTargetMasterShape(currentShape, “Eq_Server_Rack”, “{A4E88C11-92D3-4F32-8812-70C9142E5B12}”) Then
‘ バージョン判定による必要性の確認
If IsShapeOutdated(currentShape, 2.0) Then
‘ シェイプ置換とShapeSheetデータの引継ぎ
If ReplaceShapeWithLatestMaster(currentShape, vsoTargetStencil, “Eq_Server_Rack”) Then
refUpdatedCount = refUpdatedCount + 1
Else
refErrCount = refErrCount + 1
End If
End If
End If
End If
‘ COMオブジェクトの明示的解放(メモリリーク/GDIリソースの枯渇を防止)
Set currentShape = Nothing
Next i
End Sub
‘ ——————————————————————————
‘ シェイプのマスター特定ロジック(多角的一元特定)
‘ ——————————————————————————
Private Function IsTargetMasterShape(ByRef vsoShape As Visio.Shape, _
ByVal targetNameU As String, _
ByVal targetGUID As String) As Boolean
IsTargetMasterShape = False
‘ 1. Master参照の有無を判定
If vsoShape.Master Is Nothing Then Exit Function
‘ 2. ユニバーサル名によるベース名比較(サフィックス .XX の除去対応)
Dim baseMasterName As String
baseMasterName = vsoShape.Master.NameU
‘ ドキュメントステンシル上で付与される .123 等のドット以降の数値を正規化
Dim dotPos As Long
dotPos = InStrRev(baseMasterName, “.”)
If dotPos > 0 Then
If IsNumeric(Mid(baseMasterName, dotPos + 1)) Then
baseMasterName = Left(baseMasterName, dotPos – 1)
End If
End If
If UCase$(baseMasterName) = UCase$(targetNameU) Then
IsTargetMasterShape = True
Exit Function
End If
‘ 3. ShapeSheetのUser定義セル(GUID)による完全一致判定(名前に依存しない)
If vsoShape.CellExistsU(USER_CELL_MASTER_ID, visExistsAnywhere) <> 0 Then
Dim shapeGUID As String
shapeGUID = vsoShape.CellsU(USER_CELL_MASTER_ID).ResultStr(visNone)
If UCase$(shapeGUID) = UCase$(targetGUID) Then
IsTargetMasterShape = True
Exit Function
End If
End If
End Function
‘ ——————————————————————————
‘ シェイプが定義より古いか(SchemaVersion判定)
‘ ——————————————————————————
Private Function IsShapeOutdated(ByRef vsoShape As Visio.Shape, ByVal requiredVersion As Double) As Boolean
IsShapeOutdated = False
If vsoShape.CellExistsU(USER_CELL_VERSION, visExistsAnywhere) <> 0 Then
Dim currentVersion As Double
currentVersion = vsoShape.CellsU(USER_CELL_VERSION).Result(visNumber)
If currentVersion < requiredVersion Then
IsShapeOutdated = True
End If
Else
' バージョンCellが存在しない旧型シェイプはすべて更新対象とみなす
IsShapeOutdated = True
End If
End Function
' ------------------------------------------------------------------------------
' シェイプの置換とデータコンテキスト(ShapeData / 位置 / 接続)の引き継ぎ
' ------------------------------------------------------------------------------
Private Function ReplaceShapeWithLatestMaster(ByRef vsoOldShape As Visio.Shape, _
ByRef vsoStencil As Visio.Document, _
ByVal masterNameU As String) As Boolean
On Error GoTo ReplaceError
Dim vsoLatestMaster As Visio.Master
Set vsoLatestMaster = vsoStencil.Masters.ItemU(masterNameU)
' Visio 2013以降の標準API `Shape.Replace` を活用(存在する場合)
' ※位置、接続(Connects)、図面テキスト、一部のShapeDataをネイティブで維持可能
Dim vsoNewShape As Visio.Shape
' 旧シェイプの重要なカスタムデータ(カスタムプロパティ)を退避
Dim dataDictionary As Object
Set dataDictionary = BackupShapeData(vsoOldShape)
' シェイプの置換実行
Set vsoNewShape = vsoOldShape.Replace(vsoLatestMaster, visReplaceFlagsDefault)
' 退避させたデータを新シェイプへリストア(スキーマ変更への対応)
Call RestoreShapeData(vsoNewShape, dataDictionary)
' 明示的解放
Set dataDictionary = Nothing
Set vsoNewShape = Nothing
Set vsoLatestMaster = Nothing
ReplaceShapeWithLatestMaster = True
Exit Function
ReplaceError:
Debug.Print "[ERROR] Replacement Exception: " & Err.Description
ReplaceShapeWithLatestMaster = False
End Function
' ------------------------------------------------------------------------------
' ShapeSheetデータ(Prop.xxx)の退避ヘパー関数の作成
' ------------------------------------------------------------------------------
Private Function BackupShapeData(ByRef vsoShape As Visio.Shape) As Object
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
If vsoShape.SectionExists(visSectionProp, visExistsAnywhere) <> 0 Then
Dim i As Long
Dim rowCount As Long
rowCount = vsoShape.RowCount(visSectionProp)
For i = 0 To rowCount – 1
Dim cellName As String
Dim cellVal As String
cellName = vsoShape.CellsSRC(visSectionProp, i, visCustPropsValue).Name
cellVal = vsoShape.CellsSRC(visSectionProp, i, visCustPropsValue).ResultStr(visNone)
If Not dict.Exists(cellName) Then
dict.Add cellName, cellVal
End If
Next i
End If
Set BackupShapeData = dict
End Function
‘ ——————————————————————————
‘ 新シェイプへのShapeSheetデータ再注入
‘ ——————————————————————————
Private Sub RestoreShapeData(ByRef vsoShape As Visio.Shape, ByRef dict As Object)
If dict Is Nothing Then Exit Sub
Dim key As Variant
For Each key In dict.Keys
‘ 新マスター側にも同名のプロパティ行が存在する場合のみ値を注入
If vsoShape.CellExistsU(CStr(key), visExistsAnywhere) <> 0 Then
vsoShape.CellsU(CStr(key)).FormulaFormulaU = “””” & dict(key) & “”””
End If
Next key
End Sub
—
4. エンタープライズ開発における重要なアーキテクチャ解説
上記コードには、大量の図面を自動処理する大規模バッチやレガシー移行案件で絶対に外してはならないポイントが含まれています。
① `Shape.Replace` メソッドの安全な適用
Visio 2013以降、Visio Object Modelには `Shape.Replace` が追加されました。これにより、手動で旧シェイプの位置(`PinX`, `PinY`)や接続情報(`Connects`)をコードで手動再配線する莫大な工数が不要となりました。
しかし、標準の `Replace` ではマスター定義から削除されたプロパティの不整合やShapeSheet数式の破綻を起こすため、上記の通り `Scripting.Dictionary` を介した「プロパティ明示退避&選択的復元」のガードロジックが必須となります。
② COMオブジェクトの明確なライフサイクル管理
VBAは参照カウント方式のガベージコレクション(GC)です。Visioのような重厚なCOMサーバーをループ処理で何千回も呼び出す場合、ループ内で使用した `Visio.Shape` や `Visio.Master` の変数を `Set obj = Nothing` で明示的に解放しないと、GDIオブジェクトの枯渇によるメモリリークを引き起こし、最終的に `Out of Memory`(実行時エラー 7)で強制終了します。
‘ 必須のメモリ解放パターン
For i = vsoShapes.Count To 1 Step -1
Set currentShape = vsoShapes.Item(i)
‘ — 処理 —
Set currentShape = Nothing ‘ 明示的にCOM参照カウントをデクリメント
Next i
③ パフォーマンスの「絶対防衛線」
Visioの再計算・再描画パイプラインは極めて高負荷です。
以下の3行は、バッチ処理の実行速度を数十倍から数百倍跳ね上げます。
Visio.Application.EventsEnabled = False ‘ イベントカスケードの阻止
Visio.Application.ScreenUpdating = 0 ‘ 画面描画のロック
Visio.Application.DeferRecalc = True ‘ ShapeSheet幾何計算の遅延評価
特に `DeferRecalc = True` は、すべての置換が完了するまで内部の幾何学再計算を保留するため、大規模図面での高速化には絶対の威力を発揮します。
—
5. 結論:アーキテクトが目指すべき運用の未来
Visioを単なる「絵描きツール」として扱うか、「グラフィカルなデータ構造」として掌握するか。その境界線は、この `Master` オブジェクトのライフサイクルと参照モデルの制御力にあります。
今回提示した「多角的一元特定」と「コンテキスト保存型同期」の実装パターンは、単に目の前のコードを動かすだけでなく、将来にわたって保守可能なレガシー資産へとシステムを引き上げるための技術的基盤です。図面内に留まる「動かぬデータ」を、プログラムから正確に特定・制御可能な「活きたデータ」へと昇華させてください。
