【入門編】プロ向け:計算エンジンの競合を回避する「制約条件の段階的適用」アルゴリズム – Project VBA解析バイブル

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

【Project VBAの深淵】計算エンジンの衝突を回避せよ:制約条件の「段階的適用」アルゴリズム

こんにちは。現場の最前線で、複雑怪奇なプロジェクト・スケジュールの自動化と格闘し続けているチーフアーキテクトです。

「マクロの記録」を使って、なんとかタスクの開始日を書き換えるところまではできた。でも、いざ複雑な依存関係(先行・後続関係)を持つ大規模なプロジェクトに適用しようとすると、「計算エラーが発生しました」「日付が予期せぬ方向に飛んでいった」「スケジュールが壊れた」……。

そんな経験はありませんか?

もしあなたがそう感じているなら、おめでとうございます。あなたは今、「単なるツールの操作」から「計算エンジンの制御」という、プロフェッショナルの領域への入り口に立っています。

今日は、多くのエンジニアが挫折する「MS Projectの計算エンジンとの衝突」を回避し、美しく、かつ確実に制約条件を適用するための「段階的適用(Layered Application)アルゴリズム」について、その極意を伝授しましょう。

1. なぜ、一度に設定すると「壊れる」のか?

まず、敵を知ることから始めましょう。なぜ、全てのタスクに対して `ConstraintType`(制約条件)を一気に書き換えると、エラーが起きるのか。

MS Projectは、単なる「表計算ソフト」ではありません。タスク同士が網の目のようにつながった、動的な「依存関係ネットワーク」です。

衝突のメカニズム:綱引きの悲劇

想像してみてください。プロジェクトという名の「綱引き」があります。

1. タスクA(先行)が「1月1日に開始せよ!」と叫んでいます(Must Start On)。
2. タスクB(後続)が「タスクAが終わったらすぐ開始せよ!」(ASAP)と繋がっています。
3. そこに、あなたがマクロでタスクBに対し「できるだけ遅く開始せよ!」(ALAP)という制約を、タスクAの計算が終わる前に叩き込んだとします。

この瞬間、Projectの計算エンジンの中で「Aは前へ! Bは後ろへ!」という矛盾した信号が同時に発生します。エンジンがこの矛盾を解決しようとして計算ループに陥ったり、無理やり整合性を取ろうとして日付を数年先に飛ばしたりするのが、エラーの正体です。

「一度に全てを変えようとしないこと」。これが、エンジンを掌握する第一歩です。

2. 解決策:依存関係の「深さ」に基づいた段階的適用

プロのエンジニアは、制約を適用する際、プロジェクトの「依存関係の階層(Level)」を意識します。

全てのタスクを一度に処理するのではなく、以下のステップを踏むのです。

1. 依存関係の順序(トポロジカル順序)を特定する。
2. 「先行タスク」から順番に、制約を適用していく。
3. 一つの階層(レベル)を適用するたびに、エンジンの計算を一度完了(Recalculate)させる。
4. 計算が安定した状態で、次の階層のタスクへ進む。

これを図解するとこうなります。

graph TD
    A[Level 0: 依存関係のないタスク] -->|制約適用 & 再計算| B[Level 1: Level 0に依存するタスク]
    B -->|制約適用 & 再計算| C[Level 2: Level 1に依存するタスク]
    C -->|制約適用 & 再計算| D[完了: スケジュール安定]

このように、「波紋が広がるように」制約を伝播させていくのが、最も安全で確実なアルゴリズムなのです。

3. 実装コード:プロフェッショナル・テンプレート

では、実際にこのロジックをVBAでどう書くのか。初学者の方でも理解しやすいよう、丁寧にコメントを添えたコードを示します。

このコードの肝は、各タスクが「依存関係の何番目の階層にいるか」を計算し、階層ごとに処理を行う点にあります。

Option Explicit

