【テクニカル・上級編】VBAによる「プロジェクトカレンダー」の動的生成とタスクへの一括適用 – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見:動的プロジェクトカレンダー生成とタスク一括適用アーキテクチャ

レガシーシステムとモダンなプロジェクト管理の狭間で、我々VBAエンジニアは常に「限界」との戦いを強いられている。特にMicrosoft ProjectのCOMオブジェクトを操作する際、カレンダーの動的生成と数千件に及ぶタスク群への依存関係・カスタムカレンダーの適用は、一歩間違えばメモリリークと劇的なパフォーマンス低下を引き起こす地雷原だ。

今回は、標準機能では手の届かない「動的なプロジェクトカレンダーの構築とタスクへの一括適用」をテーマに、Project VBAの深淵に踏り込む。オブジェクトのライフサイクル管理、COMの境界を越える最適化、そして実戦で即座に機能するプロダクションコードを提示する。

1. Project VBAにおけるパフォーマンスの真実とオブジェクト制御

多くの開発者は、`For Each` を使い、タスクを1件ずつループさせてカレンダーを割り当てる。数万行のWBSを持つエンタープライズ環境において、このアプローチは自殺行為だ。COMのコンテキストスイッチが幾千回も発生し、処理が数時間に及ぶか、最悪の場合はCOM例外でクラッシュする。

内存最適化と明示的解放の鉄則

VBAはガベージコレクションを持たない。特にMS Projectのオブジェクトモデル(`Application`, `Project`, `Task`, `Calendar` 等)は、参照を適切に解放しないと、VBAのプロセス終了後も背後で `WINPROJ.EXE` がゾンビプロセスとしてメモリ上に残留し続ける。

以下の原則を遵守せよ:
1. オブジェクト変数は必ず `Nothing` で明示的に解放する。
2. ループ内でのオブジェクト生成・破棄を避け、参照のキャッシュ機構を実装する。
3. 画面描画の抑止(`ScreenUpdating`)と計算モードの手動化を徹底する。

2. アーキテクチャ概要:動的カレンダー生成エンジン

今回構築するのは、以下の要件を満たすエンジンだ。

  • ハードコードされた休日ではなく、外部データ(ExcelやDB)から例外日(祝日・特別稼働日)を動的に読み込む。
  • 既存のカレンダー構造を汚染しないよう、一意のUIDを持つカスタムカレンダーをプログラム的に生成・上書きする。
  • 特定のWBS階層、あるいはフィルタリングされたタスク群に対して、生成したカレンダーを一括適用し、依存関係(先行・後続)の整合性を維持したままスケジュールを再計算する。

3. 実装コード:エンタープライズ・カレンダー自動適用モジュール

以下のコードは、エラーハンドリング、COMオブジェクトの厳格なライフサイクル管理、そして極限まで無駄を削ぎ落としたバッチ処理を実装したプロダクションコードである。

Option Explicit

‘ ==============================================================================
‘ Module: ModProjectCalendarEngine
‘ Description: 動的カレンダー生成およびタスク一括適用エンジン
‘ Author: Chief Architect
‘ ==============================================================================

Public Sub ApplyDynamicCalendarToProject()
Dim prj As MSProject.Project
Dim cal As MSProject.Calendar
Dim tsk As MSProject.Task
Dim 稼働時間帯 As Variant

‘ 実行時間計測用
Dim startTime As Double
startTime = Timer

‘ 1. エラーハンドリングとアプリケーション環境の凍結(爆速化の基本)
On Error GoTo ErrorHandler

With Application
.ScreenUpdating = False
.Calculation = pjCalculationManual ‘ 手動計算モードでCOMの無駄な再計算を防ぐ
End With

Set prj = ActiveProject

‘ 2. 既存カスタムカレンダーのクリーンアップ(存在する場合は削除して再生成)
Dim targetCalendarName As String
targetCalendarName = “【自動生成】カスタム稼働カレンダー”

Call DeleteExistingCalendar(prj, targetCalendarName)

‘ 3. 新規ベースカレンダーの作成(標準ベースから派生)
Set cal = prj.Calendars.Add(targetCalendarName, prj.Calendar)

‘ 4. カレンダーの基本設定(週休2日、稼働時間の定義:8:00 – 17:00 休憩1時間)
‘ ※Projectの時間は分単位で指定する(例: 8:00 = 480)
With cal
‘ 土日を非稼働に設定
.WeekDays(pjSaturday).Working = False
.WeekDays(pjSunday).Working = False

‘ 平日の稼働時間を再定義
Dim i As Integer
For i = pjMonday To pjFriday
With .WeekDays(i)
.Working = True
.WorkPeriods.Add Start:=CDate(“8:00”), End:=CDate(“12:00”)
.WorkPeriods.Add Start:=CDate(“13:00”), End:=CDate(“17:00”)
End With
Next i

