Visio VBAを掌握する極限の知見:Shape.SetFormulasUと配列による超高速バルク更新
Visioの図面自動化において、数百、数千のシェイプを扱うシステムを構築した経験がある者なら、誰もが一度は「遅さ」という壁に直面する。
ネットワーク機器のトポロジー図、プラントの配管図、大規模な組織図など、動的にシェイプのプロパティや座標、カスタムプロパティ(シェイプデータ)を更新する処理において、ループ内で愚直に `.CellsU` を叩いていないだろうか?
‘ 【アンチパターン】絶対にやってはいけないループ内での個別更新
Dim shp As Visio.Shape
For Each shp In ActivePage.Shapes
‘ 1回ごとにCOM境界を跨ぎ、描画更新と再計算が発生する最悪のコード
shp.CellsU(“Prop.DeviceID”).FormulaU = “””SRV-01″””
shp.CellsU(”Width”).FormulaU = “2 इंच” ‘ 冗談だ、実際はインチやセンチ
Next shp
このアプローチは、小規模な図面であれば問題にならない。しかし、シェイプ数が500を超えたあたりから、画面がフリーズしたかのような重さに変わり、1000を超えればコーヒーを淹れる時間どころか、ランチに出られるほどの無駄な時間を生み出す。
原因は明確だ。VBAからVisioのCOMオブジェクトへのアクセス(COM境界の跨ぎ)と、プロパティ変更の都度走るVisioエンジン内部の再計算および画面再描画(レイアウト伝播)のオーバーヘッドにある。
今回は、このボトルネックを粉砕し、処理時間を1/10以下、場合によっては1/50へと劇的に圧縮する`SetFormulasU`メソッドと配列を用いたバルク更新(一括処理)の極限の知見を公開する。
—
1. Visioオブジェクトモデルの暗部:なぜ個別更新は遅いのか?
Visioの内部アーキテクチャにおいて、すべてのシェイプのセル(Formula)はスプレッドシートのような依存関係ツリーを持っている。
1つのセルの値を書き換えると、Visioは即座に以下の処理を実行する。
1. COMインターフェースの呼び出しコスト(VBA ⇄ Visioプロセス間通信)
2. 依存関係の評価(他のセルがこの値に依存していないかのチェック)
3. ジオメトリの再計算
4. UIの無効化(Invalidate)と再描画
これをループ内で数千回繰り返すのは、高速道路の料金所で1台ごとに小銭を手渡ししているようなものだ。
我々が目指すべきは、データをメモリ上で配列として完全に構築し、「1回のAPI呼び出し(トランザクション)」でVisioエンジンに流し込むことである。
—
2. 極限最適化の切り札:`SetFormulasU` とは
`Shape` オブジェクト(あるいは `Page` オブジェクト)には、複数のセルの数式を一次元配列で一括設定する `SetFormulasU` メソッドが用意されている。
object.SetFormulasU(StreamSource, Flags, [LocaleName])
このメソッドを使いこなすためには、以下の3つの配列(または二次元配列)をVBA側で完璧に準備する必要がある。
1. シートインデックスの配列(またはシェイプの配列):どのシェイプを対象にするか
2. セルインデックス(またはセルのローカル名/ユニバーサル名)の配列:どのセルを変更するか
3. 設定する数式(Formula)の配列:どんな値を代入するか
公式ドキュメントは難解だが、要するに「[シェイプの配列, セル名の配列, 数式文字列の配列] を同期させて一気に投げろ」ということだ。
—
3. 実装コード:数百個のシェイプデータを一瞬で書き換える
以下に、実業務でそのまま使える堅牢かつ超高速なバルク更新のテンプレートコードを示す。
ここでは、ページ内のすべての特定シェイプに対して、複数のシェイプデータ(Prop)を一括で流し込むシナリオを想定している。
Option Explicit
Public Sub ExecuteBulkUpdateOptimal()
Dim startTime As Double
startTime = Timer
‘ 画面描画と自動計算を停止(極限のパフォーマンスを引き出すための鉄則)
Application.ScreenUpdating = False
Application.AutoRecover = False
On Error GoTo ErrorHandler
Dim pag As Visio.Page
Set pag = ActivePage
Dim shps As Visio.Shapes
Set shps = pag.Shapes
Dim targetCount As Long
targetCount = shps.Count
If targetCount = 0 Then GoTo Cleanup
‘ —————————————————————–
配列の準備(VB_U / Universal名を使用すること。日本語ロケール依存を排除)
‘ —————————————————————–
‘ 今回は各シェイプの Width, Height と Prop.Status を一括変更すると仮定
‘ シェイプ数 変更プロパティ数分のサイズで配列を確保
Dim numProps As Long
numProps = 3 ‘ 変更するセルの数 (Width, Height, Prop.Status)
Dim totalCells As Long
totalCells = targetCount numProps
Dim sourceShapeIDs() As Integer ‘ または Long
Dim sourceStreamNames() As String
Dim sourceFormulas() As String
ReDim sourceShapeIDs(0 To totalCells – 1)
ReDim sourceStreamNames(0 To totalCells – 1)
ReDim sourceFormulas(0 To totalCells – 1)
Dim i As Long, idx As Long
idx = 0
For i = 1 To targetCount
Dim shp As Visio.Shape
Set shp = shps(i)
‘ 処理対象のシェイプIDを格納(同じシェイプに複数セルを設定する場合はIDを重複させる)
‘ セル1: Width
sourceShapeIDs(idx) = shp.ID
sourceStreamNames(idx) = “Width”
sourceFormulas(idx) = “3.5 in” ‘ サンプル値
idx = idx + 1
‘ セル2: Height
sourceShapeIDs(idx) = shp.ID
sourceStreamNames(idx) = “Height”
sourceFormulas(idx) = “2.0 in” ‘ サンプル値
idx = idx + 1
‘ セル3: Custom Property (Prop.Status)
sourceShapeIDs(idx) = shp.ID
sourceStreamNames(idx) = “Prop.Status”
sourceFormulas(idx) = “””Active””” ‘ 文字列はダブルクォーテーションでエスケープ
idx = idx + 1
Next i
‘ —————————————————————–
‘ 伝説のAPI呼出:Page.SetFormulasU によるバルク実行
‘ —————————————————————–
Dim flags As Integer
flags = visReflectIndices ‘ または visFixNamesDbl
‘ 一撃で全シェイプの全セルを書き換える
pag.SetFormulasU sourceShapeIDs, sourceStreamNames, sourceFormulas, flags
Cleanup:
‘ 環境を復元
Application.ScreenUpdating = True
Application.AutoRecover = True
MsgBox “バルク更新完了 処理時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, vbInformation
Exit Sub
ErrorHandler:
‘ 異常終了時も必ず画面描画フラグを戻すこと(さもないとVisioが固まったままになる)
Application.ScreenUpdating = True
Application.AutoRecover = True
MsgBox “エラー発生: ” & Err.Description, vbCritical
End Sub
—
4. チーフアーキテクトが教える、現場で絶対に外せない実装の鉄則
このコードを実務の巨大なシステムに組み込む際、シニアエンジニアとして知っておくべき「落とし穴」と「最適化の極意」を共有する。
① `ScreenUpdating = False` と `AutoRecover = False` の両立
大量のシェイプを操作する場合、画面の再描画を止めるだけでは不十分な場合がある。Visioの自動回復(AutoRecover)機能がバックグラウンドで図面ファイルのテンポラリ書き込みを行おうとすると、VBAの高速処理と競合してI/Oボトルネックを引き起こす。バルク処理の前後は必ず両方のフラグを制御せよ。
② ユニバーサル名 (`U`) の徹底
`Cells` ではなく `CellsU`、`SetFormulas` ではなく `SetFormulasU` を使わなければならない理由は国際化(ローカライズ)だけではない。パフォーマンス上のオーバーヘッドが異なるからだ。
ローカル名(日本語版なら「幅」「高さ」など)を指定すると、Visioエンジン側で言語依存の文字列解析が発生する。常に英語ベースのユニバーサル名を使用することで、内部的なパース処理をバイパスできる。
③ エラーハンドリングにおける「状態の確実な復元」
巨大なデータ処理中にエラーが発生した際、`ScreenUpdating = False` のままコードが中断すると、ユーザーの画面上ではVisioが完全にフリーズしたように見える(最悪の場合、強制終了するしかない)。必ず `On Error GoTo` を設置し、イミディエイトウィンドウやクリーンアップブロックで環境変数を元の状態に戻す防衛的プログラミングを徹底すること。
—
5. まとめ:レガシーの殻を破る自動化へ
VBAはレガシーな言語と揶揄されることがある。しかし、VisioのCOMオブジェクトモデルの奥底を理解し、今回紹介した `SetFormulasU` による配列処理のような「APIの本質を突いたアプローチ」を採用すれば、モダンな言語で書かれたデスクトップアプリケーションに匹敵する、あるいはそれを凌駕する爆速の自動化基盤を構築できる。
日々の業務で「Visioのマクロは遅い」と諦めていたエンジニア諸君。
今すぐコードを見直し、COM境界を跨ぐ回数を極限まで減らしてみてほしい。圧倒的なスピードの向上が、君のシステム開発における新しい武器となるはずだ。
