【テクニカル・上級編】【上級】Shape.SetFormulasUと配列を用いたバルク更新:数式ハックによる図形間の動的連携 – Visio VBA解析バイブル

スポンサーリンク

【上級】Shape.SetFormulasUと配列を用いたバルク更新:数式ハックによる図形間の動的連携

Visio VBAにおける最大の罪悪感、それは「Shapeオブジェクトのセルへの個別アクセス(`CellsU`経由の値代入)」の多用である。

数千個のシェイプを持つエンタープライズ・アーキテクチャのダイアグラム、あるいはリアルタイムプラント監視のP&ID図において、ループを回して `shp.CellsU(“PinX”).ResultIU = x` のようなコードを書いているエンジニアを見かけるたび、私はエンジニアリングの敗北を感じる。COMの境界を何千回も跨ぎ、その都度Visioの再計算エンジンを走らせる。結果、画面はフリーズし、メモリはリークし、ユーザーはコーヒーを飲みに行く。

プロフェッショナルであれば、`SetFormulasU`メソッドとVariant配列による「バルク更新(一括注入)」を使え。さらに、単なる値の流し込みではなく、「数式(Formula)の動的バインディング」を組み合わせることで、Visioのシート群をExcelの数式エンジンをも凌駕する超高速な動的依存グラフへと昇華させる。

今回は、その極限の知見をコードと理論とともに解き明かす。

—

1. なぜ `CellsU` は悪なのか? —— COM境界と再計算の罠

Visioの内部アーキテクチャにおいて、すべてのシェイプのプロパティ(位置、サイズ、色、カスタムプロパティなど)は「Cell(セル)」と呼ばれる最小単位で管理されている。そして、これらはExcelの数式と同様に、他のセルへの依存関係(Formula)を持っている。

`shp.CellsU(“Width”).FormulaU = “2 in”` のようなコードを実行すると、以下の処理が内部で発生する。
1. VBAランタイムからCOMを介してVisioのネイティブレイヤーへ命令が飛ぶ。
2. 指定されたセルが書き換えられる。
3. Visioが即座に依存関係ツリーを評価し、関連するすべてのセルの再計算(Re-calc)を実行する。

これをループ内で1000回行えば、再計算が1000回発生する。これがパフォーマンス崩壊の根本原因だ。

解決策:トランザクション的バルク更新

`Shape.SetFormulasU` は、複数のシェイプの複数のセルに対して、一回のCOM往復と「一回の再計算トリガー」で数式を一括流し込むための最強の武器である。

—

2. `SetFormulasU` の仕様と配列の構造

`SetFormulasU` メソッドのシグネチャを正確に理解している者は少ない。

object.SetFormulasU(StreamSource, [Flags], [LocaleCode])

ここで最も厄介であり、かつ強力なのが第1引数の `StreamSource` だ。これは以下の2次元配列(または1次元配列)を要求する。

  • 奇数インデックス(あるいは0始まりの偶数行): セルのストリーム(どのシェイプの、どのセルを指定するかを示すIDの配列)
  • 偶数インデックス(あるいは0始まりの奇数行): 流し込む「数式(Formula)」の文字列配列

正確には、Visioのドキュメントに定義されている `GetFormulasU` / `SetFormulasU` のストリーム配列仕様 は以下の通り、「シェイプのインデックス(またはID)の配列」 と 「セルの名前(LocalName/UIVariant/NameU)の配列」 の2つの1次元配列を組み合わせて構築する。

実務上、最も確実なのは、処理対象のシェイプIDの配列、セルの名前の配列、そして数式の2次元配列を完璧に同期させることだ。

—

3. 実践:数式ハックによる図形間ダイナミック連携の実装

百聞は一見に如かず。ここでは、「親シェイプ(Master)」の移動やサイズ変更に、数百個の「子シェイプ(Slave)」がVisioのネイティブ数式機能(Guarded Cells / Guard関数ではない動的数式)によって完全追従するシステムを構築する。

子シェイプ側に個別の座標値をVBAで書き込むのではない。子シェイプの `PinX` や `PinY` に、「親シェイプのIDを参照する数式」を配列で一括注入するのだ。これにより、VBAの手を離れた後も、Visioエンジンが自律的に図形間の動的連携を維持する。

以下のプロダクション品質のコードを参照せよ。

Option Explicit

Public Sub ExecuteBulkFormulaBinding()
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage

‘ 1. パフォーマンス・オプティマイゼーションの極意
‘ 画面描画、イベント発火、自動再計算を完全に停止し、メモリとCPUを解放する
Application.ScreenUpdating = False
Application.EventEnabled = False
vsoPage.Document.ComputationSuspended = True

On Error GoTo ErrorHandler

‘ — モックデータの準備 —
‘ 親シェイプを1つ作成
Dim vsoMasterShape As Visio.Shape
Set vsoMasterShape = vsoPage.DrawRectangle(1, 5, 3, 4)
vsoMasterShape.Text = “Parent_Hub”
Dim masterID As Long
masterID = vsoMasterShape.ID

‘ 子シェイプを100個生成(初期位置は適当)
Dim i As Long
Dim totalNodes As Long
totalNodes = 100

Dim shpArray() As Visio.Shape
ReDim shpArray(1 To totalNodes)

Dim sIDs() As Integer
Dim sNames() As String
Dim sFormulas() As String

‘ SetFormulasU 用の配列サイズを定義
‘ 1つのシェイプにつき PinX と PinY の2つのセルを更新する場合
‘ Stream配列は (1 to 2 totalNodes) の1次元配列、またはそれに準ずる構造が必要。
‘ ※Visioの仕様:Stream配列は [ShapeID_1, CellName_1, ShapeID_2, CellName_2, …] の形式。

