スケジュール計算エンジンを掌握せよ:Project VBAによる制約条件の完全解体とASAP化の極意
大規模プロジェクトの管理において、MS Projectのスケジュールエンジン(CPM: Critical Path Method)は、時に制御不能な混沌(カオス)を招く。PMOやシステム管理者が直面する最も厄介な問題の一つは、ユーザーが手動で設定した「制約条件(Constraints)」が、計算ロジックの背後で複雑に絡み合い、プロジェクト全体の論理的な流れを遮断してしまうことだ。
「なぜかクリティカルパスが正しく計算されない」「タスクを動かそうとしても日付が固定されて動かない」――これらの問題の多くは、タスクに付与された `Must Start On` や `Start No Earlier Than` といった制約が、エンジンの自由な計算を阻害していることに起因する。
本稿では、プロジェクトを一度完全に「フラット」な状態、すなわち全ての制約を排除し、純粋な論理関係(Predecessors/Successors)のみに基づいた ASAP (As Soon As Possible) 状態へと強制的に回帰させるための、極限まで最適化された実装手法を解説する。これは単なる初心者向けのスクリプトではない。大規模環境におけるオブジェクトのライフサイクルと、計算エンジンのオーバーヘッドを制御するための、アーキテクトとしての知見である。
1. スケジュールエンジンの挙動と制約の正体
MS Projectの計算エンジンは、タスク間の依存関係をグラフ理論に基づき解いていく。しかし、`ConstraintType` が `pjTaskConstraintNone`(制約なし)以外に設定されている場合、エンジンはそのタスクに対して「強制的なアンカー(錨)」を打ち込む。
このアンカーは、計算アルゴリズムが算出する「論理的な開始日」よりも優先される。結果として、スケジュールは歪み、リソースの平準化も、クリティカルパスの特定も、すべてが計算不能な不整合へと陥る。
プロジェクトを再構築する際、我々が最初に行うべきは、この「意図せぬアンカー」を物理的にすべて引き抜く作業である。
2. 極限の最適化:計算負荷の制御
数千、数万のタスクを持つプロジェクトにおいて、一つ一つのタスクの制約を書き換えるたびに、Projectの自動再計算(Automatic Calculation)が走るとどうなるか。エンジンの計算負荷は指数関数的に増大し、アプリケーションは応答不能(Not Responding)に陥る。
プロフェッショナルな実装では、以下の3点を鉄則とする。
1. Calculation Modeの制御: 計算モードを `pjManual` に切り替え、一括処理が終わるまで計算を停止させる。
2. Screen Updatingの抑制: UIの描画更新を止めることで、描画リソースを計算処理へ回す。
3. オブジェクト参照の最小化: ループ内でのオブジェクト取得コストを最小限に抑える。
3. 実装:プロジェクト・フラットニング・スクリプト
以下に、制約条件を一括削除し、プロジェクトを完全なASAP状態へと強制移行させる、堅牢なVBAコードを示す。
Option Explicit
''' <summary>
''' プロジェクト内の全タスクから制約条件を排除し、
''' 全てのタスクを ASAP (As Soon As Possible) 状態に強制リセットする。
''' </summary>
Public Sub ForceProjectToASAP()
Dim t As Task
Dim proj As Project
Dim taskCount As Long
Dim startTime As Double
' 高精度なパフォーマンス計測のための開始時刻
startTime = Timer
Set proj = ActiveProject
' --- 1. エンジンの保護 (Optimization Phase) ---
' 自動計算を停止。これを怠ると、タスク変更のたびに再計算が走り、処理時間が数千倍に膨れ上がる。
proj.Calculation = pjManual
' 画面更新の停止(MS ProjectではExcelほど劇的ではないが、COM操作のオーバーヘッド削減に寄与する)
' ※ 環境により利用不可な場合はスキップされるが、標準的な作法として記述
On Error Resume Next
Application.ScreenUpdating = False
On Error GoTo 0
On Error GoTo ErrorHandler
' --- 2. タスクの走査と制約の解体 (Execution Phase) ---
' Taskコレクションを直接ループ。大規模プロジェクトでは、インデックスによるアクセスよりも
' For Each による列挙の方が、COMオブジェクトの内部ポインタ移動において効率的な場合が多い。
taskCount = 0
For Each t In proj.Tasks
' TaskオブジェクトがNothingでないこと(空行の回避)を確認
If Not t Is Nothing Then
' 既に制約がないタスクはスキップする(不要な書き込みによるDirtyフラグの発生を防ぐ)
If t.ConstraintType <> pjTaskConstraintNone Then
' 制約の種類を「制約なし」へ変更
t.ConstraintType = pjTaskConstraintNone
' 重要:ConstraintDateに残留している日付データをクリアする。
' 型をNoneに変えても、内部的な日付データがメモリに残ることがあり、
' レガシーなAPI連携時に予期せぬ挙動を招くリスクを排除するため。
t.ConstraintDate = DateSerial(1900, 1, 1)
taskCount = taskCount + 1
End If
End If
Next t
' --- 3. エンジンの再起動 (Finalization Phase) ---
' 全ての制約を排除した後、一度だけ再計算を強制実行する。
proj.Calculate
' 計算モードを自動に戻す
proj.Calculation = pjAutomatic
' 画面更新を再開
Application.ScreenUpdating = True
MsgBox "処理完了。" & vbCrLf & _
"解除された制約数: " & taskCount & vbCrLf & _
"実行時間: " & Format(Timer - startTime, "0.00") & " 秒", vbInformation, "Architect's Report"
ExitSub:
' メモリリーク防止のため、オブジェクト参照を明示的に解放
Set t = Nothing
Set proj = Nothing
Exit Sub
ErrorHandler:
' エラー発生時も、エンジンの計算モードを自動に戻さないと、ユーザーが「動かない」と混乱するため、必ず復旧させる
proj.Calculation = pjAutomatic
Application.ScreenUpdating = True
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "System Error"
Resume ExitSub
End Sub
4. 深層:シニアエンジニアが意識すべき「メモリとCOMの境界線」
このスクリプトを、Excelや外部の .NET アプリケーション(C#等)から COM Interop を介して呼び出す場合、さらなる注意が必要だ。
オブジェクトのライフサイクル
VBA内部では `Set t = Nothing` を行うことで参照カウントを減らしているが、これは「オブジェクトの破棄」ではなく「参照の解除」に過ぎない。MS Projectのプロセスがメモリに居座り続ける(ゾンビプロセス化する)のを防ぐには、呼び出し側のホストアプリケーションにおいても、`Project.Application` オブジェクトを適切に `Marshal.ReleaseComObject`(.NETの場合)などで解放しなければならない。
残留するConstraintDateへの洞察
コード内で `t.ConstraintDate = DateSerial(1900, 1, 1)` と記述した理由を理解しているだろうか?
`ConstraintType` を `pjTaskConstraintNone` に変更しても、タスクオブジェクトのプロパティバッファには、以前の `ConstraintDate` が「隠れた状態」で残存することがある。
これは、Projectのバージョンアップや、XML/MSP形式でのエクスポート・インポート時に、予期せぬ日付の再適用を引き起こす要因となる。「型を変えたら、データも初期化する」。これは、レガシーシステムを保守する上で不可欠な、エンジニアの「防衛的プログラミング」の極致である。
5. 結論
プロジェクトの「完全ASAP化」は、単なるクレンジング作業ではない。それは、スケジュール計算エンジンというブラックボックスに対し、人間が設定した不条理な制約を取り払い、数学的な論理を取り戻させる儀式である。
大規模システムを設計する者は、常に「最悪のケース(ユーザーによる乱雑な入力)」を想定し、それを一撃で、かつ低負荷でリセットできる手段を持っておかなければならない。このスクリプトはそのための、最小にして最強の武器となるだろう。
