Visio VBAを掌握する極限の知見:複数ドキュメント間におけるShape同期・リンクのアーキテクチャ
Visio VBAにおける真の地獄は、単一ドキュメント内の自動化ではない。それは、複数の`Document`オブジェクト、複数の`Window`インスタンスがメモリ上で複雑に絡み合う、マルチドキュメント環境の制御にある。
マスター図面から各拠点用図面へ、数千個に及ぶシェイプのマスター定義、カスタムプロップ(ShapeData)、そしてスタイル定義を寸分違わず同期させる――。生半可なコードを書けば、メモリリーク、`COMException`の嵐、あるいは「ゾンビプロセス」と化したVisioがタスクマネージャーに居座ることになる。
本稿では、レガシーからモダンまでVBAの最前線を戦い抜いてきたアーキテクトの視点から、複数Visioドキュメント間でのShapeオブジェクトの安全かつ高速なコピー&リンク、そしてメモリ最適化の極限を解説する。
—
1. Visioオブジェクトモデルの暗部:マルチドキュメント制御の鉄則
初心者は`Application.Documents.Open`を安易に呼び、グローバルな`ActiveDocument`に依存したコードを書く。これが大規模自動化の現場では致命傷となる。
複数のVisioインスタンスやドキュメントを扱う際の鉄則は以下の3点だ。
1. 暗黙的な参照(ActiveX/COMのコンテキスト)を排除する
`ActiveWindow`や`ActivePage`への依存は、ユーザーがウィンドウを切り替えた瞬間にコードを崩壊させる。すべてのオブジェクト変数を明示的にスコープに閉じ込めよ。
2. ドキュメント間のコピーにおける「GUID」と「マスター」の衝突回避
単純な`Shape.Copy`と`Paste`は、クリップボードという脆弱な共有資源を介する。これはマルチスレッド的・非同期的な割り込みに弱く、パフォーマンスも最悪だ。
3. オブジェクトの明示的解放(Garbage Collectionの罠)
VBAのランタイムはCOM参照の解放タイミングが曖昧である。特に複数ドキュメントを横断する場合、参照カウントが残ったままドキュメントを閉じると、メモリリークまたはVisioの強制終了を引き起こす。
—
2. 実践アーキテクチャ:マスター図面から拠点図面へのShape同期エンジン
以下のコードは、マスター図面(テンプレート)から特定のカスタムプロパティを持つシェイプを抽出し、ターゲットとなる複数ドキュメントへ「単なるコピー」ではなく「データ同期リンク」を保持した状態でインポートする実用的なプロシージャだ。
クリップボードを汚染しない `Drop` メソッドと、マスター図形の安全な共有(Master Injection)を実装している。
Option Explicit
‘ ==============================================================================
‘ 処理名: SyncMasterShapesToTarget
‘ 概要 : マスター図面から指定シェイプをターゲットドキュメント群へ安全に同期する
‘ ==============================================================================
Sub SyncMasterShapesToTarget()
Dim appVisio As Visio.Application
Set appVisio = Visio.Application
‘ 画面描画とアラートを完全抑制(パフォーマンス最大化の基本)
appVisio.ScreenUpdating = False
appVisio.EventsEnabled = False
appVisio.AlertsEnabled = False
On Error GoTo ErrorHandler
Dim srcDoc As Visio.Document
Dim targetDoc As Visio.Document
Dim targetPaths() As String
Dim i As Long
‘ 1. マスター図面の特定(ここでは開いているドキュメントのインデックス指定または名前指定)
‘ 実運用ではファイルパスからサイレントオープンすることを推奨
Set srcDoc = GetDocumentByUIName(appVisio, “Master_Core_202X.vsdm”)
If srcDoc Is Nothing Then
Err.Raise vbObjectError + 1000, “SyncEngine”, “マスター図面が見つかりません。”
End If
‘ 2. 同期先ファイルパスのリストアップ(例としてハードコーディング。実際はINIやDBから取得)
targetPaths = Split(“C:\VisioData\Branch_A.vsdm,C:\VisioData\Branch_B.vsdm”, “,”)
‘ 3. ターゲットドキュメントをループ処理
For i = LBound(targetPaths) To UBound(targetPaths)
If Trim(targetPaths(i)) <> “” Then
Set targetDoc = appVisio.Documents.OpenEx(Trim(targetPaths(i)), visOpenRO + visOpenHidden)
‘ 核心ロジック:マスターおよびシェイプの同期実行
Call SynchronizeDocument(srcDoc, targetDoc)
‘ 変更を保存して閉じる
targetDoc.Save
targetDoc.Close
Set targetDoc = Nothing
End If
End For
CleanUp:
‘ 状態の復元
appVisio.ScreenUpdating = True
appVisio.EventsEnabled = True
appVisio.AlertsEnabled = True
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “Visio Automation Engine”
‘ 異常終了時も確実にオブジェクトを解放
If Not targetDoc Is Nothing Then
targetDoc.Close visSaveNo
Set targetDoc = Nothing
End If
Resume CleanUp
End Sub
‘ ==============================================================================
‘ 内部関数: ドキュメント間の同期処理実体
‘ ==============================================================================
Private Sub SynchronizeDocument(ByVal srcDoc As Visio.Document, ByRef targetDoc As Visio.Document)
Dim srcPage As Visio.Page
Dim targetPage As Visio.Page
Dim shpSource As Visio.Shape
Dim shpTarget As Visio.Shape
Dim mstrSource As Visio.Master
Dim mstrTarget As Visio.Master
Set srcPage = srcDoc.Pages(1)
‘ ターゲット側の描画先ページ(存在メンテ)
If targetDoc.Pages.Count = 0 Then
Set targetPage = targetDoc.Pages.Add()
Else
Set targetPage = targetDoc.Pages(1)
End If
‘ マスターシェイプ(Master)の同期
For Each mstrSource in srcDoc.Masters
On Error Resume Next
Set mstrTarget = targetDoc.Masters(mstrSource.NameU)
On Error GoTo 0
If mstrTarget Is Nothing Then
‘ ターゲットにマスターが存在しない場合はドキュメント間でマスターをコピー
mstrSource.Open.Drop targetPage, 0, 0 ‘ 依存関係解決のためのプレースホルダドロップ
‘ ※実運用では Document.DropCopyMaster を活用するのがエレガント
End If
Next mstrSource
‘ ページ上の個別シェイプの同期(IDまたはカスタムプロパティ “AssetID” でマッピング)
For Each shpSource In srcPage.Shapes
If shpSource.CellExists(“Prop.AssetID”, visSectionProp) Then
Dim assetID As String
assetID = shpSource.Cells(“Prop.AssetID”).ResultStr(“”)
Set shpTarget = FindShapeByAssetID(targetPage, assetID)
If shpTarget Is Nothing Then
‘ 新規追加:マスターからドロップ、またはシェイプ自体のクローン
If Not shpSource.Master Is Nothing Then
‘ マスター経由で生成
Set mstrTarget = targetDoc.Masters(shpSource.Master.NameU)
If Not mstrTarget Is Nothing Then
Set shpTarget = targetPage.Drop(mstrTarget, shpSource.Cells(“PinX”).ResultIU, shpSource.Cells(“PinY”).ResultIU)
End If
Else
‘ マスターなしシェイプの直接複製(Safe Copy via Clipboard代替)
‘ 注意: 大量処理ではコストが高いため極力マスター運用を推奨
shpSource.Copy
Set shpTarget = targetPage.Drop(targetDoc.Masters(1), 0, 0) ‘ ダミー経由の貼付等
‘ ※実務では BinaryStream や Shape.SpatialNeighbors による精密配置を推奨
End If
End If
‘ プロパティの値を強制同期
If Not shpTarget Is Nothing Then
Call PropagateShapeData(shpSource, shpTarget)
End If
End If
Next shpSource
End Sub
‘ ==============================================================================
‘ 補助関数群
‘ ==============================================================================
Private Function GetDocumentByUIName(ByVal app As Visio.Application, ByVal docName As String) As Visio.Document
Dim doc As Visio.Document
For Each doc in app.Documents
If doc.Name = docName Then
Set GetDocumentByUIName = doc
Exit Function
End If
Next doc
Set GetDocumentByUIName = Nothing
End Function
Private Function FindShapeByAssetID(ByVal pg As Visio.Page, ByVal id As String) As Visio.Shape
Dim shp As Visio.Shape
For Each shp in pg.Shapes
If shp.CellExists(“Prop.AssetID”, visSectionProp) Then
If shp.Cells(“Prop.AssetID”).ResultStr(“”) = id Then
Set FindShapeByAssetID = shp
Exit Function
End If
End If
Next shp
Set FindShapeByAssetID = Nothing
End Function
Private Sub PropagateShapeData(ByVal src As Visio.Shape, ByRef tgt As Visio.Shape)
‘ セルの数値を完全同期(位置やサイズは維持しつつカスタムプロパティを同期する例)
On Error Resume Next
Dim i As Long
Dim propSec As Integer
propSec = visSectionProp
If src.RowCount(propSec) > 0 Then
Dim r As Long
For r = 0 To src.RowCount(propSec) – 1
Dim rowName As String
rowName = src.RowName[propSec, r]
‘ 値の同期
If src.CellExists(rowName & “.Value”, propSec) And tgt.CellExists(rowName & “.Value”, propSec) Then
tgt.CellsU(rowName & “.Value”).FormulaU = src.CellsU(rowName & “.Value”).FormulaU
End If
Next r
End If
On Error GoTo 0
End Sub
—
3. レガシー環境の保守とメモリリークの完全駆逐
社内システムにおいて、Visio VBAはしばしば「野良マクロ」として放置され、メモリリークの温床となる。これを防ぐためのチーフアーキテクトとしての処方箋を記す。
オブジェクトの明示的「破棄(=Nothing)」の作法
VBAの参照型変数は、プロシージャを抜ければ自動解放されると誤解されている。しかし、複数のドキュメントやApplicationオブジェクトをまたぐ循環参照が存在する場合、参照カウントは0にならず、プロセスが残留する。
- ループ内でのオブジェクト生成は絶対に避ける。
‘ 悪手:ループ内で毎回インスタンスや参照を変える
For i = 1 to 10000
Set shp = page.Shapes(i)
‘ 処理
Next i
このコードは長時間の実行でVBAのヒープ領域を断片化させる。
- 巨大なドキュメントを処理した後は、明示的にメモリの強制回収を意識する。
Set shpSource = Nothing
Set shpTarget = Nothing
Set srcPage = Nothing
Set targetPage = Nothing
‘ ガベージコレクションを促すためにコンテキストを切断
—
4. エンタープライズシステム連携への展望(VBAからCOM Interop/C#への移行判断)
もし、あなたが管理しているこのVisio連携プロセスが「一日に数万シェイプを処理する」「外部のOracle/SQL Serverデータベースとリアルタイムにスキーマ同期を行う」という要件に直面しているなら、VBAの限界を直視するべき時だ。
VBAはプロトタイピングと局所的な自動化においては最強の言語である。しかし、エラーハンドリングの限界、非同期処理の欠如、そしてマルチスレッド非対応という構造的欠陥を持つ。
その次のステップとして、C# (.NET 8 / .NET Framework 4.8) によるCOM Interop(VSTO / 外付Console App) へのリファクタリングを強く推奨する。
// C# (COM Interop) による堅牢なVisioマルチドキュメント操作の概念
using Visio = Microsoft.Office.Interop.Visio;
public class VisioSyncEngine {
public static void ExecuteSync(string masterPath, string targetPath) {
Visio.Application app = new Visio.Application();
app.ScreenUpdating = false;
try {
Visio.Document srcDoc = app.Documents.Open(masterPath);
Visio.Document tgtDoc = app.Documents.OpenEx(targetPath,
(short)Visio.VisOpenSaveArgs.visOpenRO);
// 厳密なLINQによるシェイプ検索とメモリ管理
// C#であればIDisposableとusing文による確実なCOMオブジェクト解放が可能
tgtDoc.Save();
tgtDoc.Close(Visio.VisSaveOptions.visSaveNo);
srcDoc.Close(Visio.VisSaveOptions.visSaveNo);
} finally {
app.ScreenUpdating = true;
app.Quit();
// Marshal.ReleaseComObject による完全な参照カウント解放
}
}
}
—
結言
Visio VBAにおける複数ドキュメント間でのオブジェクト同期は、単なる「コピペの自動化」ではない。それは、Visioのドキュメント構造、マスターの依存関係、そしてCOMのライフサイクルを完全に掌握した者にしか許されない領域である。
本稿で示した設計思想とコードベースを武器に、あなたの組織のレガシーな図面管理プロセスを、鉄壁の自動化パイプラインへと昇華させてほしい。
