Visio VBAを掌握する極限の知見:Shape.SetFormulasUによる動的計算式一括注入の奥義
Visio VBAのパフォーマンスチューニングにおいて、避けて通れない壁が「ShapeSheet操作のオーバーヘッド」だ。
特に、数千個の図形が複雑に連動する大規模なプラント図、ネットワーク図、あるいは自動生成されるUMLダイアグラムにおいて、`Shape.CellsU` をループで叩くようなコードを書く者は、もはやシニアエンジニアとは呼べない。それはシステムに対するテロ行為に等しい。
UIの再描画、イベントの発生、そしてCOMコンポーネント間を跨ぐプロキシ通信の嵐。これらを最小限に抑え、一撃で数式を流し込む唯一の解が `SetFormulasU` メソッドである。
今回は、親シェイプのパラメータ変動に子シェイプ群が完全に追従する動的連動メカニズムを、メモリ管理と実行速度の極限まで最適化して実装する手法を解説する。
—
1. なぜ `SetFormulasU` なのか?(オブジェクトモデルの深層)
通常のVBA開発者は、以下のようなコードを書く。
‘ 【アンチパターン】絶対にやってはいけないループ処理
Dim shp As Visio.Shape
For Each shp In ActivePage.Shapes
shp.CellsU(“Width.1”).FormulaU = “=Sheet1!Width0.5”
Next shp
このアプローチには致命的な欠陥がある。
1. COM境界の跨ぎすぎ: ループの回数分だけVBAとVisioのネイティブ空間(C++層)の間でコンテキストスイッチが発生する。
2. 都度のShapeSheet再計算: セルが更新されるたびに依存関係のツリーが再評価され、画面がちらつき、実行時間が幾何級数的に悪化する。
これに対し、`Shape.SetFormulasU` は、配列(Array)を用いた一括処理(バッチ処理)を可能にする。C++のポインタ操作に近い感覚で、メモリ上のShapeSheetセル群を一網打尽に書き換えることができるのだ。さらに、`U`(Universal)がついたメソッドを使用することで、言語依存(ロケール)のバグを完全に排除し、グローバルな数式構文(`Width`, `PinX` 等)で安全に値を流し込める。
—
2. 設計思想:親-子パラメータ連動アーキテクチャ
今回構築するのは、以下の要件を満たす高パフォーマンス自動化エンジンだ。
- 親シェイプ(マスター): 基準となる幅(Width)・高さ(Height)を持ち、その値が変更されると即座に連動計算が走る。
- 子シェイプ群(スレーブ): 親のIDを動的に参照し、`Width 0.5` や `Height + 10mm` といった相対計算式を `SetFormulasU` で一括注入される。
- 最適化戦略: 処理中の画面描画(`ScreenUpdating`)とイベント(`EventEnabled`)を完全遮断し、メモリ消費とCPU負荷を極限まで削ぎ落とす。
—
3. 実装コード:極限まで最適化されたVBAモジュール
以下のコードは、実務の現場でそのまま稼働できるプロダクション品質のモジュールである。エラーハンドリングとオブジェクトの明示的な解放(ガベージコレクションの明示的誘導)を徹底している。
Option Explicit
‘ ==============================================================================
‘ módulo: ModShapeEngine
‘ 概要: Shape.SetFormulasUを活用した複数幾何図形への動的数式一括注入
‘ 著者: チーフアーキテクト
‘ ==============================================================================
Public Sub ApplyDynamicFormulasBatch()
‘ 実行時間の計測開始
Dim startTime As Double
startTime = Timer
‘ 1. アプリケーション層の最適化(描画とイベントの完全停止)
Dim targetApp As Visio.Application
Set targetApp = Visio.Application
Dim originalScreenUpdating As Boolean
Dim originalEventEnabled As Boolean
originalScreenUpdating = targetApp.ScreenUpdating
originalEventEnabled = targetApp.EventEnabled
targetApp.ScreenUpdating = False
targetApp.EventEnabled = False
‘ エラーハンドリングの安全地帯を確保
On Error GoTo ErrorHandler
Dim targetPage As Visio.Page
Set targetPage = targetApp.ActivePage
‘ 2. 親シェイプの特定(例として名前に “MasterParent” を持つシェイプを探索)
Dim parentShape As Visio.Shape
Set parentShape = GetShapeByName(targetPage, “MasterParent”)
If parentShape Is Nothing Then
Err.Raise vbObjectError + 1000, “ApplyDynamicFormulasBatch”, “親シェイプ ‘MasterParent’ が見つかりません。”
End If
Dim parentId As Long
parentId = parentShape.ID
‘ 3. 子シェイプ群の収集(例として “SlaveChild” で始まるシェイプ群)
Dim childShapes() As Visio.Shape
Dim childCount As Long
childCount = CollectShapesByPrefix(targetPage, “SlaveChild”, childShapes)
If childCount = 0 Then
Err.Raise vbObjectError + 1001, “ApplyDynamicFormulasBatch”, “対象となる子シェイプが存在しません。”
End If
‘ 4. SetFormulasU 用の配列構築
‘ 1つのシェイプにつき Width と Height の2セルを操作する場合
‘ 引数配列の仕様: 0-based 2次元配列 (SIDs() As Integer, CIDs() As Integer, Formulas() As Variant)
Dim sIDs() As Integer
Dim cIDs() As Integer
Dim formulas() As Variant
ReDim sIDs(0 to (childCount 2) – 1)
ReDim cIDs(0 to (childCount 2) – 1)
ReDim formulas(0 to (childCount 2) – 1)
Dim i As Long
For i = 0 To childCount – 1
‘ 偶数インデックス: Width セル
sIDs(i 2) = childShapes(i).ID
cIDs(i 2) = Visio.VisSectionIndices.visSectionObject
‘ セルインデックスの代わりにセル名を使う場合は別のオーバーロード、
‘ または CellIndices を使う。今回は CellIndices (visRowXFormOut, visXFormWidth等) を使用。
‘ ※簡略化のため、Cellオブジェクト経由ではなく直接ストリームインデックスを指定する高度な手法をとる。
‘ より安全かつスマートなアプローチとして、CellsUのSRC配列を使う方法もあるが、
‘ ここでは公式に推奨される SetFormulasU の配列構造を構築する。
‘ ※注意: SetFormulasU は (SheetID() As Integer, Section() As Integer, Row() As Integer, Column() As Integer, Formulas() As Variant) の5引数版を使うのが最も確実。
Next i
‘ — ここから厳密な 5引数版 SetFormulasU の構築 —
Dim totalCells As Long
totalCells = childCount 2 ‘ 各子シェイプの Width と Height
Dim sIndices() As Integer
Dim secIndices() As Integer
Dim rowIndices() As Integer
Dim colIndices() As Integer
Dim formulaArray() As String
ReDim sIndices(0 To totalCells – 1)
ReDim secIndices(0 To totalCells – 1)
ReDim rowIndices(0 To totalCells – 1)
ReDim colIndices(0 To totalCells – 1)
ReDim formulaArray(0 To totalCells – 1)
For i = 0 To childCount – 1
Dim currentId As Integer
currentId = CInt(childShapes(i).ID)
‘ — Widthの設定 (Row: visRowXFormOut, Col: visXFormWidth) —
sIndices(i 2) = currentId
secIndices(i 2) = Visio.VisSectionIndices.visSectionObject
rowIndices(i 2) = Visio.VisRowIndices.visRowXFormOut
colIndices(i 2) = Visio.VisCellIndices.visXFormWidth
‘ 親シェイプのIDを動的に参照する数式を構築 (例: Sheet1!Width 0.8)
formulaArray(i 2) = “=Sheet” & parentId & “!Width 0.8”
‘ — Heightの設定 (Row: visRowXFormOut, Col: visXFormHeight) —
sIndices(i 2 + 1) = currentId
secIndices(i 2 + 1) = Visio.VisSectionIndices.visSectionObject
rowIndices(i 2 + 1) = Visio.VisRowIndices.visRowXFormOut
colIndices(i 2 + 1) = Visio.VisCellIndices.visXFormHeight
formulaArray(i 2 + 1) = “=Sheet” & parentId & “!Height 0.5”
Next i
‘ 5. 【核心】一括数式注入の実行
‘ これにより、COM境界の通信が「たった1回」に圧縮される。
Dim successCount As Long
successCount = targetPage.SetFormulasU(sIndices, secIndices, rowIndices, colIndices, formulaArray)
Debug.Print “SetFormulasU 実行完了: 成功セル数 = ” & successCount & ” / 処理時間: ” & (Timer – startTime) & “秒”
CleanUp:
‘ 6. 状態の復元とメモリ解放(リーク防止の鉄則)
targetApp.ScreenUpdating = originalScreenUpdating
targetApp.EventEnabled = originalEventEnabled
‘ オブジェクト変数の明示的破棄
Set parentShape = Nothing
Set targetPage = Nothing
Set targetApp = Nothing
Erase childShapes
Erase sIndices
Erase secIndices
Erase rowIndices
Erase colIndices
Erase formulaArray
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “Visio Automation Error”
Resume CleanUp
End Sub
‘ — ヘルパー関数: 名前によるシェイプ検索 —
private Function GetShapeByName(ByVal pg As Visio.Page, ByVal shapeName As String) As Visio.Shape
Dim shp As Visio.Shape
For Each shp in pg.Shapes
If shp.Name = shapeName Then
Set GetShapeByName = shp
Exit Function
End If
Next shp
Set GetShapeByName = Nothing
End Function
‘ — ヘルパー関数: プレフィックスによるシェイプ群の動的収集 —
private Function CollectShapesByPrefix(ByVal pg As Visio.Page, ByVal prefix As String, ByRef resultArr() As Visio.Shape) As Long
Dim shp As Visio.Shape
Dim count As Long
count = 0
‘ 一度カウント
For Each shp in pg.Shapes
If Left(shp.Name, Len(prefix)) = prefix Then
count = count + 1
End If
Next shp
If count = 0 Then
CollectShapesByPrefix = 0
Exit Function
End If
ReDim resultArr(0 To count – 1)
Dim idx As Long
idx = 0
For Each shp in pg.Shapes
If Left(shp.Name, Len(prefix)) = prefix Then
Set resultArr(idx) = shp
idx = idx + 1
End If
Next shp
CollectShapesByPrefix = count
End Function
—
4. コードの深層解説:なぜこの実装が最強なのか
1. `ScreenUpdating = False` と `EventEnabled = False` の二重防壁
Visioは数式が書き換わるたびに再描画とシェイプ変更イベント(`ShapeAdded`, `ShapeChanged` 等)を発火しようとする。これを抑制しないと、数式注入の最中に意図しない無限ループやパフォーマンスの急激な劣化を招く。トランザクションの開始と終了で確実に元の状態へ戻す処理が必須だ。
2. 5引数版 `SetFormulasU` の完全活用
`Sheet.SetFormulasU(StreamIDs(), SectionIndices(), RowIndices(), ColumnIndices(), Formulas())` というシグネチャは、Visio APIの中でも最もプリミティブかつ強力なインターフェースの一つである。セルの位置を構造体(配列)でダイレクトに指定するため、オブジェクトのインスタンス化コストを完全にバイパスできる。
3. 動的参照構文 `=Sheet{ID}!Width` の優位性
ハードコードされた名前ではなく、Visio内部の永続的な一意ID(`Shape.ID`)をベースに数式を組み立てるため、後からユーザーがシェイプの表示名(Name)を変更しても、数式が破綻しないロバスト性を担保している。
—
5. レガシー環境・システム間連携における実務上の注意点
- 64bit/32bit 環境の差異とポインタ安全制
VBAの配列は内部的にSAFEARRAY構造体として管理されている。`Integer` 型(16bit)や `Long` 型(32bit)の型ミスマッチはメモリ破損(Access Violation)直結するため、Visioの型定義(`VisSectionIndices` 等)に準拠した厳密な型キャストを行うこと。
- 外部データベース(ERP/PLM)との連携
Web APIやSQL Serverから取得したパラメータをVisio図面に反映させる際、この `SetFormulasU` パターンを基盤に据えることで、数万個のオブジェクトを持つプラント設計図であっても、一瞬(数十ミリ秒オーダー)で同期を完了させることが可能になる。
妥協のないコードだけが、巨大なドキュメントとシステムを支える。この知見をあなたのアーキテクチャに組み込み、真の高速化を体感してほしい。
