【テクニカル・上級編】サブプロジェクト統合時の制約条件競合を自動解消するVBAスクリプト – Project VBA解析バイブル

スポンサーリンク

サブプロジェクト統合時の制約条件競合を自動解消するVBAエンジン

Project VBAの現場において、マスタープロジェクトに複数のサブプロジェクトを統合(挿入)する作業は、単なるファイルの合算ではない。それは異なる時空間とリソース制約を持つ独立したエコシステムを、一つの巨大な決定論的グラフへ強制的に組み込む残酷な演算処理に他ならない。

現場で頻発する「サブプロジェクト挿入時のスケジュール崩壊」の根本原因は、リンク切れでもなければリソース不足でもない。「ASAP(可能な限り早く)」と「ALAP(可能な限り遅く)」、あるいは固定日付制約(「指定日以降に開始」等)が、マスター側とサブ側で衝突し、MS Projectのスケジュールエンジンが無限の例外処理(あるいは意図しないクリティカルパスの歪曲)を引き起こすことにある。

本稿では、レガシーなCOM Interopの罠を回避し、メモリ空間を極限まで最適化しながら、制約条件の競合をコードベースで自動検出・調停するVBAエンジンの実装パターンを解説する。

—

1. アーキテクチャの設計思想:なぜ標準機能では破綻するのか

MS ProjectのGUI上でサブプロジェクトを挿入すると、マスタープロジェクト側で外部タスクリンクが生成される。しかし、サブプロジェクト側で設定されていた「ALAP(As Late As Possible)」制約やハード制約(「Mandatory Start」など)は、マスター側のプロジェクト開始日(Project Start)との間で論理的矛盾を生む。

MS Projectの計算エンジン(Calculation Engine)は、タスクの更新時にイベントを発生させるが、VBAから一括して数千件のタスクを操作する場合、イベントの伝播とUndoスタックの肥大化がパフォーマンスを致命的に劣化させる。

これを制圧するため、以下の三原則をコードに実装する。
1. 画面描画と自動計算の完全な封鎖(`ScreenUpdating` と `Calculation` の制御)
2. COMオブジェクトの参照チェーンの即時断絶と解放(メモリリークの根絶)
3. 制約条件のベクトル解析による自動調停(ALAP/ASAPの強制上書きロジック)

—

2. 実装コード:制約条件自動調停エンジン

以下のVBAコードは、マスタープロジェクトに統合されたサブプロジェクト群に対し、競合する制約条件(例:マスターの全体スケジュールに反する古いALAP制約)をスキャンし、安全なASAPへと動的にコンバート、または日付を再調整するプロダクション品質のエンジンである。

Option Explicit

‘ Win32 API: 処理のパフォーマンス計測用(高精度タイマー)
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
End If

‘ MS Project 制約タイプの列挙体(一部抜粋)
Private Enum PjConstraint
pjAsSoonAsPossible = 0
pjAsLateAsPossible = 1
pjMustStartOn = 2
pjMustFinishOn = 3
pjStartNoEarlierThan = 4
pjStartNoLaterThan = 5
pjFinishNoEarlierThan = 6
pjFinishNoLaterThan = 7
End Enum

Public Sub ResolveSubProjectConstraintsEngine()
Dim tStart As Currency, tFreq As Currency
QueryPerformanceFrequency tFreq
QueryPerformanceCounter tStart

Dim appProj As MSProject.Application
Set appProj = ActiveProject.Application

‘ — 1. 極限のパフォーマンス最適化:エンジン負荷の軽減 —
Dim originalCalcMode As Long
originalCalcMode = appProj.Calculation

appProj.ScreenUpdating = False
appProj.Calculation = pjManual ‘ 計算を手動モードに切り替え、バッチ処理化

On Error GoTo ErrorHandler

Dim masterProj As MSProject.Project
Set masterProj = appProj.ActiveProject

Dim tsk As MSProject.Task
Dim resolvedCount As Long
resolvedCount = 0

Debug.Print “=== SubProject Constraint Resolution Engine: Started ===”

‘ — 2. タスク走査と競合解決ロジック —
‘ マスターおよび挿入されたサブプロジェクトの全タスクを走査
For Each tsk In masterProj.Tasks
If Not tsk Is Nothing Then
‘ サブプロジェクト由来の外部タスク、またはサマリーではない実タスクを対象
If Not tsk.Summary Then

