Project VBAを掌握する極限の知見:過負荷リソースの呪縛を断つ「タスク分割」自動化エンジン
レガシーシステムの深淵において、Microsoft ProjectのVBA(Project VBA)ほど、そのドキュメントの少なさとオブジェクトモデルの狂気に満ちた領域はない。特に「リソースの過負荷(Overallocated)」の検知と、その動的な平準化ロジックの構築は、多くのシニアエンジニアやシステム管理者を絶望の淵に追い込んできた。
標準機能の「リソース平準化(Leveling)」は、黒魔術のようなブラックボックスであり、意図しないクリティカルパスの延伸や、野放図な遅延を生む。実務の現場で求められるのは、制御可能な確定論に基づいたアルゴリズムによる負荷分散だ。
今回は、特定のリソースに集中した過負荷タスクを検知し、指定された期間と分割単位に基づいて物理的にタスクを切り分け、割り当てを再構築する極限のVBA自動化エンジンを公開する。
—
1. Project VBAの隠された罠:オブジェクトのライフサイクルとメモリ管理
MS ProjectのCOMオブジェクトモデルは、ExcelやWordに比べて圧倒的にメモリリークに弱い。特に `Task` や `Resource` のコレクションをループ処理する際、参照を適切に解放しないままオブジェクトを生成し続けると、COMコンテナのヒープが肥大化し、最悪の場合はProject自体が沈黙する。
シニアエンジニアたる者、以下の鉄則を遵守せよ。
- オブジェクト変数の明示的破棄: ループ内で取得したオブジェクトは、イテレーションごとに `Nothing` を代入して参照カウントを即座に落とす。
- 画面描画の完全な抑制: `Application.ScreenUpdating = False` だけでは不十分である。バックグラウンドタスクのイベント駆動を断つために、APIレベルでの制御も視野に入れよ。
—
2. 実装アーキテクチャ:過負荷タスク動的分割エンジン
以下のコードは、指定されたリソースが「過負荷(PeakUnits > MaxUnits)」状態にあるタスクを特定し、そのデュレーションを指定日数ごとにスライスして新規タスクとして連鎖配置(FS関係の構築)を行う実用エンジンである。
Option Explicit
‘ =================================================================================
‘ 圧倒的パフォーマンスとメモリ最適化を施したリソース平準化・タスク分割エンジン
‘ Architecture Chief Note:
‘ COMオブジェクトの参照リークを完全に防ぎつつ、タスクの物理分割を安全に実行する。
‘ =================================================================================
Public Sub ExecuteTaskDecongestionEngine()
Dim prj As Project
Set prj = ActiveProject
‘ 実行時パフォーマンスの極限化:画面描画および計算の停止
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual
On Error GoTo ErrorHandler
Dim targetResourceName As String
targetResourceName = “シニアエンジニアA” ‘ ターゲットとする過負荷リソース名
Dim splitDurationDays As Double
splitDurationDays = 5# ‘ 1タスクあたりの最大分割日数(例: 5日単位に分割)
Dim t As Task
Dim r As Resource
Dim assign As Assignment
‘ 対象リソースのオブジェクト取得
Dim targetRes As Resource
Set targetRes = Nothing
For Each r In prj.Resources
If Not r Is Nothing Then
If r.Name = targetResourceName Then
Set targetRes = r
Exit For
End If
Set r = Nothing
End If
Next r
If targetRes Is Nothing Then
MsgBox “指定されたリソースが見つかりません: ” & targetResourceName, vbCritical
GoTo Cleanup
End If
‘ コレクションの安全な走査のため、一度対象タスクのIDを配列に退避(コレクション変更時のインデックスズレを防ぐ)
Dim taskIDList() As Long
Dim taskCount As Long
taskCount = 0
For Each t In targetRes.Tasks
If Not t Is Nothing Then
If Not t.Summary Then ‘ サマリータスクは除外
‘ 過負荷判定ロジック (PeakUnitsがUnitsを超過しているか簡易判定)
If IsTaskOverallocated(t, targetRes) Then
ReDim Preserve taskIDList(taskCount)
taskIDList(taskCount) = t.ID
taskCount = taskCount + 1
End If
End If
Set t = Nothing
End If
Next t
‘ 退避したタスクIDを元に分割処理を実行
Dim i As Long
For i = 0 To taskCount – 1
Set t = prj.Tasks.UniqueID(prj.Tasks(taskIDList(i)).UniqueID)
If Not t Is Nothing Then
Call SplitTaskIntoChunks(prj, t, targetRes, splitDurationDays)
Set t = Nothing
End If
Next i
MsgBox “タスクの分割と負荷平準化が完了しました。”, vbInformation
Cleanup:
‘ 状態の復元とメモリ解放
Application.Calculation = pjCalculationAutomatic
Application.ScreenUpdating = True
Set targetRes = Nothing
Set prj = ActiveProject
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume Cleanup
End Sub
‘ ——————————————————————————–
‘ 補助関数: タスクが過負荷状態にあるかを判定
‘ ——————————————————————————–
Private Function IsTaskOverallocated(t As Task, res As Resource) As Boolean
Dim asn As Assignment
IsTaskOverallocated = False
For Each asn In t.Assignments
If asn.ResourceID = res.ID Then
‘ 割当unitsがリソースの最大稼働容量を超えている場合を過負荷とみなす
If asn.Units > res.MaxUnits Then
IsTaskOverallocated = True
Set asn = Nothing
Exit Function
End If
Set asn = Nothing
End If
Next asn
End Function
‘ ——————————————————————————–
‘ コアロジック: 指定されたタスクを指定日数で切り分け、後続タスクとして連鎖配置する
‘ ——————————————————————————–
Private Sub SplitTaskIntoChunks(prj As Project, targetTask As Task, res As Resource, chunkDays As Double)
Dim originalName As String
originalName = targetTask.Name
Dim totalDuration As Double
totalDuration = targetTask.Duration / 480 ‘ ミリ秒/分単位から日単位へ変換 (1日=480分前提)
If totalDuration <= chunkDays Then Exit Sub ' 分割必要なし
Dim remainingDuration As Double
remainingDuration = totalDuration
Dim prevTask As Task
Set prevTask = targetTask
' 初回タスクの期間を切り詰め
prevTask.Duration = chunkDays 480
prevTask.Name = originalName & " (Part 1)"
Dim partIndex As Long
partIndex = 2
remainingDuration = remainingDuration - chunkDays
Do While remainingDuration > 0
Dim currentChunk As Double
If remainingDuration >= chunkDays Then
currentChunk = chunkDays
Else
currentChunk = remainingDuration
End If
‘ 新規タスクの挿入 (プレデセッサとしてprevTaskを指定)
Dim newTask As Task
Set newTask = prj.Tasks.Add(originalName & ” (Part ” & partIndex & “)”, prevTask.Index + 1)
newTask.Duration = currentChunk 480
‘ リソースの再割り当て
Dim newAsn As Assignment
Set newAsn = newTask.Assignments.Add(ResourceID:=res.ID)
‘ 依存関係の設定 (FS: Finish-to-Start)
newTask.Predecessors = prevTask.ID & “FS”
‘ 次のループへ
Set prevTask = newTask
remainingDuration = remainingDuration – currentChunk
partIndex = partIndex + 1
‘ オブジェクト解放
Set newAsn = Nothing
Set newTask = Nothing
Loop
Set prevTask = Nothing
End Sub
—
3. コードの深層解説:なぜこのロジックが現場で生き残るのか
1. コレクションの二重参照回避 (`taskIDList` による退避)
Project VBAにおいて、`For Each` ループの最中にタスクを追加・削除・構造変更すると、コレクションのポインタが無効化され、実行時エラー(あるいはサイレントクラッシュ)を引き起こす。一度タスクのIDをプリミティブな配列に退避させ、ループの外側で安全に操作する設計が、レガシー環境での安定稼働を担保する。
2. 時間単位の正規化 (`480分 = 1日`)
MS Projectの内部データ構造では、時間はすべて「分(Minutes)」で保持されている。標準の稼働日設定(1日8時間=480分)に基づき明示的に換算処理を行うことで、カレンダーの差異による予期せぬデュレーションのズレを完全に排除している。
3. リソース割り当てのトランザクション的処理
分割された各サブタスクに対して、明示的に `Assignments.Add` を実行し、さらに `Predecessors` で論理的な順序(FS関係)を強制的に結びつけることで、平準化後のスケジュール崩壊を防ぎ、プロジェクトマネージャーが意図した通りの「分割による負荷の分散」を視覚的にもデータ的にも実現している。
—
4. チーフアーキテクトからの提言
システム管理や大規模プロジェクトの運用において、VBAは単なる「マクロ記録の延長」ではない。それは、OSのメモリ構造とCOMのライフサイクルを掌中に収めた者だけが使いこなせる、極限のシステム制御インターフェースである。
安易なサードパーティ製アドインに依存する前に、自らの手でこのエンジンを導入し、リソースの過負荷というプロジェクトの癌を根絶せよ。コードは嘘をつかない。設計の美しさが、そのままシステムの堅牢性へと直結するのだ。
