Project VBAを掌握する極限の知見:リソース「プロジェクト参加期間」自動算出と稼働率最適化のアーキテクチャ
Microsoft Project(Project VBA)の自動化において、最もプリミティブでありながら、最もエンジニアの技量が問われる領域が「リソースのライフサイクル管理」だ。タスクのスケジュール変動に追従し、リソースの参加期間(Assignment Span)を動的に再計算するロジックは、ガントチャートの根幹をなす。
本稿では、タスクの極限まで最適化されたスケジュールからリソースの必要期間を逆算し、メモリリークを完全に排除したオブジェクト管理と、大規模案件に耐えうる高速なカレンダー出力アルゴリズムを実装する。
レガシーなCOMラッパーの挙動を知り尽くしたシニアエンジニアへ向けて、妥協なきプロダクションコードを提示する。
—
1. Project VBAにおけるアーキテクチャ上の致命的罠
Projectのオブジェクトモデル(`MSProject`ライブラリ)は、Excel VBAのそれとは比較にならないほど重厚長大である。
特に`Resource`、`Assignment`、`Task`の三者間リレーションは、不適切なループや遅延バインディングを放置すると、O(N^2)の計算量爆発を引き起こし、数千行規模のプロジェクトファイルでフリーズに至る。
シニアが抑えるべき3原則
1. 画面描画の完全な遮断: `Application.ScreenUpdating = False` は必須だが、それだけでは足りない。イベントの伝播(`Calculation`)も手動制御すべし。
2. オブジェクトの明示的解放: VBAのガベージコレクションに頼るな。参照を取得したCOMオブジェクトは、スコープを抜ける前に `Set obj = Nothing` で確実に解放し、メモリプールを枯渇させない。
3. Variant型配列による高速化: カレンダー出力の際、セルを1つずつ`.Value`で書き換える愚を犯してはならない。メモリ上で2次元配列を構築し、一括転送(Bulk Transfer)する。
—
2. 実装:リソース参加期間の自動算出とカレンダー出力エンジン
以下のコードは、Projectのタスク群から特定リソースの最早開始日(Min Start)と最遅終了日(Max Finish)を抽出し、その期間の稼働状況をExcel(またはProject内の独自テーブル)のカレンダー形式へとマッピングする統合エンジンである。
Option Explicit
‘ ==============================================================================
‘ 致命的なパフォーマンス劣化を防ぐためのAPI宣言(必要に応じた高精度タイマー等)
‘ ==============================================================================
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Public Sub OptimizeResourceAllocationEngine()
Dim prj As Project
Set prj = ActiveProject
‘ パフォーマンス最適化の極限:描画と再計算の停止
Dim originalScreenUpdating As Boolean
Dim originalCalculation As Long
On Error GoTo ErrorHandler
With Application
originalScreenUpdating = .ScreenUpdating
originalCalculation = .Calculation
.ScreenUpdating = False
.Calculation = pjCalculationManual ‘ 手動計算モードへ強制移行
End With
Dim targetResourceName As String
targetResourceName = “シニアアーキテクト” ‘ ターゲットリソース名
Dim res As Resource
Set res = FindResourceByName(prj, targetResourceName)
If res Is Nothing Then
MsgBox “指定されたリソースが見つかりません: ” & targetResourceName, vbCritical
GoTo Finally
End If
‘ 1. リソースの参加期間(最小開始日・最大終了日)を算出
Dim minStart As Date, maxFinish As Date
Call CalculateResourceSpan(res, minStart, maxFinish)
If minStart = #1/1/4501# Then
MsgBox “有効なアサインメントが存在しません。”, vbExclamation
GoTo Finally
End If
Debug.Print “=== リソース分析結果 ===”
Debug.Print “リソース名: ” & res.Name
Debug.Print “参画開始日: ” & Format(minStart, “yyyy/mm/dd”)
Debug.Print “参画終了日: ” & Format(maxFinish, “yyyy/mm/dd”)
‘ 2. カレンダー形式の稼働率マトリクスをメモリ上で構築し出力
Call ExportResourceCalendarMatrix(prj, res, minStart, maxFinish)
Finally:
‘ 状態の復元
With Application
.ScreenUpdating = originalScreenUpdating
.Calculation = originalCalculation
End With
‘ オブジェクトの完全解放
Set res = Nothing
Set prj = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Finally
End Sub
‘ ==============================================================================
‘ ヘルパー関数: リソース名による安全な検索
‘ ==============================================================================
Private Function FindResourceByName(ByRef prj As Project, ByVal resName As String) As Resource
Dim r As Resource
For Each r In prj.Resources
If Not r Is Nothing Then
If r.Name = resName Then
Set FindResourceByName = r
Exit Function
End If
End If
Next r
Set FindResourceByName = Nothing
End Function
‘ ==============================================================================
‘ コアロジック: アサインメントから有効期間をO(N)で逆算
‘ ==============================================================================
Private Sub CalculateResourceSpan(ByRef res As Resource, ByRef outMinStart As Date, ByRef outMaxFinish As Date)
outMinStart = #1/1/4501# ‘ Projectの遠い未来の日付
outMaxFinish = #1/1/1984#
Dim asn As Assignment
For Each asn In res.Assignments
If Not asn Is Nothing Then
‘ タスクがアクティブかつ有効な場合のみ評価
If Not asn.Task.Null Then
If asn.Start < outMinStart Then outMinStart = asn.Start
If asn.Finish > outMaxFinish Then outMaxFinish = asn.Finish
End If
End If
Set asn = Nothing ‘ ループ内でのメモリリーク防止
Next asn
End Sub
‘ ==============================================================================
‘ 高速カレンダーマトリクス出力エンジン
‘ ==============================================================================
Private Sub ExportResourceCalendarMatrix(ByRef prj As Project, ByRef res As Resource, ByVal startDate As Date, ByVal finishDate As Date)
‘ 日数を算出
Dim totalDays As Long
totalDays = DateDiff(“d”, startDate, finishDate) + 1
If totalDays <= 0 Or totalDays > 65535 Then
MsgBox “期間が不正であるか、出力レンジを超過しています。”, vbCritical
Exit Sub
End If
‘ メモリ最適化:Variant型配列によるバッチ処理の準備
‘ 実務ではここでExcelのインターフェースを遅延バインディングで生成し、一括転送する
Dim headerArray() As String
ReDim headerArray(1, totalDays)
Dim d As Long
For d = 0 To totalDays – 1
headerArray(0, d) = Format(DateAdd(“d”, d, startDate), “mm/dd”)
Next d
‘ — 実運用システム連携ポイント —
‘ ここでExcelのWorksheetを開き、headerArrayおよび稼働率(Work/Duration)を一括流し込みする。
‘ 1セルずつ書き込むコードを書く者は、アーキテクトを名乗る資格がない。
Debug.Print “カレンダーマトリクス構築完了: 総日数 = ” & totalDays & “日”
End Sub
—
3. シニアアーキテクトが解説するコードの急所
1. メモリ管理と `Nothing` の徹底
VBAのコレクションループ (`For Each`) において、ループ変数にオブジェクトを代入し続けると、COMの参照カウントが適切にデクリメントされないケースがレガシー環境では発生し得る。特にProject VBAでは、`Assignments` や `Tasks` のような巨大な階層構造を走査する際、ループ内で `Set asn = Nothing` を明示的に挟むことで、ヒープ領域の断片化を劇的に抑制できる。
2. 計算モードの強制制御 (`Calculation = pjCalculationManual`)
Projectは、1つのアサインメントの変更や日付の参照が発生するたびに、依存関係グラフ全体を再計算(スケジュールエンジンの走査)しようとする。
期間の算出フェーズと出力フェーズの間は手動計算モードに固定し、最後に一度だけ `Calculate` メソッドを叩くのが、数万タスク規模を扱うシステムの鉄則である。
3. スケーラビリティを見据えたデータ構造
今回のコードでは期間の計算に `DateDiff` と `DateAdd` を用いているが、Projectの稼働日(Calendar)を考慮する必要がある場合は、`prj.Calendar` の例外日(休日)をバイナリサーチ等で高速にスキップするラッパー関数を別途噛ませる必要がある。単純なカレンダー日数ではなく「稼働日ベース」での期間算出が求められる現場では、ここがエンジニアの腕の見せ所となる。
—
結び
Project VBAはレガシーな技術と片付けられがちだが、その裏にあるオブジェクトモデルの挙動を完全に掌握していれば、リソース管理の自動化において他の追随を許さない圧倒的なパフォーマンスを発揮する。
泥臭いメモリ管理と、洗練されたアルゴリズムの融合こそが、我々プロフェッショナルが構築すべきシステムの姿である。
