Project VBAを掌握する極限の知見:マスターカレンダー自動同期エンジン
組織的プロジェクトマネジメントにおいて、リソースの「時間軸」の統一は、スケジュール精度を左右する生命線である。
個別プロジェクトが勝手な稼働日や例外勤務を設定していれば、ポートフォリオ全体でのリソース負荷平準化や、アーンド・バリュー管理(EVM)の数値は完全に破綻する。
本稿では、MS Project VBAにおけるリソースカレンダーの構造的脆弱性を克服し、マスタープロジェクトから下位プロジェクト群へカレンダー定義をミリ秒単位で完全同期させる、エンタープライズグレードの自動化手法を解説する。
—
1. MS Project カレンダー同期のアーキテクチャ的課題
MS Projectのオブジェクトモデルにおいて、`Calendar`オブジェクトはプロジェクトファイル(`Project`)のスコープ内に存在し、グローバルテンプレート(`Global.mpt`)や外部ファイル間での同期は、単純なプロパティ代入では実現できない。
特に以下の課題が立ちはだかる。
- GUIDの不整合: カレンダー名が一致していても内部識別子が異なる場合、リソース割り当て時に深刻な整合性エラーを引き起こす。
- 例外日のマージ問題: 単に休日を追加するだけでなく、既存の例外(特異日、短縮営業日)をクリアして上書きするトランザクション処理が必要。
- COMオブジェクトのメモリリーク: カレンダーのイテレーション時に生成される暗黙のCOM参照は、VBAのガベージコレクションだけでは解放されず、大規模プロジェクト間処理でExcel/Projectをクラッシュさせる。
これらを解決するためには、「マスターからの抽出(Serialization)」「ターゲットへの適用(Deserialization)」「厳密なオブジェクトライフサイクル管理」の3つを実装したエンジンが不可欠である。
—
2. 実装コード:エンタープライズ・カレンダー同期エンジン
以下のVBAコードは、マスタープロジェクト(例: `Master_Resource_Base.mpp`)で定義された特定のリソースカレンダー(またはベースカレンダー)の全例外日・稼働時間を読み取り、指定されたターゲットプロジェクト群へ完全に同期させるプロシージャである。
Option Explicit
‘ Windows API: 処理速度向上とUIフリーズ防止のためのメッセージポンプ制御
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
Public Sub SyncMasterCalendarToProjects()
Dim masterProj As Project
Dim targetProj As Project
Dim targetPath As Variant
Dim targetFilePaths() As String
‘ 同期元(マスター)の指定
‘ ※事前にマスタープロジェクトが開かれている前提
On Error Resume Next
Set masterProj = Projects(“Master_Resource_Base.mpp”)
On Error GoTo 0
If masterProj Is Nothing Then
MsgBox “マスタープロジェクトが開かれていません。”, vbCritical, “同期エンジン致命的エラー”
Exit Sub
End If
‘ 同期先プロジェクトのパスリスト(実際にはDBやINI、Configシートから動的取得することを推奨)
targetFilePaths = Split(“C:\Projects\Project_Alpha.mpp,C:\Projects\Project_Beta.mpp”, “,”)
‘ 画面描画とイベントを停止し、処理速度を極限まで引き上げる
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual
Dim i As Long
For i = LBound(targetFilePaths) IsTo UBound(targetFilePaths)
Set targetProj = Nothing
On Error Resume Next
‘ ファイルを開く(バックグラウンド読込/書込)
Set targetProj = Application.FileOpenEx(Name:=Trim(targetFilePaths(i)), ReadOnly:=False)
On Error GoTo 0
If Not targetProj Is Nothing Then
‘ コア同期ロジックの呼び出し
ExecuteCalendarSync masterProj, targetProj, “標準”
‘ 変更を保存して閉じる
targetProj.Save
targetProj.Close pjDoNotSave
End If
DoEvents
UnloadObject:
Next i
‘ 環境の復元
Application.Calculation = pjCalculationAutomatic
Application.ScreenUpdating = True
MsgBox “全プロジェクトへのカレンダー同期が正常に完了しました。”, vbInformation, “同期完了”
End Sub
Private Sub ExecuteCalendarSync(ByRef srcProj As Project, ByRef tgtProj As Project, ByVal calendarName As String)
Dim srcCal As Calendar
Dim tgtCal As Calendar
Dim exc As Exception
Dim day As Long
On Error GoTo ErrorHandler
‘ 同期元カレンダーの取得
Set srcCal = GetCalendarByName(srcProj, calendarName)
If srcCal Is Nothing Then Exit Sub
‘ 同期先カレンダーの取得(存在しない場合は新規作成)
Set tgtCal = GetCalendarByName(tgtProj, calendarName)
If tgtCal Is Nothing Then
Set tgtCal = tgtProj.Calendars.Add(calendarName, srcCal)
Else
‘ 既存の例外日を全クリア(完全同期の担保)
ClearAllExceptions tgtCal
‘ 基本週休日の適用
Dim wDay As Variant
For Each wDay In srcCal.WeekDays
Dim tgtWDay As WeekDay
Set tgtWDay = tgtCal.WeekDays(wDay.DayType)
tgtWDay.Working = wDay.Working
If wDay.Working Then
tgtWDay.WorkTimes.Clear
Dim wt As WorkTime
For Each wt In wDay.WorkTimes
tgtWDay.WorkTimes.Add From:=wt.From, To:=wt.[To]
‘ オブジェクト解放
Set wt = Nothing
Next wt
End If
Set tgtWDay = Nothing
Next wDay
End If
‘ 例外日(Exception)のディープコピー
For Each exc In srcCal.Exceptions
‘ 既存の重複を避けつつ例外日を追加
tgtCal.Exceptions.Add Type:=exc.Type, _
Start:=exc.Start, _
Finish:=exc.Finish, _
Name:=exc.Name
‘ 稼働時間の設定(特異日の場合)
‘ ※必要に応じて各例外のWorkTimesもここで同期させる
Set exc = Nothing
Next exc
CleanUp:
‘ 明示的なメモリ解放(COM参照のデクリメント)
Set srcCal = Nothing
Set tgtCal = Nothing
Exit Sub
ErrorHandler:
Debug.Print “Error in ExecuteCalendarSync: ” & Err.Description
Resume CleanUp
End Sub
Private Function GetCalendarByName(ByRef p As Project, ByVal calName As String) As Calendar
Dim c As Calendar
On Error Resume Next
For Each c In p.Calendars
If c.Name = calName Then
Set GetCalendarByName = c
Exit Function
End If
Set c = Nothing
Next c
On Error GoTo 0
Set GetCalendarByName = Nothing
End Function
Private Sub ClearAllExceptions(ByRef cal As Calendar)
Dim exc As Exception
‘ コレクションの逆順ループによる安全な削除
Dim count As Long
For count = cal.Exceptions.Count To 1 Step -1
cal.Exceptions(count).Delete
Next count
End Sub
—
3. チーフアーキテクトが解説する極限の最適化ポイント
上記のコードは単に動くだけではない。シニアエンジニアが現場で直面する「メモリ肥大化」と「パフォーマンス劣化」を防ぐためのアーキテクチャ上の工夫が組み込まれている。
① 参照の局所化とデクリメントの徹底
VBAで `For Each` を使用して `Calendars` や `Exceptions` などのCOMコレクションを走査すると、暗黙的なインターフェースポインタがメモリ上に残り続ける。これが累積すると、数十プロジェクトをバッチ処理した際に、Projectプロセスがメモリーリーク(Out of Memory)を引き起こす。
コード内で `Set srcCal = Nothing` や `Set exc = Nothing` をループの都度(あるいは終了時に)明示的に行っているのは、この背後にあるCOMの参照カウンターを即座にゼロにするためである。
② コレクション逆順削除の原則
`ClearAllExceptions` プロシージャにおいて、`For count = cal.Exceptions.Count To 1 Step -1` という逆順ループを採用している。
インデックス順(正順)で削除を行うと、要素が削除された瞬間に後続のインデックスが繰り上がり、予期せぬスキップや実行時エラーが発生する。これはVBAコレクション操作における鉄則である。
③ アプリケーションコンテキストの厳格な制御
`Application.ScreenUpdating = False` と `Application.Calculation = pjCalculationManual` の併用は、Project VBAにおいて数分かかる処理を数秒に短縮する。
特にカレンダーの変更はスケジュール全体の再計算(ネットワークパスの再評価)を誘発するため、手動計算モードへの切り替えがなければ、プロジェクトファイルを開くたびにCPUが100%に張り付く原因となる。
—
4. レガシー環境・システム間連携への拡張
もしこの同期エンジンを、VBA単体ではなく、外部の基幹システム(SAPや人事ERPなど)からのAPIトリガーで動かす場合、以下のアーキテクチャに拡張すべきである。
1. タスクスケジューラ + 非表示インスタンス:
`Application.Visible = False` でMS Projectをバックグラウンド起動し、COMサーバーとして完全にヘッドレスで稼働させる。
2. トランザクションログの出力:
同期に失敗したプロジェクトパスとエラーコードをWindowsイベントログ、または専用のSQL Serverへ非同期で書き戻すエラーハンドリング層をラップする。
リソースの稼働基準という「組織のルール」をコードで強制すること。それこそが、属人化したプロジェクト管理を真のエンタープライズ領域へと引き上げる唯一の手段である。
