制御不能なスケジュールを、数学的必然へと変える――Project VBAにおける制約条件(Constraint)の完全掌握
プロジェクト管理の世界において、スケジュールは「願望」であってはならない。それは、依存関係とリソースの制約に基づいた「数学的必然」であるべきだ。
しかし、実務の現場ではどうだろうか。ユーザーが不用意にタスクに「開始前指定」や「終了後指定」といった制約(Constraint)を打ち込み、プロジェクト全体のクリティカルパスを破壊している光景を、私は何度見てきたことか。
本稿では、初心者向けのテーマである「タスクの制約条件を判定し、デフォルト(ASAP: As Soon As Possible)へ置換する」という処理を題材に、単なるコードの書き方を超えた、エンジニアリングとしてのスケジューリング制御の真髄を解説する。
1. 概念の再定義:制約条件(Constraint)とは何か
多くの初学者は、制約条件を単なる「カレンダー上の設定」だと誤解している。だが、アーキテクトの視点では、制約条件は「スケジューリングエンジン(CPM: Critical Path Method)に対する、論理的強制力」である。
MS Projectの計算エンジンは、タスク間の「先行・後続関係(Dependency)」を基に、フリーフロート(余裕期間)を計算する。しかし、タスクに `pjTaskConstraintStartNoEarlierThan`(開始前指定)などの制約が付与された瞬間、そのタスクは論理的な依存関係から切り離され、エンジンによる動的な計算が阻害される。
これが、大規模プロジェクトにおいて「なぜかスケジュールが動かない」「計算が合わない」という怪現象を引き起こす正体だ。我々の任務は、これらの「不純な制約」を排除し、プロジェクトを純粋なロジックへと回帰させることにある。
2. パフォーマンスの罠:計算エンジンの制御
VBAでタスクをループ処理する際、最も致命的なミスは「プロパティを変更するたびに、MS Projectの再計算エンジンを起動させてしまうこと」だ。
タスクが1,000個あるプロジェクトで、1つずつ制約を変更し、その都度エンジンが再計算を行えば、計算量は $O(n^2)$ に近づき、アプリケーションは実質的にフリーズする。
これを防ぐためには、処理の開始時に計算モードを `Manual`(手動)に切り替え、全処理が完了した後に一度だけ `Automatic`(自動)に戻す。これは、レガシーなシステム間連携においても、トランザクションの整合性を保つための鉄則である。
3. 実装:最適化された制約置換ロジック
以下に、堅牢でパフォーマンスを考慮した実装例を示す。単なる `For Each` ではなく、オブジェクトのライフサイクルとエラーハンドリングを考慮したプロフェッショナルな構造だ。
Option Explicit
''' <summary>
''' プロジェクト内の全てのタスクを走査し、
''' ASAP(制約なし)以外の制約を持つタスクを、デフォルトの制約に一括置換する。
''' </summary>
Public Sub ResetTaskConstraintsToASAP()
Dim pjApp As MSProject.Application
Dim targetProj As MSProject.Project
Dim t As MSProject.Task
Dim changeCount As Long
Dim calcModeBefore As MSProject.Calculation
' オブジェクトの参照取得
Set pjApp = MSProject.Application
Set targetProj = pjApp.ActiveProject
' 1. 計算モードのバックアップと一時停止
' プロパティ変更のたびに再計算が走るのを防ぐ(極めて重要)
calcModeBefore = targetProj.Calculation
targetProj.Calculation = pjCalculationManual
On Error GoTo ErrorHandler
changeCount = 0
' 2. タスクコレクションの走査
' コレクションの要素を直接操作する場合、インデックスによるループも検討すべきだが、
' 読み取り専用の判定であれば For Each が最も直感的かつ高速である。
For Each t In targetProj.Tasks
' Nothingチェック(削除されたタスクや空のインデックス対策)
If Not t Is Nothing Then
' 3. 制約条件の判定
' pjTaskConstraintAsSoonAsPossible (0) 以外を検知
If t.ConstraintType <> pjTaskConstraintAsSoonAsPossible Then
' 制約の置換
t.ConstraintType = pjTaskConstraintAsSoonAsPossible
' 制約に伴う日付の不整合を防ぐため、日付もリセットすることを推奨
' (制約を外しても、日付が固定されたままだと実質的な制約として残るため)
' t.Start = t.Start ' 必要に応じて論理的な再計算に任せる
changeCount = changeCount + 1
End If
End If
Next t
' 4. 計算モードの復元と強制再計算
targetProj.Calculation = calcModeBefore
targetProj.Calculate ' 最後に一度だけエンジンを回す
MsgBox "処理完了。" & vbCrLf & _
"置換されたタスク数: " & changeCount, vbInformation, "Architect Report"
CleanUp:
' 5. オブジェクトの明示的解放
' COMオブジェクトのライフサイクルを管理し、メモリリーク(ゾンビプロセス)を防ぐ
Set t = Nothing
Set targetProj = Nothing
Set pjApp = Nothing
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "System Error"
Resume CleanUp
End Sub
4. 極限の知見:メモリ管理とCOMオブジェクトのライフサイクル
シニアエンジニアとして、コードの背後にある「メモリの挙動」を無視することはできない。
オブジェクトの解放(Destruction)
VBAは、変数のスコープが外れれば自動的に参照カウントを減らす。しかし、大規模な自動化ツールをExcelや外部アプリケーションから呼び出す場合、`Set t = Nothing` による明示的な解放を怠ると、MS Projectのプロセス(`MSProject.exe`)がタスクマネージャーに残り続ける「ゾンビプロセス」を引き起こす。これは、システム管理者が最も嫌う、リソースの枯渇原因だ。
Windows APIとの連携(将来的な拡張)
もし、この処理を数万タスク規模の超巨大プロジェクトで行う場合、VBAの標準的な `Timer` では精度が足りない。その場合は、`kernel32` の `GetTickCount64` 等を呼び出し、プロセスの実行時間をマイクロ秒単位で計測・プロファイリングすることで、ボトルネックが「オブジェクトへのアクセス」にあるのか、「エンジンによる計算」にあるのかを切り分ける必要がある。
5. まとめ:エンジニアとしての規律
「制約なし」の状態を作ることは、単なるデータのクリーニングではない。それは、プロジェクトを「決定論的なモデル」へと昇華させるプロセスである。
1. エンジンの制御を忘れるな: `Calculation = Manual` は、パフォーマンスにおける絶対的な規律である。
2. オブジェクトのライフサイクルを統制せよ: `Nothing` への代入は、システムの安定性を担保する作法である。
3. 論理的整合性を追求せよ: 制約を外した後の日付の挙動までを見据えてこそ、真のアーキテクトである。
技術は、単なる手段に過ぎない。その手段を、いかに正確に、いかに効率的に、いかに美しく制御するか。そこにこそ、プロフェッショナルの価値が宿るのだ。
