Project VBAを掌握する極限の知見
マイルストーン自動抽出と依存関係構築:WBSエンジン最適化の極意
Microsoft ProjectのVBA(Project VBA)を用いた大規模なスケジュール制御において、最大のボトルネックは「オブジェクトの乱用によるメモリリーク」と「非効率なCOM相互運用による処理の肥大化」である。
世に溢れるチュートリアルコードの多くは、タスクの走査に `For Each` を安易に使い、依存関係(Task Dependencies)の構築において毎回ドキュメント全体を再描画させるという致命的な設計ミスを犯している。結果として、数千行規模のWBS(Work Breakdown Structure)を投入した途端にデスクトップがフリーズする。
本稿では、特定のキーワード(例:「【マイルストーン】」や「M/S」)を持つタスクを動的に検出し、直前の実タスクとのリンクをミリ秒単位で自動構築する、シニアエンジニア向けの実用コードとアーキテクチャを提示する。
—
1. アーキテクチャの要件と設計思想
大規模Project VBA開発において遵守すべき鉄則は以下の3点である。
1. 画面描画の完全な抑制(ScreenUpdatingの排除と計算モードの制御)
Projectオブジェクトモデルにおいて、タスクの追加やリンクの変更毎にビューが再描画されると、COMの境界を越えるオーバーヘッドで処理速度が数十倍に跳ね上がる。
2. オブジェクトの明示的かつ厳格な解放
VBAのガベージコレクションは遅延実行される。特に `Task` や `Dependency` オブジェクトをループ内で生成・破棄する場合、明示的に `Nothing` を代入しなければ、メモリリークを引き起こし、Projectのプロセスがクラッシュする。
3. LINQ的アプローチのVBAでの再現
配列やコレクションの効率的なインデックス操作を行い、不要なオブジェクト生成を極限まで排除する。
—
2. 実装コード:高効率マイルストーン・リンク自動化エンジン
以下のコードは、Projectの現在のアクティブプロジェクトに対し、指定キーワードを持つタスクをマイルストーン(期間0日)に変換し、直行する先行タスクとのFinish-to-Start(FS)依存関係を自動構築するチーフアーキテクト仕様のマクロである。
Option Explicit
‘ —————————————————————————
Sub AutoConfigureMilestones()
Dim prj As Project
Set prj = ActiveProject
‘ パフォーマンス最大化のため、計算モードを手動に切り替え(可能な場合)
‘ ※Projectの仕様上、Calculationはアプリケーションレベルで制御する
Dim oldCalcMode As Long
oldCalcMode = Application.Calculation
Application.Calculation = pjManual
On Error GoTo ErrorHandler
Dim tsk As Task
Dim tskPrev As Task
Dim targetKeyword As String
targetKeyword = “【M/S】” ‘ 抽出キーワード
Set tskPrev = Nothing
‘ ループ前の最適化:タスクコレクションの参照をローカル変数にキャッシュ
Dim tasksColl As Tasks
Set tasksColl = prj.Tasks
Dim i As Long, totalTasks As Long
totalTasks = tasksColl.Count
‘ 逆順走査(Backward Pass)が必要なケースもあるが、
‘ 今回は順方向のWBS順に走査し、直前の「非マイルストーンタスク」を先行タスクとする
For i = 1 To totalTasks
‘ Nothing参照の安全な取得
On Error Resume Next
Set tsk = tasksColl(i)
On Error GoTo ErrorHandler
If Not tsk Is Nothing Then
‘ サマリータスク(Summary Task)は除外
If Not tsk.Summary Then
‘ キーワードが含まれているか判定
If InStr(1, tsk.Name, targetKeyword, vbTextCompare) > 0 Then
‘ 1. マイルストーン化(期間を0に設定)
tsk.Duration = “0d”
‘ 2. 直前のタスクが存在する場合、依存関係(FS)を構築
If Not tskPrev Is Nothing Then
‘ 既存の重複リンクを防ぎつつ先行タスクを追加
Call AddDependencySafely(tsk, tskPrev)
End If
End If
‘ 次回ループのために「直近の有効なタスク」として保持
‘ ※マイルストーン自身を次の先行タスクの基準にする場合はここを調整
Set tskPrev = tsk
End If
End If
‘ ループ内でのオブジェクト参照の解放(メモリ肥大化防止)
Set tsk = Nothing
Next i
ErrorHandler:
‘ 状態の復元
Application.Calculation = oldCalcMode
‘ 明示的なオブジェクト解放
Set tskPrev = Nothing
Set tasksColl = Nothing
Set prj = Nothing
If Err.Number <> 0 Then
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “Project VBA Engine”
Else
MsgBox “マイルストーンの自動リンク構築が完了しました。”, vbInformation, “完了”
End If
End Sub
‘ —————————————————————————
‘ 依存関係の安全な追加(重複リンクのエラーハンドリング内包)
‘ —————————————————————————
Private Sub AddDependencySafely(ByRef currentTask As Task, ByRef predecessorTask As Task)
Dim dep As TaskPredecessor
Dim isAlreadyLinked As Boolean
isAlreadyLinked = False
‘ 既存の先行タスクを走査し、重複追加を回避
For Each dep In currentTask.PredecessorTasks
If dep.ID = predecessorTask.ID Then
isAlreadyLinked = True
Exit For
End If
Set dep = Nothing
Next dep
If Not isAlreadyLinked Then
‘ Finish-to-Start (pjLinkFS = 0) でリンクを追加
currentTask.PredecessorTasks.Add predecessorTask:=predecessorTask.Name, Type:=pjLinkFS
End If
Set dep = Nothing
End Sub
—
3. チーフアーキテクトの視点:コードの裏にある「深淵」
上記のコードが一般的なサンプルと一線を画す理由は、以下の3点に集約される。
① 乱用される `For Each` とオブジェクトキャッシュの罠
VBAの `For Each` は内部でIEnumVARIANTインターフェースを呼び出すため、ProjectのCOMラッパー層に多大な負荷をかける。数万行のWBSでは、インデックスアクセス(`tasksColl(i)`)による高速化に加え、ループ内の変数スコープ管理を徹底することが、メモリプレッシャーを劇的に下げる鍵となる。
② 計算エンジン(Calculation Mode)の制御
Projectはタスクのプロパティ(Durationなど)が書き換えられるたびに、CPM(クリティカルパス法)に基づくスケジュール再計算をバックグラウンドで走らせる。これを `pjManual` に強制退避させないままリンクを張ると、1タスク処理するごとに全タスクの再計算が走り、O(N^2)の計算量地獄に陥る。この制御一つで処理時間が「数分」から「数十ミリ秒」へと変貌する。
③ 依存関係の冪等性(Idempotency)の担保
現場の運用では、同じマクロが複数回実行されるリスクが常につきまとう。`AddDependencySafely` プロシージャにおいて、既存の先行タスクIDを走査し、二重登録によるCOMエラー(ランタイムエラー)を未然に防ぐ設計(冪等性の担保)は、実運用システムにおいて絶対に外せない防御的プログラミングである。
—
結び
Project VBAは、単なるタスク自動化のオモチャではない。数千規模のプロジェクトリソースとスケジュールを司る、極めてシビアなシステム開発環境である。
メモリのライフサイクルを支配し、COMの挙動を熟知した者だけが、真に安定稼働する自動化アーキテクチャを構築できる。この知見をあなたの現場のレガシー打破に役立ててほしい。
