【テクニカル・上級編】初心者向け:VBAによる制約条件の一括削除と、プロジェクトの完全ASAP化 – Project VBA解析バイブル

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

スケジュール計算エンジンを掌握せよ: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化」は、単なるクレンジング作業ではない。それは、スケジュール計算エンジンというブラックボックスに対し、人間が設定した不条理な制約を取り払い、数学的な論理を取り戻させる儀式である。

大規模システムを設計する者は、常に「最悪のケース(ユーザーによる乱雑な入力)」を想定し、それを一撃で、かつ低負荷でリセットできる手段を持っておかなければならない。このスクリプトはそのための、最小にして最強の武器となるだろう。

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