【実務・中級編】【初心者向け】「制約なし」タスクのデフォルト挙動を理解する:プロジェクト開始日との連動ロジック – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

【Project VBA 極限解説】「制約なし」の罠を回避せよ:ASAPロジックとプロジェクト開始日の連動性を制御する

プロジェクト管理ツールをVBAで自動化しようとする者が、必ずと言っていいほど陥る「底なし沼」がある。

それは、「タスクの制約条件(Constraint)を理解せずに、日付(Start/Finish)だけを操作しようとすること」だ。

「制約なし(None)に設定したから、タスクは自由に動くはずだ」……もし君がそう考えているなら、今すぐその認識を捨てろ。MS Projectのスケジューリングエンジンにおいて、「制約なし」とは「自由」を意味しない。それは「As Soon As Possible (ASAP) —— プロジェクト開始日または先行タスクの終了直後に、可能な限り早く開始せよ」という、極めて強力なデフォルトの制約を意味するのだ。

本稿では、初心者が陥る「意図しない日付のズレ」の正体を解明し、プロフェッショナルとして、堅牢なスケジューリング自動化ツールを設計するための極限の知見を伝授する。

1. 誤解の源泉:`pjTaskConstraintNone` の真実

MS Projectのタスクには `ConstraintType` というプロパティが存在する。多くの開発者は、制約を設けない場合に `pjTaskConstraintNone` を選択する。

しかし、エンジニアリングの観点から見れば、これは「制約がない」状態ではない。「ASAP(可能な限り早く)という制約が適用されている」状態なのだ。

スケジューリングエンジンの決定論的挙動

ASAP状態のタスクの開始日は、以下のロジックで決定される。

$$Task\_Start = \max(Project\_Start, \max(Predecessors\_Finish\_Dates))$$

つまり、タスクの開始日は、「プロジェクト全体の開始日」と「先行タスクの終了日」の、より遅い方に強制的に引き寄せられる。

ここで恐ろしい副作用が発生する。プロジェクトの開始日(Project Start Date)を1日変更しただけで、制約なしとされた全タスクがドミノ倒しのようにシフトするのだ。 これが、データベースから日付を流し込んでも、Projectを開いた瞬間に日付が書き換わってしまう「バグ」の正体である。

2. VBAによる挙動のシミュレーション

まずは、この挙動を視覚的に理解するためのシミュレーションコードを示そう。このコードは、プロジェクト開始日の変更が「制約なしタスク」にどのような影響を与えるかを実証する。

Option Explicit

