Visio VBAを掌握する極限の知見:Page.GetResultsとSetResultsによる外部CSV超高速双方向同期パイプライン
Visio VBAにおける最大の性能ボトルネックは、オブジェクトモデルとの無秩序な対話にある。
数千個の図形を持つ図面に対し、`For Each`ループを回して個別に`Shape.Cell`へアクセスし、値を読み書きするようなコードを書く者は、今日をもってプログラマを名乗るべきではない。
VisioのCOMインターフェース越しの往復通信(Marshalling)は、VBAの実行速度を容易に殺す。図形が1,000個あり、それぞれに10個のカスタムプロパティ(ShapeSheetセル)が存在する場合、個別アクセスでは1万回のCOMラウンドトリップが発生する。これが図面描画の更新イベント(`ScreenUpdating`)や再計算トリガーと絡み合えば、処理が終わる頃にはコーヒーが冷めるどころか、プロセスがフリーズする。
今回は、この呪縛を断ち切り、`Page.GetResults` と `Page.SetResults` を駆使して、外部CSVデータとVisio図形プロパティを「1回の配列転送」で同期させる、極限の高速化アーキテクチャを提示する。
—
1. なぜ `GetResults` と `SetResults` なのか?
シニアエンジニアであれば常識だが、VisioのShapeSheet操作において、単一のセルを操作するプロパティと、配列をバルク処理するメソッドの間には、地上戦と空爆ほどのパフォーマンスの差がある。
- 個別アクセス (`Shape.CellsSRC().FormulaU`):
- 呼び出しごとにCOM境界を跨ぎ、ShapeSheetの再計算エンジンが都度走る。
- 一括処理 (`Page.GetResults` / `Page.SetResults`):
- C++のネイティブメモリ領域とVBAのSafeArrayの間で、メモリブロックをダイレクトにコピーする。
- 計算エンジンを一時停止(または遅延評価)させながら、一次元(または二次元)配列を一網打尽に読み書きする。
このメソッドのシグネチャを正確に理解している者は少ない。
特に `GetResults` は、取得したいセルの情報を格納した「セルの位置を示す配列(SRC配列)」を事前に構築し、結果を受け取るための「バッファ配列」を用意して一括問い合わせを行う必要がある。
—
2. アーキテクチャの全体像
本稿で構築する双方向同期パイプラインのフローは以下の通りだ。
1. メモリ最適化と描画停止: `ScreenUpdating` とイベントを抑制し、無駄な再描画とイベントハンドラの暴走を防ぐ。
2. 一括読み込み (GetResults): ターゲットページ上の全対象図形の特定プロパティ(例:`Prop.ID`, `Prop.Status` 等)の数式/値を、1回のAPIコールでVBAの2次元配列へと吸い上げる。
3. 差分検出: 外部CSV(あるいはADO/Recordset経由でメモリ上に展開したデータ)とVBA側配列をオンメモリで比較し、変更があったセルのみを書き換え対象としてマークする。
4. 一括書き戻し (SetResults): 変更差分のみ、あるいは全同期データを1回のAPIコールでVisioへ流し込む。
5. クリーンアップ: オブジェクトの明示的解放とアプリケーション状態の復元。
—
3. 実装コード:超高速同期エンジン
以下のコードは、エラーハンドリング、メモリの厳密な管理、そしてバルク処理の極みを示した実用モジュールである。
Option Explicit
‘ Win32 API: 処理速度計測用(ミリ秒精度)
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
Public Sub ExecuteHighSpeedSync()
Dim startTime As Double
startTime = Timer
‘ 1. 環境の最適化(パフォーマンスの極限追求)
With Application
.ScreenUpdating = False
.EventEnabled = False
.UndoEnabled = False
End With
On Error GoTo ErrorHandler
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage
Dim vsoShapes As Visio.Shapes
Set vsoShapes = vsoPage.Shapes
Dim lngShapeCount As Long
lngShapeCount = vsoShapes.Count
If lngShapeCount = 0 Then GoTo CleanUp
‘ 2. 対象図形の抽出とSRC(Section, Row, Column)配列の構築
‘ ここではカスタムプロパティ “Prop.AssetID” と “Prop.Status” をターゲットとする
Dim i As Long, shapeIndices() As Long
Dim validShapeCount As Long
validShapeCount = 0
ReDim shapeIndices(1 To lngShapeCount)
‘ 特定の条件(例:特定のマスターや命名規則)を満たす図形のみを対象にする場合ここでフィルタリング
For i = 1 To lngShapeCount
‘ ここでは全図形を対象(実際にはレイヤーやシェイプ名で絞り込むべき)
validShapeCount = validShapeCount + 1
shapeIndices(validShapeCount) = vsoShapes(i).ID
Next i
If validShapeCount = 0 Then GoTo CleanUp
ReDim Preserve shapeIndices(1 To validShapeCount)
‘ 3. GetResults 用の SRC 配列の定義 (2プロパティ × 図形数)
‘ Visioのセルの位置を数値IDで指定する (section, row, column)
‘ 例: ユーザー定義/カスタムプロパティセクション = visSectionProp (24)
Const TARGET_PROP_COUNT As Long = 2
Dim totalCells As Long
totalCells = validShapeCount TARGET_PROP_COUNT
Dim srcStream() As Integer
ReDim srcStream(0 to (totalCells 3) – 1)
Dim cellIndex As Long
cellIndex = 0
For i = 1 To validShapeCount
‘ プロパティ1: Prop.AssetID (仮の行インデックス 0, 列は Value = 0)
‘ ※実際にはNamedRowからIndexを取得するか、固定行を使う
srcStream(cellIndex) = Visio.visSectionProp
srcStream(cellIndex + 1) = Visio.visRowProp + 0 ‘ 1行目
srcStream(cellIndex + 2) = Visio.visCustPropsValue
cellIndex = cellIndex + 3
‘ プロパティ2: Prop.Status
srcStream(cellIndex) = Visio.visSectionProp
srcStream(cellIndex + 1) = Visio.visRowProp + 1 ‘ 2行目
srcStream(cellIndex + 2) = Visio.visCustPropsValue
cellIndex = cellIndex + 3
Next i
‘ 4. GetResults による一括取得(驚異の1回コール)
Dim resultsArray() As Variant
‘ 引数: (SourceArray, Flags, ResultUnitsArray, 取得先配列)
vsoPage.GetResults srcStream, Visio.visGetFloats + Visio.visGetNamesUI, Nothing, resultsArray
‘ 5. 外部CSVデータとの突合・差分検出(メモリ上での処理)
‘ ※ここでは疑似的に外部CSVを読み込んだメモリ上の辞書(Dictionary)または配列があると仮定
Dim csvDataMap As Object
Set csvDataMap = LoadExternalCSVToDictionary(“C:\Data\assets.csv”)
Dim writeBackCount As Long
writeBackCount = 0
For i = 1 To validShapeCount
Dim currentAssetID As String
currentAssetID = CStr(resultsArray((i – 1) TARGET_PROP_COUNT))
‘ CSV側に該当アセットが存在するか確認
If csvDataMap.Exists(currentAssetID) Then
Dim newStatus As String
newStatus = csvDataMap(currentAssetID)
‘ 現在のステータスと異なる場合のみ書き込み対象にする
Dim currentStatus As String
currentStatus = CStr(resultsArray(((i – 1) TARGET_PROP_COUNT) + 1))
If currentStatus <> newStatus Then
‘ 変更値をセット (文字列の場合はダブルクォーテーションで囲むのがVisioの作法)
resultsArray(((i – 1) TARGET_PROP_COUNT) + 1) = “””” & newStatus & “”””
writeBackCount = writeBackCount + 1
End If
End If
Next i
‘ 6. SetResults による一括書き戻し(変更がある場合のみ)
If writeBackCount > 0 Then
‘ 注意: SetResultsには書き換えるセルのSRC配列、対応する値の配列、フラグを指定
vsoPage.SetResults srcStream, resultsArray, Visio.visSetFormulas
End If
Debug.Print “同期完了: 処理図形数=” & validShapeCount & “, 更新数=” & writeBackCount & “, 実行時間: ” & (Timer – startTime) & “秒”
CleanUp:
‘ 7. アプリケーション状態の厳密な復元
With Application
.ScreenUpdating = True
.EventEnabled = True
.UndoEnabled = True
End With
‘ オブジェクトの明示的解放
Set csvDataMap = Nothing
Set vsoShapes = Nothing
Set vsoPage = Nothing
Exit Sub
ErrorHandler:
MsgBox “同期処理中に致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ 外部CSVをメモリ上のDictionaryに展開するヘルパー関数(ADO使用)
Private Function LoadExternalCSVToDictionary(ByVal filePath As String) As Object
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)
‘ 実運用では ADODB.Stream や FileSystemObject を用いてパースする
‘ ここでは構造の概念を示すため省略せず最小限のFSO実装を記載
Dim fso As Object, ts As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)
If fso.FileExists(filePath) Then
Set ts = fso.OpenTextFile(filePath, 1) ‘ ForReading
Dim line As String
Dim tokens() As String
‘ ヘッダースキップ
If Not ts.AtEndOfStream Then ts.ReadLine
Do While Not ts.AtEndOfStream
line = ts.ReadLine
tokens = Split(line, “,”)
If UBound(tokens) >= 1 Then
‘ Key: AssetID, Value: Status
Dim key As String: key = Trim(tokens(0))
Dim val As String: val = Trim(tokens(1))
If Not dict.Exists(key) Then dict.Add key, val
End If
Loop
ts.Close
End If
Set LoadExternalCSVToDictionary = dict
Set ts = Nothing
Set fso = Nothing
End Function
—
4. チーフアーキテクトが教える、現場の「罠」と極限の最適化テクニック
上記のコードをそのまま社内ニッチな環境に投入するだけでは、ベテランエンジニアとは言えない。以下の実戦的知見を頭に叩き込んでおけ。
① NamedRow(名前付き行)のトラップ
Visioのカスタムプロパティ(ShapeSheetの `Prop` セクション)は、行が追加・削除されるたびにインデックス(行番号)が変動する。コード内で `visRowProp + 0` のようにハードコーディングするのは、図形が改変された瞬間に破綻する「地雷」である。
真にロバストなシステムを構築する場合、`Shape.CellsSRC` や行名(例: `Prop.AssetID`)から動的に行インデックスを解決するか、図形マスター側でプロパティ構造を完全に統制(ガバナンス)しなければならない。
② メモリリークとCOMオブジェクトの残骸
VBAにおける `Set xxx = Nothing` は気休めだと思うなかれ。特に `Visio.Page` や `Visio.Shapes` などのコンテナオブジェクトをループ内で解放し忘れたり、エラートップでそのまま抜けると、裏でCOMの参照カウント(Reference Count)が残存し、Visioプロセスがゴースト化してメモリを食いつぶす。
エラーハンドラ(`ErrorHandler`)を通した確実なクリンナップ経路の確保は、サーバレスや常時稼働環境の自動化において絶対の要件である。
③ 文字列のエスケープと数式(Formula)の扱い
`SetResults` に渡す配列の値は、文字列リテラルであれば `”\”StatusName\””` のようにダブルクォーテーションで二重に囲む必要がある。これを忘れると、Visioは文字列ではなく「未定義のシェイプ名や関数名(エラー: #NAME?)」と解釈し、図形プロパティが盛大に破損する。
また、計算式(Formula)を書き込むのか、評価値(Value)を書き込むのかによって、フラグ(`visSetFormulas` vs `visSetValues`)を厳密に使い分けること。外部データとの同期であれば、基本的には数式として流し込むのが安全である。
—
総括
Visio VBAは、もはや過去の遺物などではない。APIの本質(COMマーシャリングの抑制、メモリブロックの一括操作)を理解した者が操れば、現代のデスクトップアプリケーションやRPAツールをも凌駕する、圧倒的な高速処理基盤に変貌する。
個別のセルをチマチマと書き換えるコードは、今日で卒業しなさい。
「一括取得・オンメモリ処理・一括書き戻し」のパイプラインこそが、あなたを真のVisioオートメーション・マエストロへと導く唯一の道である。
