【実務・中級編】【上級者向け】Project VBAにおける「コレクション」操作の高速化:Taskオブジェクトのキャッシュ戦略 – Project VBA解析バイブル

スポンサーリンク

【上級者向け】Project VBAにおける「コレクション」操作の高速化:Taskオブジェクトのキャッシュ戦略

開発プロジェクトの規模が拡大し、管理すべきタスクが数千、数万規模に達したとき、多くのVBAエンジニアが同じ壁にぶ当たる。

「マクロの実行に数十分かかる。終わらない。」

WBSの再構築、前提条件(先行タスク・後続タスク)の自動設定、クリティカルパスの動的解析。これらを愚直に実装すると、VBAは途端に重い足取りになる。
原因は明確だ。COMの境界を越えるコスト、そして`ActiveProject.Tasks`という巨大なコレクションへの無秩序なアクセスが、メモリとCPUをドレインしているのだ。

今回は、数千タスクを飲み込む巨大プロジェクトであっても、一瞬で処理を完結させるための「Taskオブジェクト・キャッシュ戦略」を伝授する。オブジェクトのライフサイクルとメモリの裏側を理解し、現場で即座に使える堅牢なプロダクションコードを手に入れてほしい。

—

なぜ、あなたのVBAは遅いのか?(根本原因の解剖)

MS ProjectのVBAにおいて、最もやってはいけないアンチパターンはこれだ。

‘ 【最悪のアンチパターン】ループのたびにCOMオブジェクトにアクセス
Dim i As Long
For i = 1 to 5000
ActiveProject.Tasks(i).Text1 = “処理済み”
ActiveProject.Tasks(i).Start = Date
Next i

1. COM境界を超える「マーシャリング・コスト」

VBA(クライアント)からMS Projectの内部エンジン(COMコンポーネント)へアクセスするたびに、プロセス間通信またはスレッド境界を越える「マーシャリング」が発生する。たった1回のアクセスは数ミリ秒であっても、5000回繰り返せば数秒から数十秒のロスとなる。

2. コレクションのインデックス参照の罠

`Tasks(i)` と指定した瞬間、Projectは内部リストを先頭から順に走査し、該当するインデックスのオブジェクトを探しに行っている(O(N)の計算量)。これをループ内で回すと、全体の計算量は $O(N^2)$ に跳ね上がる。

【解決策】メモリ上への「キャッシュ」と「ディクショナリ」の活用

一度アクセスしたTaskオブジェクトをVBAのメモリ空間(配列や`Scripting.Dictionary`)に丸ごと取り込み(キャッシュ)、VBA側で高速に演算を行った後、一括して書き戻す。これが大規模プロジェクトを制する唯一の解である。

—

堅牢な設計:Scripting.DictionaryによるIDマッピング

数千のタスクを効率よく扱うためには、行番号(ID)やユニークID(UniqueID)ではなく、「タスクのUniqueIDをキーにしたDictionary」を構築するのが最も堅牢かつ高速である。行番号はタスクの挿入・削除で狂うが、UniqueIDは不変だからだ。

アーキテクチャの全体像

1. フェーズ1(読み込み): `ActiveProject.Tasks` を一度だけ走査し、`UniqueID` をキー、`Task` オブジェクトを値として `Scripting.Dictionary` に格納する。
2. フェーズ2(高速メモリ処理): ディクショナリ経由でO(1)の速度でオブジェクトにアクセスし、前提条件やカスタムフィールドの書き換えをメモリ上で完結させる。
3. フェーズ3(一括書き戻し・解放): 必要に応じてProjectへ反映させ、オブジェクト参照を完全に解放する。

—

プロダクションコード例:依存関係自動設定エンジン

以下のコードは、数千タスクの前提条件(先行タスク)を一括で高速に結びつけるための実用モジュールだ。エラーハンドリング、メモリの確実な解放、そして実務に耐えうるパフォーマンスチューニングを施している。

Option Explicit

‘ メインエントリーポイント
Public Sub ExecuteHighSpeedTaskMapping()
Dim startTime As Double
startTime = Timer

‘ 1. エラーハンドリングと画面描画の停止(描画停止はパフォーマンス向上の鉄則)
Application.ScreenUpdating = False
On Error GoTo ErrorHandler

Dim tskDict As Object
Set tskDict = CreateObject(“Scripting.Dictionary”)

Dim tsk As Task
Dim projTasks As Tasks
Set projTasks = ActiveProject.Tasks

