1. 序論:`Task.Start` への代入が招く「CPM網の静かなる崩壊」
エンタープライズの基幹システムやERPとMicrosoft Projectを連携させる際、多くのシニアエンジニアが見落とし、後日クリティカルな障害として顕在化する問題がある。それが「タスク日付プロパティへの安易な直接書き込み」だ。
‘ 破滅への一歩:多くの開発者が平然と書いてしまうアンチパターン
ActiveProject.Tasks(1).Start = “2026/04/01”
Excel VBAの感覚でセルに値を流し込むようにこの1行を実行した瞬間、Microsoft Projectの内部スケジュール計算エンジン(CPM: Critical Path Method)は、開発者の意図しない「副作用」をスケジュールデータに刻み込む。
Projectにおいて、タスクの開始日(`Start`)や終了日(`Finish`)は本来、「ネットワークカレンダー」「先行・後続タスクのリンク関係」「タスクの作業時間・期間」「制約条件」の4要素によって動的に導出される計算結果(Read-Onlyに近い派生値)である。
ここに直接日付を代入すると、Projectのエンジンは論理の整合性を保つため、タスクに暗黙の制約条件――通常は `pjSNET` (Start No Earlier Than: 指定日以後に開始) または `pjFNET` (Finish No Earlier Than) ――を自動的に付与する。
この仕様を知らずに全タスクの日付を外部DBから流し込むと、WBS上の全タスクにセミハード制約(Semi-hard Constraints)が強制設定され、先行タスクの前倒しや遅延が後続タスクに一切波及しない「論理リンクが死滅した巨大な固定スケジュール表」へと成り下がる。
本稿では、Projectの計算エンジンをバイパスして直接日付を書き込む危険性をアーキテクチャレベルで解剖し、COMおよびWindows APIを極限まで最適化しつつ、制約条件を正しく制御するための設計原則を提示する。
—
2. 内部アーキテクチャの解剖:制約条件とスケジューリングの力学
Microsoft Projectの内部エンジンは、フォワード・パス(Early Start/Finishの計算)とバックワード・パス(Late Start/Finishの計算)を反復処理し、スラック(トータルフロート)とクリティカルパスをリアルタイムに算出する。
制約条件(ConstraintType)の3つの階層
エンジンが解くべき方程式において、制約条件は以下の3つの硬度に分類される。
| 分類 | 制約タイプ定数 | 動作とCPM網への影響 |
| :— | :— | :— |
| フレキシブル制約
(推奨) | `pjASAP` (できるだけ早く)
`pjALAP` (できるだけ遅く) | エンジンが依存関係とカレンダーに基づいて完全に自由に計算する。論理リンクが100%機能する。 |
| セミハード制約
(境界制約) | `pjSNET` (指定日以後に開始)
`pjSNLT` (指定日以前に開始)
`pjFNET` (指定日以後に終了)
`pjFNLT` (指定日以前に終了) | 日付の境界を固定する。日付を直接代入するとエンジンが自動的にこれを付与するため、動的計算が阻害される。 |
| ハード制約
(固定制約) | `pjMSO` (指定日に開始)
`pjMFO` (指定日に終了) | 先行タスクの依存関係を完全に無視し、指定日に固定する。クリティカルパスが寸断される。 |
`Task.Start` 代入時に内部で起きていること
開発者が `Task.Start = DateValue` を発行した際、内部では以下のトランザクションが不可逆的に実行される。
1. 制約型の自動書き換え: `Task.ConstraintType` が `pjASAP` から `pjSNET` へ強制変換される。
2. 制約日付の固定: `Task.ConstraintDate` に代入値が自動設定される。
3. 早期開始日(Early Start)の固定化: 当該タスクは、先行タスクがどれだけ早く完了しようとも、指定日より前に前倒しされることがなくなる(フロートの歪み)。
4. カスケード再計算のトリガー: 1タスクごとにバックグラウンドで依存関係ツリーの探索が走り、数千タスク規模のプロジェクトではCOM通信のオーバーヘッドと相まって処理時間が指数関数的に増大する。
—
3. 実装検証:暗黙制約の発生とCPM破壊の検証コード
実際にコードを動かしてエンジンの挙動を確認する。以下のコードは、`Task.Start` を直接書き換えた瞬間に制約条件がどのように書き換わるかを監視するものだ。
Option Explicit
‘ ==============================================================================
‘ 概要: Start直接代入による制約条件の自動改変(副作用)を実証する
‘ ==============================================================================
Public Sub Verify_Constraint_SideEffect()
Dim prjApp As MSProject.Application
Dim prjTask As MSProject.Task
Set prjApp = MSProject.Application
prjApp.ScreenUpdating = False
‘ テスト用アクティブプロジェクト内の先頭タスクを取得
On Error Resume Next
Set prjTask = prjApp.ActiveProject.Tasks(1)
On Error GoTo 0
If prjTask Is Nothing Then
MsgBox “タスクが存在しません。1件以上タスクを作成して実行してください。”, vbCritical
Exit Sub
End If
Debug.Print “— 書き込み前 —”
Debug.Print “Task Name : ” & prjTask.Name
Debug.Print “Constraint Type : ” & GetConstraintTypeName(prjTask.ConstraintType)
Debug.Print “Constraint Date : ” & prjTask.ConstraintDate
Debug.Print “Start Date : ” & prjTask.Start
‘ 【危険】Startプロパティへの直接書き込み
prjTask.Start = DateAdd(“d”, 5, prjTask.Start)
Debug.Print “— 書き込み後(直接代入) —”
Debug.Print “Constraint Type : ” & GetConstraintTypeName(prjTask.ConstraintType)
Debug.Print “Constraint Date : ” & prjTask.ConstraintDate
Debug.Print “Start Date : ” & prjTask.Start
prjApp.ScreenUpdating = True
Set prjTask = Nothing
Set prjApp = Nothing
End Sub
Private Function GetConstraintTypeName(ByVal cType As PjConstraint) As String
Select Case cType
Case pjASAP: GetConstraintTypeName = “0: できるだけ早く (ASAP) [完全柔軟]”
Case pjALAP: GetConstraintTypeName = “1: できるだけ遅く (ALAP) [完全柔軟]”
Case pjSNET: GetConstraintTypeName = “4: 指定日以後に開始 (SNET) [セミハード]”
Case pjSNLT: GetConstraintTypeName = “5: 指定日以前に開始 (SNLT) [セミハード]”
Case pjFNET: GetConstraintTypeName = “6: 指定日以後に終了 (FNET) [セミハード]”
Case pjFNLT: GetConstraintTypeName = “7: 指定日以前に終了 (FNLT) [セミハード]”
Case pjMSO: GetConstraintTypeName = “2: 指定日に開始 (MSO) [ハード]”
Case pjMFO: GetConstraintTypeName = “3: 指定日に終了 (MFO) [ハード]”
Case Else: GetConstraintTypeName = “不明な制約型: ” & cType
End Select
End Function
実行結果のイミディエイトウィンドウを見れば一目瞭然である。初期状態が `pjASAP` であったタスクが、`Start` プロパティに代入した瞬間に `pjSNET` に改ざんされ、`ConstraintDate` にその日付が焼き込まれる。
—
4. 正解のアーキテクチャ:スケジュール同期のプロトコル
外部システム(基幹DBや進捗管理システム)からタスクの「日付」を取り込む際、アーキテクトが採るべきアプローチは以下の3点に集約される。
1. 予定(未着手)タスクの同期:
日付を直接入れるのではなく、「先行関係(Predecessors)」 と 「期間(Duration)」 を流し込み、日付自体は計算エンジンに算出させる。どうしても日付を指定しなければならない外部境界(納期など)にのみ、明示的に `ConstraintType` と `ConstraintDate` をセットする。
2. 実績の同期:
既に開始・完了している作業に対しては、`Start` / `Finish` ではなく、`ActualStart` (実績開始日) および `ActualFinish` (実績終了日) を使用する。実績系プロパティは制約条件を汚染せず、正しく「実績」としてスケジュールラインを固定する。
3. 基準計画との乖離管理:
「当初予定の日付」を保持したい場合は、カレントスケジュールを汚すのではなく、`BaselineStart` (ベースライン開始日) 領域(Baseline 0〜10)に書き込む。
—
5. 極限のプロダクションコード:堅牢・高速な外部データ同期エンジン
以下に示すのは、数千〜万単位のタスク群を外部データから安全に同期するためのエンタープライズ品質のクラスモジュール実装である。
Windows APIによる高精度なプロファイリング、計算エンジンの完全一時停止、描画ロック、およびCOM参照の徹底破棄を組み込んでいる。
実装:`clsProjectSyncEngine.cls`
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “clsProjectSyncEngine”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
‘ ==============================================================================
‘ クラス名: clsProjectSyncEngine
‘ 責務 : Project計算エンジンを破壊せず、高速にタスク属性を反映する同期エンジン
‘ ==============================================================================
‘ Win32 API 宣言 (64bit / 32bit 両対応)
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (lpFrequency As Currency) As Long
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” (lpFrequency As Currency) As Long
Private Declare Function LockWindowUpdate Lib “user32” (ByVal hwndLock As Long) As Long
End If
Private m_App As MSProject.Application
Private m_Project As MSProject.Project
Private m_OriginalCalcMode As PjCalculation
Private m_Frequency As Currency
Private m_StartTime As Currency
Private m_EndTime As Currency
Private Sub Class_Initialize()
Set m_App = MSProject.Application
Set m_Project = m_App.ActiveProject
QueryPerformanceFrequency m_Frequency
End Sub
Private Sub Class_Terminate()
‘ 万一の異常終了時にもリソース解放と設定復元を保証
Call RestoreEngineState
Set m_Project = Nothing
Set m_App = Nothing
End Sub
‘ ——————————————————————————
‘ エンジン停止と高速化の初期化
‘ ——————————————————————————
Public Sub BeginSync()
QueryPerformanceCounter m_StartTime
‘ 1. 自動計算エンジンのサスペンド
m_OriginalCalcMode = m_App.Calculation
m_App.Calculation = pjManual
‘ 2. UI描画・イベント抑止
m_App.ScreenUpdating = False
m_App.EnableEvents = False
‘ 3. Windows APIによるウィンドウ描画の完全ロック
#If VBA7 Then
Dim hwndApp As LongPtr
#Else
Dim hwndApp As Long
#End If
hwndApp = m_App.WindowHwnd32
LockWindowUpdate hwndApp
End Sub
‘ ——————————————————————————
‘ エンジンの再同期と終了処理
‘ ——————————————————————————
Public Sub EndSync()
On Error Resume Next
‘ 1. ウィンドウ描画ロックの解除
LockWindowUpdate 0
‘ 2. 計算エンジンの再稼働とフル再計算の実行
m_App.Calculation = m_OriginalCalcMode
m_App.CalculateProject
‘ 3. UI・イベントの復旧
m_App.ScreenUpdating = True
m_App.EnableEvents = True
QueryPerformanceCounter m_EndTime
Dim elapsedSec As Double
elapsedSec = CDbl(m_EndTime – m_StartTime) / CDbl(m_Frequency)
Debug.Print “=== 同期処理完了 ===”
Debug.Print “処理時間: ” & Format(elapsedSec, “0.0000”) & ” 秒”
On Error GoTo 0
End Sub
‘ ——————————————————————————
‘ 安全なタスク更新API:暗黙制約を発生させずにスケジュールを同期する
‘ ——————————————————————————
Public Function UpdateTaskSafe( _
ByVal taskUID As Long, _
ByVal durationMinutes As Long, _
Optional ByVal actualStartDate As Variant, _
Optional ByVal actualFinishDate As Variant, _
Optional ByVal forcedConstraintType As PjConstraint = pjASAP, _
Optional ByVal forcedConstraintDate As Variant _
) As Boolean
Dim t As MSProject.Task
Set t = m_Project.Tasks.GetByGuid(m_Project.Tasks.UniqueID(taskUID).Guid)
‘ 安全策:タスク取得
If t Is Nothing Then
UpdateTaskSafe = False
Exit Function
End If
On Error GoTo ErrHandler
‘ 1. 実績がある場合は実績プロパティを優先更新
‘ (これによりStart/Finishがエンジン内部で自然に確定し、制約を汚染しない)
If Not IsMissing(actualStartDate) Then
If IsDate(actualStartDate) Then
t.ActualStart = CDate(actualStartDate)
End If
End If
If Not IsMissing(actualFinishDate) Then
If IsDate(actualFinishDate) Then
t.ActualFinish = CDate(actualFinishDate)
End If
End If
‘ 2. 未完了部分の期間(Duration)を同期(※Start/Finishではなく期間をセット)
If durationMinutes > 0 Then
t.Duration = durationMinutes
End If
‘ 3. 制約条件の明示的コントロール
‘ 暗黙のpjSNETを排除し、指定された制約型を厳密に適用
If t.ActualFinish = “NA” Then ‘ 未完了タスクのみ制約を再設定
If forcedConstraintType = pjASAP Or forcedConstraintType = pjALAP Then
t.ConstraintType = forcedConstraintType
‘ ASAP/ALAPの場合、ConstraintDateは不要
Else
‘ セミハード・ハード制約を設定する場合は必ず日付とペアでセット
If Not IsMissing(forcedConstraintDate) Then
If IsDate(forcedConstraintDate) Then
t.ConstraintType = forcedConstraintType
t.ConstraintDate = CDate(forcedConstraintDate)
End If
End If
End If
End If
UpdateTaskSafe = True
Cleanup:
Set t = Nothing
Exit Function
ErrHandler:
Debug.Print “タスク UID [” & taskUID & “] の同期失敗: ” & Err.Description
UpdateTaskSafe = False
Resume Cleanup
End Function
Private Sub RestoreEngineState()
On Error Resume Next
LockWindowUpdate 0
m_App.Calculation = m_OriginalCalcMode
m_App.ScreenUpdating = True
m_App.EnableEvents = True
On Error GoTo 0
End Sub
使用例:エントリーポイントの実装
Public Sub Exec_SafeScheduleSync()
Dim syncEngine As clsProjectSyncEngine
Set syncEngine = New clsProjectSyncEngine
‘ 高速同期モードの開始(描画と計算の停止)
syncEngine.BeginSync
‘ — 外部データ読み出しループのシミュレーション —
Dim i As Long
For i = 1 To ActiveProject.Tasks.Count
‘ 外部キーやUIDを元に同期
‘ 例: 期間を更新し、制約条件を明示的にASAP(できるだけ早く)にリセット
syncEngine.UpdateTaskSafe _
taskUID:=ActiveProject.Tasks(i).UniqueID, _
durationMinutes:=480 2, _ ‘ 2日 (1日8h = 480m)
forcedConstraintType:=pjASAP
Next i
‘ ————————————————–
‘ 同期終了(フルの再計算とUI描画の復元)
syncEngine.EndSync
Set syncEngine = Nothing
MsgBox “同期処理が正常に完了しました。”, vbInformation
End Sub
—
6. アーキテクトが遵守すべきチェックリスト
システム間連携やバッチ同期を設計する際は、以下の原則をチームのコーディング規約に組み込んでいただきたい。
1. `Task.Start` / `Task.Finish` への直接代入はコードレビューで即時却下する
日付の指定が必要な場合、それは「実績(`ActualStart`)」なのか、「外部要因による制約(`ConstraintType` + `ConstraintDate`)」なのか、「ただの期間変更(`Duration`)」なのかを設計書レベルで定義させる。
2. 計算モードは `pjManual` で括り、最後に `CalculateProject` を1度だけ呼ぶ
ループ内で都度計算を走らせると、タスク数 $N$ に対して $O(N^2)$ 以上の計算負荷とCOMブリッジオーバーヘッドが生じる。
3. COMオブジェクトのスコープを最小限に保つ
Projectのオブジェクトモデル(特に `Task`, `Resource`, `Assignment`)は循環参照を内包しやすい。オブジェクト変数はループ内部で生成・解放を徹底するか、使い終わった段階で必ず明示的に `Set xxx = Nothing` を実行する。
4. ハード制約(MSO/MFO)を極力排除する
システム間連携でどうしても特定日時にタスクを固定したい場合でも、まずは `pjSNET` やマイルストーン依存関係で表現できないかを検討する。ハード制約は、CPM本来の価値である「先行遅延によるプロジェクト完了日の自動予測」を完全に破綻させる。
Microsoft Projectの価値は、単なるガントチャート描画ツールではなく、動的なCPM計算エンジンを内蔵している点にある。そのエンジンをバイパスして生データを力任せにねじ込む愚を避け、エンジンの力学に沿ったコードを設計することこそが、堅牢なシステムを構築するプロの責務である。