''' <summary>
''' プロジェクトの制約条件を、依存関係の順序に従って安全に適用する
''' </summary>
Sub ApplyConstraintsSafely()
    Dim t As Task
    Dim dictLevels As Object
    Set dictLevels = CreateObject("Scripting.Dictionary")
    
    Dim maxLevel As Integer
    maxLevel = 0
    
    On Error GoTo ErrorHandler
    
    ' 高速化のため、画面更新と計算を一時停止(ただし、段階的適用では計算が必要)
    Application.ScreenUpdating = False
    
    ' --- STEP 1: 各タスクの「依存階層」を計算する ---
    ' ここでは、先行タスクの数に基づいた簡易的な階層計算を行います
    For Each t In ActiveProject.Tasks
        If Not t Is Nothing Then
            Dim level As Integer
            level = GetDependencyLevel(t)
            dictLevels(t.ID) = level
            If level > maxLevel Then maxLevel = level
        End If
    Next t

    ' --- STEP 2: 階層ごとに制約を適用していく ---
    Dim currentLevel As Integer
    For currentLevel = 0 To maxLevel
        Debug.Print "現在、階層 " & currentLevel & " を処理中..."
        
        For Each t In ActiveProject.Tasks
            If Not t Is Nothing Then
                ' 現在の階層に属するタスクのみを対象にする
                If dictLevels(t.ID) = currentLevel Then
                    
                    ' 【ここに制約適用ロジックを記述】
                    ' 例:特定の条件を満たすタスクに ALAP (できるだけ遅く) を設定
                    Call SetTaskConstraint(t)
                    
                End If
            End If
        Next t
        
        ' --- STEP 3: 階層が変わるごとに計算エンジンを同期させる ---
        ' これが「衝突」を防ぐ最も重要な一行です
        Application.Calculate
        DoEvents ' OSに制御を戻し、計算の完了を待機
    Next currentLevel

    MsgBox "制約条件の適用が完了しました。", vbInformation
    
CleanUp:
    Application.ScreenUpdating = True
    Exit Sub

ErrorHandler:
    MsgMsgBox "エラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

''' <summary>
''' タスクの依存関係の深さを再帰的に計算する関数
''' </summary>
Function GetDependencyLevel(targetTask As Task) As Integer
    Dim pred As Task
    Dim maxPredLevel As Integer
    maxPredLevel = 0
    
    ' 先行タスク(Predecessors)を辿る
    If targetTask.Predecessors.Count > 0 Then
        For Each pred In targetTask.Predecessors
            ' 先行タスクのレベル + 1 が自分のレベルの候補
            Dim l As Integer
            l = GetDependencyLevel(pred) + 1
            If l > maxPredLevel Then maxPredLevel = l
        Next pred
    End If
    
    GetDependencyLevel = maxPredLevel
End Function

''' <summary>
''' 個別のタスクに対する制約適用ルール(ビジネスロジック)
''' </summary>
Sub SetTaskConstraint(t As Task)
    ' ここに具体的なルールを書き込みます
    ' 例:タスク名に「重要」と入っていたら、制約を「Must Start On」にする等
    If InStr(t.Name, "重要") > 0 Then
        t.ConstraintType = pjMustStartOn
        t.ConstraintDate = Date + 7 ' 1週間後に強制設定
    Else
        ' 通常は ASAP (制約なし) に設定
        t.ConstraintType = pjAsSoonAsPossible
    End If
End Sub

4. ここをクリアすれば、Project VBAの基本はバッチリですよ

このコードを読み解く上で、大切にしてほしいポイントが3つあります。

① 再帰関数(Recursive Function)の活用

`GetDependencyLevel` 関数の中で、自分自身を呼び出していますね。これが「再帰」です。複雑な依存関係のツリーを掘り進めるには、この手法が最もエレガントです。これを使えるようになると、データの構造を自在に扱えるようになります。

② `Application.Calculate` の「重み」を知る

「計算を何度も呼び出すと遅くなるのでは?」と不安になるかもしれません。その通りです。しかし、「遅いコード」よりも「間違った結果を出すコード」の方が、業務においては致命的です。
段階的な計算は、エンジンの整合性を守るための「必要経費」だと心得ましょう。

③ `DoEvents` による制御の譲渡

`DoEvents` は、VBAが計算に没頭しすぎてWindows全体がフリーズするのを防ぎ、計算エンジンに「一息つく時間」を与えます。大規模な処理を行う際の、エンジニアの優しさ(配慮)です。

最後に

いかがでしたか?
「マクロの記録」で得た知識は、いわば「辞書」です。しかし、今回お伝えしたような「アルゴリズム」は、その辞書を使って物語を紡ぐ「文法」に当たります。

文法を理解すれば、あなたはMS Projectという強力なエンジンを、意のままに操る「指揮者」になれるはずです。

一歩ずつ、確実に。プロフェッショナルの階段を登っていきましょう。応援していますよ!

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