''' <summary>
''' プロジェクト開始日の変更が「制約なし」タスクに与える影響をシミュレートする
''' </summary>
Sub SimulateSchedulingLogic()
    Dim pj As Project
    Set pj = ActiveProject
    
    ' 初期化:既存のタスクをクリア
    Application.DisplayAlerts = False
    pj.Tasks.RemoveAll
    Application.DisplayAlerts = True
    
    ' 1. プロジェクト開始日の設定
    pj.ProjectStart = DateValue("2023/10/01")
    
    ' 2. タスクの作成
    Dim t1 As Task, t2 As Task, t3 As Task
    Set t1 = pj.Tasks.Add("タスク1 (制約なし)")
    Set t2 = pj.Tasks.Add("タスク2 (制約なし)")
    Set t3 = pj.Tasks.Add("タスク3 (開始日固定: Must Start On)")
    
    ' タスク1の期間を設定
    t1.Duration = "5d"
    
    ' タスク2をタスク1の後続に設定 (ASAPロジックの連鎖)
    t2.Predecessors = t1.ID
    t2.Duration = "3d"
    
    ' タスク3は、プロジェクト開始日に強制固定
    t3.ConstraintType = pjTaskConstraintMustStartOn
    t3.Start = DateValue("2023/10/01")
    t3.Duration = "2d"
    
    Debug.Print "--- 初期状態 ---"
    Debug.Print "Project Start: " & pj.ProjectStart
    Debug.Print "T1 Start: " & t1.Start ' 10/01
    Debug.Print "T2 Start: " & t2.Start ' 10/06 (T1終了後)
    Debug.Print "T3 Start: " & t3.Start ' 10/01 (固定)

    ' 3. プロジェクト開始日を1週間遅らせる
    pj.ProjectStart = DateValue("2023/10/08")
    
    Debug.Print "--- プロジェクト開始日を1週間変更後 ---"
    Debug.Print "T1 Start: " & t1.Start ' 10/08 にズレる(ASAPのため)
    Debug.Print "T2 Start: " & t2.Start ' 10/13 にズレる(連鎖)
    Debug.Print "T3 Start: " & t3.Start ' 10/01 のまま(制約が強いため)
    
    MsgBox "シミュレーション完了。イミディエイトウィンドウを確認してください。"
End Sub

このコードを実行すれば、`t1` と `t2` はプロジェクト開始日に同期して動くが、`t3` は動かないことがわかる。「制約なし」は「プロジェクトの開始日」という重力に支配されているのだ。

3. 堅牢な設計:設計者が守るべき「3つの鉄則」

業務自動化ツールを設計する際、単に `Task.Start = someDate` と書くのは素人の仕事だ。それは、エンジンの計算結果を「上書き」しているだけで、エンジンの「意図」を破壊している。

アーキテクトとして、以下の設計指針を遵守せよ。

① 日付ではなく「制約(Constraint)」を保存せよ

外部データベース(SQL ServerやExcel)にスケジュール情報を保存する場合、`Start Date` だけを保存してはならない。
必ず `ConstraintType`(制約の種類) と `ConstraintDate`(制約の日付) をセットで保持せよ。
インポート時には、日付を直接書き込むのではなく、制約条件を再構築することで、MS Projectのスケジューリングエンジンに正しく計算させるのが「正しい作法」である。

② 「硬い制約」と「柔らかい制約」を使い分けよ

  • Must Start On / Must Finish On (硬い制約): 納期や契約上の日付など、絶対に動かせないもの。
  • As Soon As Possible (柔らかい制約): 通常のタスク。先行関係によって動くべきもの。

自動化ツールでタスクを生成する際は、これらを明確に区別して `ConstraintType` を割り当てるロジックを組み込め。

③ オブジェクトの副作用を考慮せよ

VBAで大量のタスクを操作する場合、`Task.Start` を書き換えるたびにスケジューリングエンジンが走り、計算負荷(オーバーヘッド)が発生する。
大規模プロジェクトを扱うなら、計算を一時的に止める、あるいは計算が終わった後に一括で制約を適用するような、パフォーマンスを意識した実装が必要だ。

4. プロダクション・コード例:堅牢なタスク生成モジュール

以下に、実務でそのまま利用可能な、保守性の高いタスク生成用クラス(またはモジュール)の設計例を示す。日付を「指定する」のではなく、「制約を構築する」アプローチだ。

Option Explicit

''' <summary>
''' 業務レベルのタスク生成ロジック
''' </summary>
Public Sub CreateRobustTask( _
    ByVal targetTask As Task, _
    ByVal targetDate As Date, _
    ByVal constraintType As MSProject.TaskConstraint, _
    ByVal durationDays As Double)

    On Error GoTo ErrorHandler

    ' 1. 期間の設定
    targetTask.Duration = durationDays * 480 ' 分単位換算 (8h * 60m)
    
    ' 2. 制約条件の適用
    ' 単にStartを入れるのではなく、ConstraintTypeを明示的に指定する
    targetTask.ConstraintType = constraintType
    
    ' 3. 制約日付の設定
    ' 制約の種類に応じた日付の流し込み
    Select Case constraintType
        Case pjTaskConstraintMustStartOn, pjTaskConstraintStartNoEarlierThan
            targetTask.Start = targetDate
            
        Case pjTaskConstraintMustFinishOn, pjTaskConstraintFinishNoLaterThan
            ' Finish日付を指定する場合、Startはエンジンに任せるか、計算して設定する
            targetTask.Finish = targetDate
            
        Case pjTaskConstraintAsSoonAsPossible, pjTaskConstraintAsLateAsPossible
            ' ASAP/ALAPの場合は、日付を指定しても制約が優先されるため、
            ' 基本的には先行関係(Predecessors)に依存させる
            ' 必要に応じて、プロジェクト開始日との兼ね合いを考慮
            targetTask.ConstraintDate = targetDate
            
        Case Else
            ' その他の制約
            targetTask.Start = targetDate
    End Select

    Exit Sub

ErrorHandler:
    ' プロダクションコードでは、エラーを握りつぶさず、
    ' どのタスクで失敗したかをログに残す設計にせよ
    Debug.Print "Error creating task [" & targetTask.Name & "]: " & Err.Description
    Err.Raise Err.Number, "CreateRobustTask", Err.Description
End Sub

''' <summary>
''' 実装テスト
''' </summary>
Sub TestRobustImplementation()
    Dim t As Task
    Set t = ActiveProject.Tasks.Add("堅牢なタスク")
    
    ' 例:絶対に11月1日に開始しなければならないタスク
    Call CreateRobustTask(t, DateValue("2023/11/01"), pjTaskConstraintMustStartOn, 5)
    
    MsgBox "タスク生成完了。制約が正しく適用されたか確認してください。"
End Sub

結論

MS Project VBAにおける真のマスターとは、プロパティの値を書き換える者ではない。スケジューリングエンジンのアルゴリズムを理解し、その計算ロジックを「利用」する者だ。

「制約なし」という言葉の裏に隠された「ASAP」の重みを知ることで、君の作るツールは、ユーザーの意図を正確に反映し、プロジェクトの変更に柔軟に応答する、真にプロフェッショナルな道具へと進化するだろう。

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