マクロの記録から「設計」へ。Project VBAで実現する「制約条件シミュレーション」の極意
こんにちは。現場の自動化を支えるアーキテクトです。
「マクロの記録」を使って、なんとか作業を自動化できた。そんな喜びを感じた時期が、僕にもありました。でも、現場で本当に求められるのは「記録された操作」ではなく、「プロジェクトの未来を予測するロジック」です。
例えば、プロジェクトマネージャーからこんな無茶振りをされたことはありませんか?
「このタスクの開始を2日遅らせたら、クリティカルパス(最重要経路)はどう変わるかな? 今の計画を壊さずに、パッと確認したいんだけど」
これを手動でやるのは危険です。一度制約(Constraint)を書き換えてしまうと、元のスケジュールに戻すのが大変ですし、計算結果が変わるたびに「あれ、さっきと何が違うんだっけ?」と混乱してしまいます。
今日は、そんな悩みを一撃で解決する「制約条件の変更がクリティカルパスに与える影響を事前にシミュレーションするツール」の作り方を解説します。ここをマスターすれば、あなたは単なる「マクロ使い」から、プロジェクトの動態を制御する「エンジニア」へとステップアップできますよ。
1. 本質を知る:MS Projectの「計算エンジン」と「制約」の関係
コードを書く前に、まずMS Projectの「脳」がどう動いているかを理解しましょう。ここを飛ばすと、どんなに綺麗なコードを書いても、意図しない計算結果に泣かされることになります。
制約条件(Constraint)とは何か?
MS Projectのタスクには、デフォルトで「ASAP(可能な限り早く)」という制約がかかっています。これは「先行タスクが終わったら、すぐに始めていいよ」という自由な状態です。
しかし、ここに「Start No Earlier Than(〜より前には開始しない)」などの制約を加えると、エンジンの計算ルールが変わります。
- ASAP (pjTaskStartASAP): 自由なスケジュール。
- Start No Earlier Than (pjTaskStartNoEarlierThan): 特定の日付まで「待機」を強制する。
なぜシミュレーションが必要なのか?
制約を変更すると、そのタスクの「余裕(Slack)」が削られ、結果としてクリティカルパス(Total Slackが0のタスクの連なり)が別のルートへ移動することがあります。
我々が作るツールの設計思想はこうです。
1. 現在の状態をメモリに保存する(バックアップ)
2. 仮の制約を適用して計算(Calculate)を実行する
3. クリティカルパスの変化を特定・レポートする
4. 元の状態に完璧に復元する(ロールバック)
この「変更して、見て、戻す」というサイクルこそが、プロフェッショナルなシミュレーションの核心です。
2. 実装:シミュレーション・エンジンのコード
それでは、実際に動くコードを見ていきましょう。今回は「特定のタスクの制約を変更したとき、どのタスクが新たにクリティカルパスに加わるか」を判定する関数を作成します。
Option Explicit
''' <summary>
''' 特定のタスクの制約を変更した際の影響をシミュレーションする
''' </summary>
''' <param name="targetTaskId">シミュレーション対象のタスクID</param>
''' <param name="newConstraint">適用したい新しい制約タイプ</param>
Sub SimulateConstraintImpact(targetTaskId As Long, newConstraint As PjConstraintType)
Dim targetTask As Task
Dim t As Task
Dim originalConstraint As PjConstraintType
Dim originalStart As Date
Dim criticalPathChanges As String
Dim impactFound As Boolean
' 1. 対象タスクの存在確認
On Error Resume Next
Set targetTask = ActiveProject.Tasks(targetTaskId)
On Error GoTo 0
If targetTask Is Nothing Then
MsgBox "指定されたタスクIDが見つかりません。", vbCritical
Exit Sub
End If
' 2. 【重要】現在の状態をメモリに退避(バックアップ)
' オブジェクトのプロパティを個別に保存しておくのが、最も安全なロールバック手法です。
originalConstraint = targetTask.ConstraintType
originalStart = targetTask.Start
Debug.Print "--- シミュレーション開始 ---"
Debug.Print "対象タスク: " & targetTask.Name
Debug.Print "元の制約: " & GetConstraintName(originalConstraint)
' 3. 仮の制約を適用
targetTask.ConstraintType = newConstraint
' 4. 【重要】計算エンジンの再実行
' プロパティを変えただけでは計算は走りません。明示的にCalculateを呼び出す必要があります。
ActiveProject.Calculate
' 5. クリティカルパスの変化を調査
' 変更後のプロジェクト全体をスキャンし、クリティカルなタスクを特定します。
criticalPathChanges = "新たにクリティカルになったタスク:" & vbCrLf
impactFound = False
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
' Task.Critical プロパティは、そのタスクがクリティカルパス上にあるかをBooleanで返します
If t.Critical Then
criticalPathChanges = criticalPathChanges & " - " & t.Name & " (開始: " & t.Start & ")" & vbCrLf
impactFound = True
End If
End If
Next t
' 6. 結果の表示
If impactFound Then
MsgBox "【シミュレーション結果】" & vbCrLf & _
"制約変更により、以下のタスクがクリティカルパスに含まれます:" & vbCrLf & vbCrLf & _
criticalPathChanges, vbInformation, "影響レポート"
Else
MsgBox "制約を変更しても、クリティカルパスに変化はありませんでした。", vbInformation
End If
' 7. 【極めて重要】元の状態に完全復元(ロールバック)
' シミュレーションが終わったら、必ず元のプロジェクト状態に戻します。
' これを忘れると、ユーザーの元々の計画を破壊してしまいます。
targetTask.ConstraintType = originalConstraint
' 注意:日付を直接戻す場合は、制約との整合性に注意が必要ですが、
' 基本的にはConstraintTypeを戻すことで、エンジンの計算により元の開始日に戻ります。
ActiveProject.Calculate
Debug.Print "--- シミュレーション終了(復元完了) ---"
End Sub
''' <summary>
''' 制約タイプを読みやすい文字列に変換するヘルパー関数
''' </summary>
Function GetConstraintName(cType As PjConstraintType) As String
Select Case cType
Case pjTaskStartASAP: GetConstraintName = "ASAP (可能な限り早く)"
Case pjTaskStartNoEarlierThan: GetConstraintName = "Start No Earlier Than (〜より前には開始しない)"
Case pjTaskStartNoLaterThan: GetConstraintName = "Start No Later Than (〜までに開始する)"
Case Else: GetConstraintName = "その他"
End Select
End Function
''' <summary>
''' テスト実行用メインルーチン
''' </summary>
Sub RunTestSimulation()
' 例:タスクID 5 の制約を「Start No Earlier Than」に変更した場合のシミュレーション
' 実際の日付は、プロジェクトの状況に合わせて調整してください。
Call SimulateConstraintImpact(5, pjTaskStartNoEarlierThan)
End Sub
3. プロの視点:コードの解説と「陥りやすい罠」
このコードには、初学者が躓きやすいポイントと、エンジニアが必ず意識する「安全性」が詰まっています。
① `ActiveProject.Calculate` の重み
多くの初心者は、プロパティ(`targetTask.ConstraintType`など)を書き換えただけで、計算が終わったと思い込みます。しかし、MS Projectは「命令を受けた瞬間に計算する」のではなく、「計算が必要になったタイミングで計算する」という、いわゆる遅延評価に近い動きをします。
シミュレーション結果を正確に取得するには、`Calculate` メソッドを明示的に呼び出し、計算エンジンを強制的に回すことが不可欠です。
② ロールバック(復元)の哲学
このツールの最も重要な部分は、後半の「元の状態に戻す」処理です。
業務自動化において、「元のデータを壊さないこと」は「新しい機能を実装すること」よりも100倍重要です。もし、シミュレーション中にエラーが発生してコードが止まってしまった場合、制約が書き換わったままになってしまいます。
> アドバイス:
> より堅牢なツールを作るなら、`On Error GoTo ErrorHandler` を使い、エラーが発生した場合でも必ず「復元処理」を通ってから終了するように設計しましょう。
③ パフォーマンスの考慮
上記のコードでは `For Each t In ActiveProject.Tasks` を使って全タスクをループしています。タスク数が数百件程度なら問題ありませんが、数万件規模のプロジェクトでは、このループが重くなります。
実務では、`ActiveProject.CriticalTasks` コレクションを直接利用するなど、オブジェクトモデルを駆使して「必要なものだけを触る」工夫が求められます。
4. まとめ:次の一歩へ
今回のシミュレーションツールは、単なる「制約の変更」を自動化したものではありません。「もし〜だったら(What-if)」という、プロジェクト管理の本質的な問いに答えるための道具です。
1. バックアップを取る
2. エンジンを回す
3. 結果を読み取る
4. 元に戻す
この4ステップは、あらゆる業務自動化のフレームワークに通じる鉄則です。
ここをクリアすれば、Project VBAの基本はバッチリですよ。次は、このシミュレーション結果をExcelに出力したり、グラフ化したりして、さらに高度な「意思決定支援ツール」へと進化させてみてください。
君の書くコードが、プロジェクトの混乱を防ぎ、チームを救う武器になることを期待しています。頑張ってくださいね!