‘ 5. 動的な例外日(祝日・特別休暇)の追加(例として固定値だが実運用ではDB/Excelから配列で取得)
.Exceptions.Add Type:=pjExByDate, Start:=CDate(“2023/12/29”), Finish:=CDate(“2024/01/03”)
End With

‘ 6. 対象タスク群へのカレンダー一括適用と依存関係の最適化
Dim appliedCount As Long
appliedCount = 0

‘ パフォーマンス最適化のため、タスクコレクションを一括走査
For Each tsk in prj.Tasks
If Not tsk Is Nothing Then
‘ 特定の条件を満たすタスク(例: サマリー以外、かつ特定のテキストフィールドにフラグがある等)
If Not tsk.Summary Then
‘ タスクレベルでのカスタムカレンダー適用
tsk.Calendar = targetCalendarName

‘ 依存関係の整合性チェック(例:先行タスクとの論理矛盾があればログ出力)
Call ValidateTaskDependencies(tsk)

appliedCount = appliedCount + 1
End If
End If
Next tsk

‘ 7. 再計算の実行と画面描画の復元
Application.Calculation = pjCalculationAutomatic
prj.Calculate
Application.ScreenUpdating = True

MsgBox “カレンダーの動的適用が完了しました。” & vbCrLf & _
“処理件数: ” & appliedCount & ” タスク” & vbCrLf & _
“処理時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “アーキテクチャ・インフォメーション”

CleanUp:
‘ 8. オブジェクトの明示的解放(メモリリーク防止の極意)
Set tsk = Nothing
Set cal = Nothing
Set prj = Nothing
Exit Sub

ErrorHandler:
‘ 異常系処理
Application.ScreenUpdating = True
Application.Calculation = pjCalculationAutomatic
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “System Error”
Resume CleanUp
End Sub

‘ ==============================================================================
‘ 補助ルーチン: 既存カレンダーの安全な削除
‘ ==============================================================================
Private Sub DeleteExistingCalendar(ByRef prj As MSProject.Project, ByVal calName As String)
Dim c As MSProject.Calendar
On Error Resume Next
Set c = prj.Calendars(calName)
If Not c Is Nothing Then
‘ 標準カレンダーは削除できないため、ベース以外のカスタムカレンダーのみ削除
If Not c.BaseCalendar Then
c.Delete
End If
End Sub
On Error GoTo 0
Set c = Nothing
End Sub

‘ ==============================================================================
‘ 補助ルーチン: タスクの依存関係バリデーション
‘ ==============================================================================
Private Sub ValidateTaskDependencies(ByRef tsk As MSProject.Task)
Dim predLink As TaskPredecessor
On Error Resume Next
For Each predLink In tsk.Predecessors
‘ 循環参照や異常なリードタイムの検出ロジックをここに記述
If predLink.Lag > 480 5 Then ‘ 5日以上の極端なラグがある場合など
‘ ログ出力やフラグ立て
End If
Next predLink
On Error GoTo 0
End Sub

4. シニアエンジニアが押さえるべき「罠」と回避策

このコードを実務に投入する際、以下のアーキテクチャ上の制約に直面する。これらを事前に潰すことがプロの仕事だ。

① ベースカレンダーとリソースカレンダーの競合

MS Projectにおいて、タスクにカレンダーを割り当てる場合 (`Task.Calendar`)、そのタスクにアサインされているリソースのカレンダーとの間で「優先順位の競合」が発生する。

  • 知見: タスク自体のカレンダー制約を厳格に適用したい場合、リソース側のスケジュール設定(`Resource.Calendar`)がタスクカレンダーを上書きしないよう、タスクの「リソースはカレンダーを無視する(Resource Can Level)」プロパティをVBA側で制御する必要がある。

② COMマーシャリングのオーバーヘッドを最小化する

`For Each tsk In prj.Tasks` のループ内でのプロパティアクセス(例: `tsk.Summary`, `tsk.Name`)は、COM境界を跨ぐため非常にコストが高い。
もし数万件のタスクを扱う場合、一度配列(Variant Array)にデータを吸い上げるか、不必要なプロパティ参照をループ内から排除するデザインパターンが必須となる。今回はタスク数上限が数千規模であることを前提に最適化しているが、さらなる大規模案件では、一度XML形式にシリアライズしてDOM操作、あるいはADOによる高速処理へシフトする判断も必要となる。

5. 結びにかえて

VBAはレガシーな言語と揶揄されることがある。しかし、Microsoft Projectの内部COM APIを直接叩き、ミリ秒単位のパフォーマンスを絞り出すこのアーキテクチャリングにおいて、VBAほどダイレクトにOSリソースとホストアプリケーションを制御できる言語は他にない。

メモリのライフサイクルを支配し、オブジェクトの生成と破棄を完璧にコントロールできた者だけが、巨大なWBSを意のままに操る特権を得る。この知見をあなたのシステムに実装し、圧倒的な自動化の果実を掴み取ってほしい。

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