【Project VBA】制約条件のテンプレート化:ルールベース自動化エンジンでスケジュールを完全に掌握する
こんにちは、チーフアーキテクトの私だ。
これまで数多の巨大プロジェクトのスケジュール管理、そしてProject VBA(MS ProjectのVBA)を用いた自動化機構の構築に立ち会ってきた。
現場からよく聞こえてくる悲鳴がある。
- 「担当者が勝手にタスクの制約(ASAP/ALAP等)を変更し、クリティカルパスが崩壊した」
- 「新規プロジェクトを立ち上げるたび、数千行のタスクに手動で制約条件をセットしている」
- 「定型業務なのに、タスクタイプやカスタムフィールドと制約条件の整合性が取れておらず、進捗予測が全くあてにならない」
これを「人間の注意力」でカバーしようなどというのは、エンジニアリングの放棄に等しい。
制約条件の適用は、ルールベースのエンジンに完全に委譲すべきだ。
今回は、MS Projectのオブジェクトモデルの挙動、ライフサイクル、そしてパフォーマンスの重みを熟知した者にしか書けない、「制約条件のテンプレート化エンジン」の極限の知見を授けよう。
—
1. なぜ「手動」と「泥臭いコード」は破綻するのか?
多くの開発者がやりがちなアンチパターンは、イベントプロシージャ(`Project_Change`など)の内部で、愚直に全タスクをループし、`Task.ConstraintType`を書き換える手法だ。
これをやると何が起きるか?
1. イベントの無限ループやカスケード地獄: 1つのタスクを書き換えたことで別のイベントが発火し、CPUが焼き切れる。
2. パフォーマンスの深刻な劣化: MS ProjectはExcelと異なり、タスクのプロパティを変更するたびに「スケジュール・エンジン(計算エンジン)」が裏で再計算を走らせる。これを考慮せずに数千タスクを触ると、VBAが数分間フリーズする。
3. ルールとデータの密結合: 「どのタスクにどの制約を貼るか」というビジネスロジックがコード内にハードコーディングされ、仕様変更のたびにソースコードの改修が必要になる。
堅牢なアーキテクチャの要件
私たちが構築すべきエンジンは以下の3つを満たさなければならない。
- 一括処理(Batch Processing): 計算エンジンのトリガーを最小限に抑え、トランザクション的に処理する。
- データ駆動(Data-Driven): ルール(テンプレート定義)を外部(または専用のマスターテーブル)から読み込み、コードを変更せずにルールを追加・変更できる。
- 安全な型安全とエラーハンドリング: MS Project固有の定数(`pjConstraintAsap`など)や、存在しないタスクに対する耐性を持つ。
—
2. 設計:制約条件テンプレート・エンジンの全体像
今回の実装では、「カスタムテキストフィールド(例: `Text1` = タスク種別)」をキーにして、あらかじめ定義されたマスタールールに基づき、適切な制約条件(`ConstraintType`)と必要に応じた制約日付(`ConstraintDate`)を自動適用するエンジンを作成する。
[マスターデータ/ルール定義] ──(判定)──> [対象タスクの抽出] ──> [バッチ制約適用] ──> [スケジュール再計算]
これを実務レベルの堅牢性を持たせたVBAコードとして落とし込む。
—
3. プロダクションコード:制約条件自動適用エンジン
以下のコードは、エラーハンドリング、パフォーマンス最適化(画面描画の抑制)、および保守性を考慮したプロダクション品質のモジュールである。
Option Explicit
‘ ==============================================================================
‘ モジュール名: clsConstraintEngine
‘ 概要: タスクのカスタムフィールドを元に、ルールベースで制約条件をテンプレート適用する
‘ ==============================================================================
‘ MS Projectの制約定数の可読性を高めるための列挙体
Public Enum TaskConstraintTypeEx
exASAP = 0 ‘ できるだけ早く (As Soon As Possible)
exALAP = 1 ‘ できるだけ遅く (As Late As Possible)
exFNET = 2 ‘ 指定日以降に終了 (Finish No Earlier Than)
exFNLT = 3 ‘ 指定日以前に終了 (Finish No Later Than)
exMSO = 4 ‘ 指定日 ต้อง 終了 (Must Start On) – ※環境依存文字除外
exMFO = 5 ‘ 指定日 終了 (Must Finish On)
exSNET = 6 ‘ 指定日以降に開始 (Start No Earlier Than)
exSNLT = 7 ‘ 指定日以前に開始 (Start No Later Than)
End Enum
Public Sub ApplyConstraintTemplates()
Dim tsk As Task
Dim appCalcMode As Long
‘ 1. パフォーマンス最適化の極意:
‘ 自動計算モードを一時的に手動にし、画面描画を停止することで爆速化させる
appCalcMode = Application.Calculation
Application.Calculation = pjCalculationManual
On Error GoTo ErrorHandler
‘ ステータスバーに進捗を表示
Application.StatusBar = “制約条件テンプレートの適用を開始しています…”
Dim appliedCount As Long
appliedCount = 0
‘ 2. アクティブプロジェクトの全タスクを走査(サマリータスクやマイルストーンの除外判定もここで可能)
For Each tsk In ActiveProject.Tasks
If Not tsk Is Nothing Then
‘ 削除されたタスクや空行の除外
If tsk.Summary = False And tsk.ExternalTask = False Then
‘ 3. ルール評価エンジン(ここでタスクの属性に応じたロジックを適用)
If EvaluateAndApplyRule(tsk) Then
appliedCount = appliedCount + 1
End If
End If
End If
Next tsk
‘ 4. 処理終了後のクリーンアップと再計算
Application.Calculation = appCalcMode
Application.Calculate
Application.StatusBar = “完了: ” & appliedCount & ” 件のタスクに制約条件を適用しました。”
MsgBox “制約条件のテンプレート適用が正常に完了しました。” & vbCrLf & _
“適用件数: ” & appliedCount & ” 件”, vbInformation, “制約エンジン”
Exit Sub
ErrorHandler:
‘ 異常終了時も必ず計算モードとステータスバーを復元する
Application.Calculation = appCalcMode
Application.StatusBar = “”
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “エンジン異常終了”
End Sub
Private Function EvaluateAndApplyRule(ByRef tsk As Task) As Boolean
Dim isModified As Long
isModified = False
‘ 例として、「Text1(タスク種別)」の値に応じて制約を分岐させる
‘ 実務ではここをDictionaryオブジェクトや別シートのマスター参照に拡張すると最強になる
Dim taskType As String
taskType = Trim(CStr(tsk.Text1))
Select Case taskType
Case “マイルストーン_外部依存”
‘ 外部マイルストーンは「指定日以降に終了(FNET)」かつ、期限日を制約日とするルール
If tsk.ConstraintType <> pjFNET Then
tsk.ConstraintType = pjFNET
‘ もしDeadlineが設定されていればそれを制約日に流用する等の高度な制御
If tsk.Deadline <> “NA” Then
tsk.ConstraintDate = tsk.Deadline
End If
isModified = True
End If
Case “定常ルーチン”
‘ 定常業務は常に「できるだけ早く(ASAP)」を強制
If tsk.ConstraintType <> pjASAP Then
tsk.ConstraintType = pjASAP
isModified = True
End If
Case “ハードリリース”
‘ 絶対死守のリリース日は「指定日終了(MFO)」
If tsk.ConstraintType <> pjMFO Then
tsk.ConstraintType = pjMFO
If tsk.Finish <> “” Then
tsk.ConstraintDate = tsk.Finish
End If
isModified = True
End If
Case Else
‘ デフォルトルール(未定義の場合はASAPにフォールバック、または何もしない)
‘ 今回は厳格運用のため、未定義タイプはログまたは何もしない
End Select
EvaluateAndApplyRule = isModified
End Function
—
4. プロフェッショナルのための実装上の注意点
このコードをそのままエンタープライズ環境に投入するにあたり、チーフアーキテクトとしていくつか重要な注意点を補足しておく。
① スケジュール・エンジンの競合を防ぐ `Application.Calculation`
MS Projectでは、VBAからタスクのプロパティを変更するたびに全体スケジュールが再計算される。数千行あるプロジェクトでこれをやると、処理時間がO(N²)的な挙動を起こして数十分かかることがある。
コード内にある通り、処理の最初に `Application.Calculation = pjCalculationManual` にし、最後に `pjCalculationAutomatic`(あるいは元の状態)に戻して `Application.Calculate` を叩く。これが鉄則だ。
② カスタムフィールド(Text1-30, Flag1-20)の設計思想
ハードコーディングされた `Select Case taskType` は、小規模〜中規模であれば十分に機能するが、巨大組織のPMO(プロジェクトマネジメントオフィス)レベルになると破綻する。
その場合は、プロジェクト内の特定のタスク(あるいは別ファイル)に「ルール定義テーブル」を持たせ、`Dictionary` オブジェクトにマッピングしてからエンジンを回す設計にリファクタリングしてほしい。
③ サマリータスク(要約タスク)への配慮
MS Projectの要約タスク(親タスク)に対して無理やり制約条件をコードから突っ込むと、プロジェクト管理上予期せぬスケジュールの歪みやエラーを引き起こす。上記のコードでは `If tsk.Summary = False` でしっかりと除外している。ここを怠ると、スケジュールの整合性が完全に破壊されるので注意しろ。
—
5. おわりに
ツールやマクロの価値は、「人間のミスを排除し、クリエイティブで本質的な思考の時間を生み出すこと」に尽きる。
今回紹介した「制約条件のテンプレート化エンジン」を導入すれば、プロジェクトのスケジュール品質は劇的に均一化され、「誰がタスクを作っても破綻しないスケジュール構造」が手に入る。
君たちの現場でも、泥臭い手動オペレーションは今すぐコードで駆逐し、真のエンジニアリングを適用してほしい。健闘を祈る。