‘ ==========================================
‘ フェーズ1: キャッシュ構築(O(N)の走査を1回のみに限定)
‘ ==========================================
Dim i As Long
Dim totalTasks As Long
totalTasks = projTasks.Count

For i = 1 To totalTasks
Set tsk = projTasks(i)
If Not tsk Is Nothing Then
‘ UniqueIDをキーとしてDictionaryに格納
If Not tskDict.Exists(tsk.UniqueID) Then
tskDict.Add tsk.UniqueID, tsk
End If
End If
Next i

Debug.Print “キャッシュ構築完了: ” & tskDict.Count & ” タスク (” & Format(Timer – startTime, “0.00秒”) & “)”

‘ ==========================================
‘ フェーズ2: メモリ上での高速依存関係(前提条件)設定
‘ 例として、「UniqueID: 10」のタスクの先行に「UniqueID: 5」を設定する処理
‘ ==========================================
Dim targetTask As Task
மொழி Dim predecessorTask As Task

‘ ディクショナリ経由でオブジェクトをO(1)で取得
If tskDict.Exists(10) And tskDict.Exists(5) Then
Set targetTask = tskDict(10)
Set predecessorTask = tskDict(5)

‘ 前提条件(Link)の追加
‘ すでにリンクが存在するかどうかのチェックもメモリ上で行える
Dim lng As Dependency
Dim isAlreadyLinked As Boolean
isAlreadyLinked = False

For Each lng In targetTask.Predecessors
If lng.FromTask.UniqueID = predecessorTask.UniqueID Then
isAlreadyLinked = True
Exit For
End If
Next lng

If Not isAlreadyLinked Then
targetTask.Predecessors.Add predecessorTask
End If
End If

‘ (ここに数千行に及ぶ複雑な依存関係・WBS自動構築ロジックを記述する)

‘ 終了処理
Application.ScreenUpdating = True
MsgBox “処理が正常終了しました。実行時間: ” & Format(Timer – startTime, “0.00秒”) & “秒”, vbInformation
Exit Sub

ErrorHandler:
Application.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical

‘ メモリリーク防止のためのクリーンアップ
Set tskDict = Nothing
Set projTasks = Nothing
End Sub

—

現場で絶対に外せない3つの実務的注意点

プロトタイプでは動くが、実際の業務環境(サーバー連携や他システムからのデータインポート)でクラッシュするコードには共通の欠陥がある。以下の3点を必ず遵守せよ。

1. 画面描画の抑制(`ScreenUpdating = False`)

MS Projectは、Taskオブジェクトのプロパティが書き換わるたびにガントチャートやネットワーク図の再描画(レイアウト計算)を走らせようとする。これが激重の原因になる。処理の最初に `Application.ScreenUpdating = False` をかけ、終了時に必ず `True` に戻すこと。エラー時にも戻るように `On Error GoTo` のハンドリングが必須である。

2. COMオブジェクトの参照解放(メモリリーク対策)

VBAはガベージコレクションの挙動が特殊である。特に `Scripting.Dictionary` に格納されたオブジェクトや、ループ内で生成・取得したオブジェクト参照は、処理の最後に明示的に `Set xxx = Nothing` とし、ディクショナリ自体も `tskDict.RemoveAll` および `Set tskDict = Nothing` で解放しなければ、Projectプロセスがメモリを食いつぶしたまま残留する。

3. データベース・外部ファイル連携時のトランザクション思想

ExcelやSQL Serverからタスクデータをインポートし、VBScriptやProject VBA側で一括構築する際、途中でエラーが起きた場合のロールバック機構がないと、中途半端に依存関係が結ばれた「ゴミデータ」が残る。
大規模な変更を行う場合は、処理の前にプロジェクトのバックアップ(または一時的なUndoバッファの意識)を持ち、トランザクション的な堅牢性を担保する設計を心がけよう。

—

チーフアーキテクトからの提言

「動けばいい」というコードは、タスクが100個のときには許される。しかし、あなたが向き合っているのは企業の命運を握る大規模プロジェクトのWBSだ。

オブジェクトのライフサイクルを理解し、無駄なCOMアクセスを排除する「キャッシュ戦略」を取り入れた瞬間から、あなたのVBAは「おもちゃのスクリプト」から「堅牢なエンタープライズ・ツール」へと進化する。

機敏に動き、絶対に破綻しないコードを書け。それが、プロの業務自動化エンジニアのプライドだ。

タイトルとURLをコピーしました