ReDim sStream(1 To totalNodes 2) As Variant
ReDim sFormulas(1 To totalNodes 2) As Variant

Dim offsetX As Double, offsetY As Double

For i = 1 To totalNodes
Set shpArray(i) = vsoPage.DrawOval(0, 0, 0.5, 0.5)
shpArray(i).Text = “Node_” & i

‘ 座標計算のオフセット(例として円形に配置する数式を組む)
‘ 子シェイプのPinX数式: = Sheet.!PinX + COS(i angle) radius
‘ 子シェイプのPinY数式: = Sheet.!PinY + SIN(i angle) radius
Dim angle As Double
angle = (i / totalNodes) 2 3.14159265358979

‘ 奇数スロット: PinX
sStream((i – 1) 2 + 1) = shpArray(i).ID
sStream((i – 1) 2 + 2) = “PinX”

‘ 偶数スロット: PinY
sStream((i – 1) 2 + 1 + totalNodes 2) = shpArray(i).ID ‘ ※実際にはインデックス管理を統一する
Next i

‘ — より洗練された SetFormulasU の実装パターン —
‘ Visioの GetFormulasU / SetFormulasU は、
‘ SheetIDArray() と CellNameArray() の2つの1次元配列を受け取るオーバーロードが最も安定する。

Dim sheetIDs() As Long
Dim cellNames() As String
Dim formulas() As String

Dim arraySize As Long
arraySize = totalNodes 2 ‘ 各シェイプに PinX と PinY

ReDim sheetIDs(0 To arraySize – 1)
ReDim cellNames(0 To arraySize – 1)
ReDim formulas(0 To arraySize – 1)

For i = 1 To totalNodes
Dim idxPinX As Long
Dim idxPinY As Long
idxPinX = (i – 1) 2
idxPinY = (i – 1) 2 + 1

‘ — PinX の設定 —
sheetIDs(idxPinX) = shpArray(i).ID
cellNames(idxPinX) = “PinX”
‘ 【数式ハック】親シェイプのIDを動的に参照するUniversal Formulaを構築
‘ Sheet.!PinX というVisioのセブレファレンス構文を使用
formulas(idxPinX) = “Sheet.” & masterID & “!PinX + ” & (0.5 Cos(i 0.0628))

‘ — PinY の設定 —
sheetIDs(idxPinY) = shpArray(i).ID
cellNames(idxPinY) = “PinY”
formulas(idxPinY) = “Sheet.” & masterID & “!PinY + ” & (0.5 Sin(i 0.0628))
Next i

‘ ★ ここが極限のハイライト:一回の呼び出しで数式を全注入
Dim successCount As Long
successCount = vsoPage.SetFormulasU(sheetIDs, cellNames, formulas)

Debug.Print “Successfully updated formulas for ” & successCount & ” cells.”

ErrorHandler:
If Err.Number <> 0 Then
MsgBox “Error encountered: ” & Err.Description, vbCritical
End If

‘ 2. 必ず環境を復元する(これを怠るとVisioが不安定になる)
vsoPage.Document.ComputationSuspended = False
Application.EventEnabled = True
Application.ScreenUpdating = True

‘ オブジェクトの明示的解放(メモリ管理の徹底)
Set vsoPage = Nothing
Set vsoMasterShape = Nothing
End Sub

—

4. チーフアーキテクトが教える「現場の知見」とアンチパターン

このアーキテクチャを現場に投入する際、シニアエンジニアが直面する罠と、それを回避するための極意を共有する。

① 単位系(Unit)の罠:`Formula` vs `FormulaU`

文字列として数式を流し込む際、`Formula` を使うとローカル言語(日本語版なら `幅` や `高さ`、あるいは `Sheet.1!引数` の日本語表記など)に依存してしまう。
必ず `SetFormulasU` および `FormulaU`(Universal Formula) を使用せよ。プロパティ名や関数名は常に英語(例: `PinX`, `Width`, `COS`, `GUARD`)で記述しなければ、国際環境や将来のバージョンアップでシステムが崩壊する。

② 計算一時停止 (`ComputationSuspended`) の絶対厳守

大量の数式を注入する際、Visioは数式間の循環参照や依存関係の解決をその都度行おうとする。
`Document.ComputationSuspended = True` を宣言することで、再計算エンジンを完全にロックし、すべての数式注入が完了した瞬間に一度だけツリー全体を再評価させることができる。これにより、処理時間が数分から数十ミリ秒へと劇的に短縮される。

③ メモリリークの防止とCOMの解放

VBAはガベージコレクションを持たない。特にVisioのシェイプコレクションやページオブジェクトをループ内で参照し続けると、COMの参照カウンタがインクリメントされたままメモリ上に残る。
処理の最後には必ず `Set obj = Nothing` を明示的に行い、VBAのイミディエイトウィンドウやバックグラウンドプロセスにゴーストプロセスを残さないようにコードを設計すること。

—

結び

VBAは、もはや過去の遺物などではない。APIとオブジェクトモデルの深淵を理解したプロフェッショナルが操れば、最新のモダン言語で書かれた重厚なアプリケーションをも凌駕する、圧倒的な爆速自動化エンジンへと生まれ変わる。

個別のセルを愚直に叩くコードは今すぐ捨て去り、`SetFormulasU` と配列による数式ハックで、図形たちが自律的に駆動する真のダイナミック・システムを構築せよ。

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