‘ 競合パターンA: ALAP制約がマスターの全体制約とコンフリクトを起こしている場合
‘ ※ ここでは全社ポリシーとして「統合プロジェクトでは原則ASAPをベースラインとする」仮定で補正
If tsk.ConstraintType = pjAsLateAsPossible Then
‘ 外部先行タスクとのデッドロックを防ぐため、制約をASAPにフォールバック
tsk.ConstraintType = pjAsSoonAsPossible
resolvedCount = resolvedCount + 1
Debug.Print “Resolved [ALAP -> ASAP]: Task ID ” & tsk.ID & ” (” & tsk.Name & “)”

‘ 競合パターンB: 過去のハード制約(Must Start On等)が残存している場合
ElseIf tsk.ConstraintType = pjMustStartOn Or tsk.ConstraintType = pjMustFinishOn Then
‘ マイルストーン以外で過去日付を向いているハード制約を柔軟な制約(SNET)へ格下げ
If tsk.Start < masterProj.ProjectStart Then tsk.ConstraintType = pjStartNoEarlierThan tsk.ConstraintDate = masterProj.ProjectStart resolvedCount = resolvedCount + 1 Debug.Print "Resolved [Hard Constraint -> SNET]: Task ID ” & tsk.ID & ” (” & tsk.Name & “)”
End If
End If

End If
End If
‘ オブジェクト参照の即時解放(ループ内リーク防止)
Set tsk = Nothing
Next tsk

‘ — 3. 変更の確定と再計算 —
appProj.Calculation = originalCalcMode
masterProj.UpdateAll

Dim tEnd As Currency
QueryPerformanceCounter tEnd
Dim elapsedSec As Double
elapsedSec = CDbl(tEnd – tStart) / CDbl(tFreq)

Debug.Print “=== Execution Completed. Resolved Items: ” & resolvedCount & ” / Time: ” & Format(elapsedSec, “0.000”) & ” sec ===”
MsgBox “制約条件の自動競合解消が完了しました。” & vbCrLf & _
“調整されたタスク数: ” & resolvedCount & vbCrLf & _
“処理時間: ” & Format(elapsedSec, “0.000”) & ” 秒”, vbInformation, “チーフアーキテクト統制システム”

CleanUp:
‘ — 4. 確実な環境復元とメモリ解放 —
appProj.ScreenUpdating = True
appProj.Calculation = originalCalcMode
Set masterProj = Nothing
Set appProj = Nothing
Exit Sub

ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “Engine Critical Error”
Resume CleanUp
End Sub

—

3. シニアエンジニアが押さえるべき深層ポイント

COMオブジェクトの参照チェーンとメモリ管理

VBAにおける最大の悪習は、`For Each` ループ内でオブジェクト変数(今回の場合は `MSProject.Task`)を明示的に解放せず、VBAのガベージコレクション(暗黙の参照カウント解放)に処理を委ねることだ。数万タスク規模の大規模マスタープロジェクトにおいてこれをやると、COMラッパーのメモリリークにより、ExcelやProjectプロセスが突然死(クラッシュ)する。
コード内にある `Set tsk = Nothing` は、ループのイテレーションごとにCOM参照を確実にデクリメントするための必須の防衛策である。

計算エンジン(`Calculation`)の強制停止による高速化

MS Projectは、1つのタスクの制約日付(`ConstraintDate`)や制約タイプ(`ConstraintType`)を変更するたびに、依存関係グラフ全体を再計算しようとする。これを無効化せずに数千件のループを回すと、処理時間が数十分単位に膨れ上がる。
一時的に `Calculation = pjManual` に退避させ、一括変更後にプロジェクト全体を `UpdateAll`(または自動計算モードへの復帰)させることで、演算コストを $O(N^2)$ から $O(N)$ へと劇的に圧縮できる。

レガシー環境とバージョン差異への配慮

MS Projectのバージョン(Project 2013から2021、そしてProject Online/Desktop Client)によって、内部COMインターフェースの振る舞いや、制約タイプ(Constraint Type)の列挙体の内部値が微妙に異なる場合がある。
そのため、マジックナンバーを直接コードに記述するのではなく、列挙体(Enum)として明示的に定義し、APIの仕様変更に対する堅牢性を担保している。

—

結語

自動化とは単なる手作業の置き換えではない。システムが持つ潜在的な矛盾の芽を、コードの力によってあらかじめ摘み取り、決定論的な安定性を担保するエンジニアリングそのものである。
サブプロジェクトの統合においてスケジュールが乱れるのは、ツール側の問題ではなく、制御を怠った人間の責任である。上記のエンジンを基盤に組み込むことで、いかに複雑怪奇なマルチプロジェクト環境であっても、一物一価の整然としたスケジュール統制を実現できる。

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