Visio VBAを掌握する極限の知見:Shape.GetResultsとSetResultsによる配列一括転送
Visio VBAにおける最大の罪悪、それは「シェイプのループ内でセル(Cell)に個別アクセスすること」だ。
数千のシェイプを持つ図面に対し、`For Each shp In ActivePage.Shapes` を回し、その中で `shp.Cells(“Prop.Cost”).FormulaU = …` と叩くコードを書いた瞬間、そのマクロは数分、あるいは数十分の沈黙という名の「ゴミ」と化す。COMの境界を何千回、何万回と跨ぐオーバーヘッドを甘く見た代償だ。
シニアエンジニアであれば、Visioの描画エンジンとCOM相互運用(COM Interop)のメカニズムを理解していなければならない。本稿では、`Shape.GetResults` と `Shape.SetResults` を駆使し、メモリ上でデータを一括処理して極限のパフォーマンスを引き出す手法を解説する。
—
1. なぜ個別アクセスは「死」を意味するのか
Visioの各シェイプが持つ数多くのセル(形状、位置、カスタムプロパティなど)は、それぞれが独立したオブジェクト(`Cell`オブジェクト)として存在している。
VBAから `shp.Cells(“Width”).ResultIU` のようにアクセスすると、以下の処理が内部で発生する。
1. VBAランタイムからCOM境界を越えてVisioのネイティブエンジンへリクエストが飛ぶ。
2. 指定されたセル名をパースし、対応する `Cell` オブジェクトのインスタンスがヒープ上に生成される。
3. 値を取得・設定し、その都度COMラッパーの参照カウンタが変動する。
4. 再びCOM境界を越えてVBA側へ制御が戻る。
これを数千のシェイプ×複数のプロパティで行えば、CPU時間の大半が「COMの往復」と「オブジェクトの生成・破棄」に費やされるのは自明である。
救世主:GetResults と SetResults
このボトルネックを根絶するのが、`Page.GetResults` および `Page.SetResults`(または `Shape` コレクションに対するメソッド群)である。
これらは、「一陣の配列」を一度のCOM呼び出しでVisioエンジンに投げ込み、メモリ上で一括処理して一瞬で返す。オブジェクトの生成コストをゼロにし、COMの往復回数を最小化する、Visio VBAにおける唯一無二の最適解だ。
—
2. アーキテクチャ設計:一括処理のデータフロー
`GetResults` / `SetResults` を用いる際、最大化すべきは「メモリの連続性」と「型の一致」である。
- 取得(GetResults): 取得したいシェイプの配列、セルの名前(またはインデックス)の配列を渡し、二次元配列(Variant)として一括回収する。
- 設定(SetResults): 変更対象のシェイプID配列、セル名配列、そして型を一致させた値の配列を渡し、一撃でコミットする。
ここで重要となるのが、Visioが要求するデータ型の厳格さだ。文字列を入れるべきところに数値(Variant/Double)を突っ込めば、容赦なく「型が一致しません(Error 13)」の例外が飛ぶ。
—
3. 実践コード:数千シェイプのデータを一瞬で読み書きする
以下のコードは、アクティブページ上の全シェイプからカスタムプロパティ(例: `”Prop.ID”` と `”Prop.Status”`)を一括取得し、値を加工した上で、一括書き戻す実用的な実装である。
Option Explicit
‘ ==============================================================================
‘ 致命的な遅延を排除し、数千シェイプをミリ秒単位で処理するマスター関数
‘ ==============================================================================
Public Sub BatchProcessShapesLimitless()
Dim startTime As Double
startTime = Timer
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage
Dim vsoShapes As Visio.Shapes
Set vsoShapes = vsoPage.Shapes
Dim lngCount As Long
lngCount = vsoShapes.Count
If lngCount = 0 Then
MsgBox “処理対象のシェイプが存在しません。”, vbExclamation
Exit Sub
End If
‘ 1. シェイプのインデックス(またはID)と、処理対象のセル名義を定義
‘ ※パフォーマンス維持のため、シート名やセル名は定数またはあらかじめ用意する
Dim intShapeIndex As Long
‘ シェイプの配列を確保(Visioのコレクションは 1-based)
Dim sourceShapes() As Visio.Shape
ReDim sourceShapes(1 To lngCount)
Dim shapeIndices() As Integer
ReDim shapeIndices(1 To lngCount)
For intShapeIndex = 1 To lngCount
Set sourceShapes(intShapeIndex) = vsoShapes(intShapeIndex)
shapeIndices(intShapeIndex) = sourceShapes(intShapeIndex).ID
Next intShapeIndex
‘ 2. 取得するセル名の定義(ユニIDやステータスプロパティを想定)
‘ 複数指定する場合は 0-based の 1次元配列で渡す
Dim srcCellNames(0 To 1) As String
srcCellNames(0) = “Prop.ID”
srcCellNames(1) = “Prop.Status”
‘ 返却値を受け取るための Variant 型二次元配列 (Row, Col)
Dim varResults As Variant
‘ ————————————————————————–
‘ GetResults の実行:一瞬で全シェイプのデータをメモリ上に吸い上げる
‘ ————————————————————————–
‘ 引数: (SourceNames() As Variant, Flags As Integer, ResultUnits() As Variant, [ResultNames() As Variant])
‘ ※ Visioのバージョンやインターフェース仕様に応じ、Application.GetResults を使用するのが最も堅牢
Dim appVisio As Visio.Application
Set appViso = Application
‘ 効率的なクエリ配列の構築(全シェイプ×指定セル)
Dim intNumCells As Long
intNumCells = UBound(srcCellNames) – LBound(srcCellNames) + 1
Dim totalQueries() As String
ReDim totalQueries(1 To lngCount intNumCells)
Dim q As Long: q = 1
Dim i As Long, j As Long
For i = 1 To lngCount
For j = 0 To UBound(srcCellNames)
‘ 各シェイプのプロパティパスを構築 (例: “Sheet.1!Prop.ID”)
totalQueries(q) = “Sheet.” & sourceShapes(i).ID & “!” & srcCellNames(j)
q = q + 1
Next j
Next i
Dim varValues As Variant
Dim flags As Long
flags = Visio.VisGetSetArgs.visGetSetNoCast ‘ キャストなしで高速取得
‘ アプリケーションレベルでの一括取得
appVisio.GetResults totalQueries, flags, varValues
‘ 3. 取得したデータのメモリ上でのインメモリ処理
‘ varValues は 1次元配列(各要素に値が入る)として返される
For i = LBound(varValues) To UBound(varValues)
‘ 例として、Status が “Pending” だったものを “Processing” に書き換えるロジックを挟む
If CStr(varValues(i)) = “Pending” Then
varValues(i) = “Processing”
End If
Next i
‘ ————————————————————————–
‘ SetResults の実行:メモリ上の配列を一撃でVisioエンジンへ流し込む
‘ ————————————————————————–
appVisio.SetResults totalQueries, varValues
‘ 4. クリーンアップとパフォーマンス計測の出力
Debug.Print “処理シェイプ数: ” & lngCount & ” / 処理時間: ” & Format(Timer – startTime, “0.000”) & ” 秒”
MsgBox “一括処理が完了しました。” & vbCrLf & “経過時間: ” & Format(Timer – startTime, “0.000”) & ” 秒”, vbInformation
End Sub
—
4. チーフアーキテクトが教える、現場の罠と極限の最適化
上記のコードは美しく高速だが、実際のEnterprise環境(数万シェイプ規模のプラント図や、複雑なネットワーク図)では、さらに以下の「地雷」を踏む可能性があるので注意せよ。
① 配列の次元数と方向の罠
`GetResults` / `SetResults` が返す配列、および要求する配列の次元(インデックスの方向)は、VBAの仕様とCOMのマーシャリングの都合上、意図しない挙動を示すことがある。
特に `Application.GetResults` を使う場合、渡すクエリ配列の構造と返ってくる `varValues` の型・次元(1次元の `Variant` 配列)を厳密に `VarType` や `UBound` でデバッグしながら実装すること。型ミスマッチのまま `SetResults` を叩くと、容赦なくランタイムエラー 9(インデックスが有効ではありません)やエラー 13 が発生する。
② エラーセル(#NAME? や #REF!)のハンドリング
図面が荒れていたり、数式が破損しているシェイプが含まれている場合、`GetResults` はエラー値(`Error 2023` など)をその配列要素に格納して返してくる。
これをそのまま文字列結合や数値演算に使うと、VBA側で即座にクラッシュする。
必ず以下のような型・エラーチェックのガード句をインメモリ処理ループ内に挟むこと。
If Not IsError(varValues(i)) Then
‘ 正常系処理
If VarType(varValues(i)) = vbString Then
‘ 文字列処理
End If
End If
③ 画面描画の完全な凍結(ScreenUpdating)
どれだけ `GetResults` が高速でも、Visioが裏で描画(Redraw)を行っていればCPUサイクルが無駄に消費される。
マクロの実行前には必ず以下を明記し、全処理が完了するまでVisioのGUIを完全に沈黙させよ。
Application.ScreenUpdating = False
Application.EventsEnabled = False
‘ — 処理本体 —
Application.EventsEnabled = True
Application.ScreenUpdating = True
Application.Windows.Item(1).Activate ‘ 最後に強制再描画を促す
—
5. 結び:アーキテクトの矜持
VBAは、決して「おもちゃの言語」ではない。
背後にあるCOMの挙動とメモリ管理の仕組みを熟知し、適切なAPIインターフェース(この場合は `GetResults` / `SetResults`)を選択さえすれば、C#製アドインに匹敵する爆速のデータ処理基盤へと変貌する。
個別のセルを舐めるようなレガシーなコードは今日で捨て去り、メモリを支配する者だけが扱える「配列一括転送」の圧倒的なスピードを、あなたのシステムへ実装せよ。
