【Project VBA】Collectionオブジェクトを用いたタスク情報のメモリ内高速処理:DOMアクセスの呪縛を断つアーキテクチャ
Microsoft ProjectのVBA開発において、多くのエンジニアが直面する最大の壁、それは「DOM(Document Object Model)直接操作の圧倒的な遅さ」である。
TaskオブジェクトやResourceオブジェクトに対し、ループを回しながら`.Name`や`.Start`、あるいはカスタムフィールド(Text1〜30など)を直接読み書きしたことはないだろうか。数千行規模の大規模スケジュールにおいて、このアプローチは自殺行為に等しい。セルを1つ書き換えるたびにProjectのGUIスレッドが再描画され、依存関係の再計算トリガーが引かれ、COMの境界を跨ぐオーバーヘッドが蓄積する。結果として、簡単なステータス更新に数十分もの時間が溶けていく。
この泥沼から抜け出す唯一の解が、「メモリ内(RAM)へのデータ退避とCollectionによる一括処理」である。
今回は、シニアアーキテクチャの視点から、ProjectのDOMアクセスを極限まで排除し、VBAのネイティブメモリ上でタスク情報を高速処理した上で一括書き戻しを行う、極限のパフォーマンスチューニング手法を解説する。
—
1. なぜProjectのDOM操作はこれほどまでに遅いのか
背後にあるメカニズムを理解していなければ、真の最適化はできない。
MS ProjectのVBAオブジェクトモデルは、底层(ネイティブ層)のC++エンジンとCOM(Component Object Model)を介して接続されている。VBA側から `ActiveProject.Tasks(i).Start` と記述した瞬間、以下の処理が発生している。
1. VBAランタイムからCOMインターフェースを介してProjectのエンジンへコールが飛ぶ。
2. Project側で該当タスクのインデックスを解決し、データを取得・マーシャリングする。
3. 変更を伴う場合、WBS構造やクリティカルパスの再計算エンジンが一時的に走る。
これを数万回繰り返すのだから遅いのは当然だ。さらに、`ScreenUpdating = False` や `Calculation = manual` といった一般的なExcel VBA向けのテクニックは、Project VBAにおいては限定的な効果しか持たない(そもそもProjectは計算エンジンの構造がExcelとは異なる)。
したがって、「Projectのオブジェクトに触れる回数を物理的に最小限にする」これ以外に高速化の道はない。
—
2. アーキテクチャ設計:UserDefinedType(UDT)とCollectionの融合
メモリ内高速処理の基本戦略はこうだ:
1. 一括読み込み(Read): Projectを開いた直後、必要なタスク情報を1度だけ走査し、独自のデータ構造(User-Defined Type: UDT)に格納して、それを `Collection` に蓄積する。
2. メモリ内処理(Process): 以降の複雑なロジック、データ加工、フィルタリング、値の計算は、すべてRAM上のCollectionに対してのみ行う。ProjectのDOMには一切触れない。
3. 一括書き戻し(Write): すべての処理が完了した後、Collectionのデータを逆方向へ流し込み、Projectへ一括で反映する。
なぜ Dictionary ではなく Collection なのか?
VBAにおける高速なインメモリキャッシュといえば `Scripting.Dictionary` が思い浮かぶかもしれない。しかし、ProjectのタスクID(IDやUniqueID)をキーにする場合、`Collection` の方がメモリフットプリントが小さく、純粋なシーケンシャルアクセスにおいてオーバーヘッドが少ない。また、ポータビリティの観点から外部参照設定(Microsoft Scripting Runtime)を不要にできるメリットは、現場での配布において極めて重要である。
—
3. 実装コード:極限のメモリ内処理エンジン
以下に、実業務でそのまま耐えうる堅牢性とパフォーマンスを兼ね備えたプロダクションコードを示す。このコードは、全タスクのカスタムフィールド(Text1)を一括処理し、処理時間を劇的に短縮する。
Option Explicit
‘ ==============================================================================
‘ タスク情報を保持するカスタム構造体(UDT)
‘ DOMへのアクセスを排除し、RAM上で保持するための軽量コンテナ
‘ ==============================================================================
Public Type TaskData
UniqueID As Long
ID As Long
Name As String
Start As Date
Finish As Date
Text1 As String
IsDirty As Boolean ‘ 変更フラグ(無駄な書き込みを抑制)
End Type
‘ ==============================================================================
‘ メインプロシージャ:メモリ内高速処理のオーケストレーション
‘ ==============================================================================
Sub ExecuteHighSpeedTaskProcessing()
Dim startTime As Double
startTime = Timer
‘ 1. エラーハンドリングと環境の凍結
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Application.Calculation = pjManual
Dim taskCol As Collection
Set taskCol = New Collection
‘ 2. 【Read】ProjectのDOMからメモリ(Collection)へ一括展開
Call LoadTasksToMemory(taskCol)
Debug.Print “メモリロード完了: ” & taskCol.Count & “件 (” & Format(Timer – startTime, “0.00秒”) & “)”
‘ 3. 【Process】メモリ上で高速データ処理(例:条件に応じたText1の書き換え)
Call ProcessTasksInMemory(taskCol)
Debug.Print “メモリ内処理完了 (” & Format(Timer – startTime, “0.00秒”) & “)”
‘ 4. 【Write】変更があったデータのみProjectへ一括書き戻し
Call FlushMemoryToProject(taskCol)
Debug.Print “トータル処理時間: ” & Format(Timer – startTime, “0.00秒”) & “秒”
CleanUp:
‘ 環境の復元
Application.ScreenUpdating = True
Application.Calculation = pjAutomatic
Application.CalculateAll
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ ==============================================================================
‘ 読み込みレイヤー:Project -> UDT -> Collection
‘ ==============================================================================
Private Sub LoadTasksToMemory(ByRef col As Collection)
Dim t As Task
Dim tData As TaskData
Dim tskList As Tasks
Set tskList = ActiveProject.Tasks
Dim i As Long, cnt As Long
cnt = tskList.Count
For i = 1 To cnt
Set t = tskList(i)
‘ 摘要行やサマリータスク、空行のハンドリング
If Not t Is Nothing Then
If Not t.ExternalTask Then
tData.UniqueID = t.UniqueID
tData.ID = t.ID
tData.Name = t.Name
tData.Start = t.Start
tData.Finish = t.Finish
tData.Text1 = t.Text1
tData.IsDirty = False
‘ CollectionのキーにUniqueIDを設定(一意性保証)
col.Add tData, “K_” & CStr(t.UniqueID)
End If
End If
Next i
End Sub
‘ ==============================================================================
% ロジックレイヤー:RAM上での純粋なデータ処理
‘ ==============================================================================
Private Sub ProcessTasksInMemory(ByRef col As Collection)
Dim vItem As Variant
Dim tData As TaskData
Dim i As Long
‘ CollectionはFor Eachで回す
For i = 1 To col.Count
tData = col(i)
‘ 独自ビジネスロジックの適用例:名前に特定の文字列が含まれる場合、Text1を更新
If InStr(1, tData.Name, “【重要】”, vbTextCompare) > 0 Then
If tData.Text1 <> “要警戒” Then
tData.Text1 = “要警戒”
tData.IsDirty = True ‘ 変更フラグを立てる
‘ Collection内の構造体を更新するための一時退避と再格納
‘ ※VBAのCollectionは値型UDTを直接置換できないため、一度削除して追加し直すか、
‘ パフォーマンス重視の場合は配列ベースへの転換を検討するが、今回はシンプルさを優先。
End If
End If
‘ 変更があった場合のみコレクションを更新
If tData.IsDirty Then
col.Remove i
col.Add tData, “K_” & CStr(tData.UniqueID), i ‘ 同じ位置に挿入
End If
Next i
End Sub
‘ ==============================================================================
‘ 書き戻しレイヤー:Collection -> Project DOM (Dirtyフラグ制御)
‘ ==============================================================================
Private Sub FlushMemoryToProject(ByVal col As Collection)
Dim vItem As Variant
Dim tData As TaskData
Dim t As Task
For Each vItem In col
tData = vItem
‘ 変更があった(IsDirty = True)タスクのみProjectのDOMを叩く
If tData.IsDirty Then
Set t = ActiveProject.Tasks.UniqueID(tData.UniqueID)
If Not t Is Nothing Then
t.Text1 = tData.Text1
End If
End If
Next vItem
End Sub
—
4. シニアエンジニアが押さえておくべき「メモリ管理と罠」
上記のコードは極めて高速に動作するが、VBAおよびProjectのアーキテクチャ特性を理解していないと、思わぬメモリリークや予期せぬ挙動を引き起こす。現場で必ず考慮すべき実践的知見を共有する。
① UDTのCollection内直接置換の罠
VBAの `Collection` オブジェクトに `User-Defined Type (UDT)` を格納した場合、`col(i) = tData` のような直接的な値の書き換えは構文エラーになる。
そのため、コード例にあるように一度 `Remove` してインディケータを指定して `Add` し直すか、あるいはパフォーマンスを極限まで追求する場合は、UDTではなく「配列(Array)」をメモリ上で直接操作するアーキテクチャに昇華させるべきである。数万件規模のタスクを扱う場合、Collectionのオーバーヘッドすら無視できない領域に達するため、その場合は `Variant型動的配列` をメモリキャッシュとして使用するのが正解となる。
② ProjectのUniqueIDとIDの混同による致命的バグ
Projectにおいて、行の移動や削除を行うと `ID`(画面上の行番号)は動的に変動する。
したがって、メモリキャッシュのキーや逆引きに `ID` を使用してはならない。必ず一意かつ不変の識別子である `UniqueID` を使用すること。DOMへ書き戻す際も `ActiveProject.Tasks.ID(id)` ではなく、必ず `ActiveProject.Tasks.UniqueID(uid)` を使わなければ、データの整合性が完全に破壊される。
③ 巨大プロジェクトにおけるメモリ肥大化
数万件のタスクと豊富なカスタムフィールドを持つEnterprise環境(Project Server / Project Online連携案件など)では、すべてのプロパティをUDTに詰め込むとVBAのヒープ領域を圧迫する可能性がある。
必要なプロパティ(カラム)のみを精査し、UDTの構造体を極限までスリム化(軽量化)することが、64bit環境であっても安定稼働させるための絶対条件である。
—
総括
Project VBAにおけるパフォーマンスチューニングの本質は、「COM境界の横断回数をいかにゼロに近づけるか」に尽きる。
今回解説したメモリ内キャッシュとCollection(あるいは配列)を活用した一括処理アプローチは、単なる「処理の高速化」にとどまらず、巨大なスケジュールを扱う企業システムにおいて、VBA製アドインの信頼性を実用レベルまで引き上げるための必須の設計思想である。
レガシーな枠組みの中でも、アーキテクチャの選択次第でモダンなシステムに匹敵するパフォーマンスを引き出すことは十分に可能だ。あなたのプロジェクトにもこの知見を直ちに組み込み、無駄な待ち時間から解放された真のエンジニアリングを実現してほしい。
