Visio VBAを掌握する極限の知見:トランザクション駆動型Undoパターンによる図面データの絶対防衛
Visioのオブジェクトモデルを相手にする開発者であれば、一度は悪夢を見たことがあるはずだ。
数千個のシェイプを走査し、座標の再計算、マスターシェイプの動的配置、プロパティの書き込みを行う巨大なバッチ処理。その実行中、ループの7割を超えたあたりで予期せぬ実行時エラー(例えば、存在しないカスタムプロパティへのアクセスや、不正なハンドルの参照)が発生する。
画面を見ると、図面は半分だけ更新され、古いシェイプと新しいカオスが混ざり合った「ゴミ屋敷」のような状態になっている。
これをユーザーが手動でCtrl+Z連打で戻そうものなら、Visioは沈黙するか、最悪の場合、ドキュメント構造そのものが破損して `.vsd/.vsdx` が開かなくなる。
アマチュアの開発者はここで「エラー処理でメッセージを出して終了」という暴挙に出る。
しかし、プロフェッショナルなアーキテクトが目指すべきは、「一歩も妥協しない原子性(Atomicity)の担保」だ。Visio VBAにおいてトランザクションスコープを擬似的に構築し、障害発生時には一瞬で原状回復(ロールバック)させるための設計と実装の極意をここに開示する。
—
1. Visio Undoスタックの深層と「偽りのトランザクション」
RDB(リレーショナルデータベース)の世界であれば `BEGIN TRANSACTION` と `ROLLBACK` で一撃だが、VisioのCOMオブジェクトモデルにはネイティブな「トランザクション制御オブジェクト」は存在しない。
ここで頼るべきは、Visioが内部で管理しているUndo(元に戻す)スタックである。
Visioは、ユーザーの操作やVBAからの図形操作を「Undoユニット」としてスタックに積み上げる。しかし、VBAの実行中、標準状態では次のような問題が発生する。
1. 細切れのUndoユニット: ループ内で1つのシェイプを変更するたびにUndoスタックが1つ消費され、スタックの深度制限(デフォルトまたは設定による)を超えると、古い変更から順に消えていく。
2. エラー時の巻き戻し精度の欠如: VBAの `On Error` で単に処理を抜けても、それまでに実行された数十〜数千の変更はスタックにバラバラに散らばっており、一括して綺麗に戻すことが極めて困難。
これを解決するのが、「カスタムUndoスコープ(BeginUndoScope / EndUndoScope)」を用いたトランザクションの強制カプセル化である。
—
2. 設計思想:RAIIパターンとエラーバウンダリ
C++やRustにおけるRAII(Resource Acquisition Is Initialization)の概念をVisio VBAに持ち込む。
処理の開始時に巨大なUndoスコープの「コンテナ」を1つ開き、その中で起きたすべての変更をそのコンテナに飲み込ませる。もし処理が成功すればそのスコープを「確定(Commit)」し、例外や意図しない中断が発生した場合は、スコープ単位で「一括破棄(Abort / Undo)」する。
これにより、データベースのトランザクションと同等の原子性をVisio上で擬似的に再現できる。
—
3. 実装コード:実戦投入可能な「SafeTransaction」エンジン
以下のコードは、実際のエンタープライズ環境(数万シェイプを扱うプラント図・ネットワーク図の自動生成バッチなど)で耐えうるよう設計された、トランザクション管理モジュールの完成形である。
標準モジュール(例: `MdlTransaction`)として実装せよ。
Option Explicit
‘ =========================================================================
‘ Visio トランザクション制御エンジン (v1.0.0)
‘ Architecture Note:
‘ VisioのUndoスコープを悪用し、VBA実行中の例外発生時に一括ロールバックを
‘ 強制する堅牢なエラーバウンダリを提供する。
‘ =========================================================================
Private m_UndoScopeID As Long
Private m_IsTransactionActive As Boolean
”’
”’
”’ 対象のVisio Applicationオブジェクト
”’ Undoメニューに表示される処理名
Public Sub BeginSecureTransaction(ByRef vsoApp As Visio.Application, ByVal scopeName As String)
If m_IsTransactionActive Then
Err.Raise 9999, “BeginSecureTransaction”, “ネストされたトランザクションはサポートされていません。”
End If
On Error GoTo ErrorHandler
‘ 画面描画をフリーズさせ、パフォーマンスを極限まで高める(メモリ最適化の基本)
vsoApp.ScreenUpdating = False
vsoApp.ShowChanges = False
‘ VisioのUndoスタックに新しいカスタムスコープをオープン
m_UndoScopeID = vsoApp.BeginUndoScope(scopeName)
m_IsTransactionActive = True
Exit Sub
ErrorHandler:
‘ スコープ開始に失敗した場合は描画を復旧してエラーを投げる
vsoApp.ScreenUpdating = True
vsoApp.ShowChanges = True
m_IsTransactionActive = False
Err.Raise Err.Number, “BeginSecureTransaction”, “トランザクションの開始に失敗しました: ” & Err.Description
End Sub
”’
”’
Public Sub CommitSecureTransaction(ByRef vsoApp As Visio.Application)
If Not m_IsTransactionActive Then Exit Sub
On Error GoTo ErrorHandler
‘ スコープを正常終了(コミット)
vsoApp.EndUndoScope m_UndoScopeID, True
‘ 描画設定の復旧
Call CleanupEnvironment(vsoApp)
Exit Sub
ErrorHandler:
‘ コミット失敗時は強制ロールバックへフォールバック
Call RollbackSecureTransaction(vsoApp)
Err.Raise Err.Number, “CommitSecureTransaction”, “コミットに失敗しました。自動ロールバックを実行しました: ” & Err.Description
End Sub
”’
”’
Public Sub RollbackSecureTransaction(ByRef vsoApp As Visio.Application)
If Not m_IsTransactionActive Then Exit Sub
On Error Resume Next
‘ 第2引数を False にすることで、このスコープ内の全操作を「なかったこと」にする
vsoApp.EndUndoScope m_UndoScopeID, False
‘ 描画設定の復旧
Call CleanupEnvironment(vsoApp)
End Sub
”’
”’
Private Sub CleanupEnvironment(ByRef vsoApp As Visio.Application)
m_IsTransactionActive = False
m_UndoScopeID = 0
‘ 描画を再開
If Not vsoApp Is Nothing Then
vsoApp.ScreenUpdating = True
vsoApp.ShowChanges = True
‘ Visioの内部キャッシュを解放するため強制的にイベント処理を挟む軸を作る
DoEvents
End If
End Sub
呼び出し側の実装パターン(利用例)
実際の業務ロジックでは、以下のように `On Error Goto` と組み合わせて完全なガードを構築する。
Sub BusinessProcess_ExecuteMassUpdate()
Dim vsoApp As Visio.Application
Set vsoApp = Application
Dim hasError As Boolean
hasError = False
‘ 1. トランザクションの開始
BeginSecureTransaction vsoApp, “一括シェイプ最適化処理”
On Error GoTo Catch
‘ — 【ここから業務ロジック(何千回もの図形操作)】 —
Dim vsoPage As Visio.Page
Set vsoPage = vsoApp.ActivePage
Dim i As Long
For i = 1 to 1000
‘ 例: シェイプの生成やプロパティ変更
‘ 途中でバグや想定外の値が入ってエラーが発生したとする
If i = 500 Then Err.Raise 1001, , “意図しないデータ不整合を検知”
Next i
‘ — 【業務ロジックここまで】 —
‘ 2. 正常終了時のコミット
CommitSecureTransaction vsoApp
Exit Sub
Catch:
hasError = True
‘ 3. 異常終了時のロールバック
Dim errDesc As String
errDesc = Err.Description
RollbackSecureTransaction vsoApp
MsgBox “処理中に致命的なエラーが発生したため、図面を原状回復しました。” & vbCrLf & _
“詳細: ” & errDesc, vbCritical, “トランザクション・ロールバック”
End Sub
—
4. チーフアーキテクトが教える「現場の罠」とメモリ最適化の極意
このパターンを実務導入するにあたり、シニアエンジニアとして知っておくべきハードウェア・アーキテクチャ上の注意点を挙げる。
① `ScreenUpdating = False` の絶対的遵守
大量のシェイプを操作する際、画面描画(UIスレッドとCOMの同期)が走るたびにCPUとメモリのバスが占有され、処理速度が10倍以上低下するだけでなく、Visio自体のメモリリークを引き起こす原因となる。トランザクション内では必ず画面描画を殺せ。ただし、エラー時も含めて必ず復旧させることが大前提である(上記の `CleanupEnvironment` がそれを担保している)。
② オブジェクト変数の即時解放(`Set … = Nothing`)
VBAのガベージコレクションは頼りにならない。特にトランザクション内で大量の `Visio.Shape` や `Visio.Cell` をループ生成・参照する場合、ループの都度オブジェクト変数を使い回すか、不要になった時点で明示的に `Nothing` を代入してCOM参照カウンタをデクリメントしなければならない。これを怠ると、ロールバック処理そのものがメモリ不足(Error 7: Out of memory)で失敗するという最悪の結末を迎える。
③ レガシー環境・アドイン連携時の注意
外部システム(C#.NET製COM Add-inやデータベース)との連携中にこのVBAトランザクションを走らせる場合、Visio側のロールバックと外部DB側のトランザクション(2層コミット問題)の整合性をどう取るかが課題となる。
基本原則として、「Visioの処理を完全に完結させてから外部DBを叩く」、あるいはその逆の順序を守り、VBA側は純粋に「図面ファイルの整合性維持」だけにこのUndoパターンを適用すること。
—
5. 結び:コードの品格は「失敗への備え」に宿る
動くコードを書くことは、プログラミングの初学者でもできる。
しかし、「壊れたときに、いかに美しく、何事もなかったかのように元の世界へ引き返せるか」をデザインできるか否かこそが、シニアエンジニアとコード職人を分ける境界線である。
Visio VBAにおける `BeginUndoScope` を手懐けた者よ、あなたの図面データは今日から、いかなる予期せぬ例外からも完全に守られることになる。圧倒的な堅牢性を持つ自動化システムを、自信を持って構築せよ。
