Project VBAを掌握する極限の知見:プロジェクトカレンダーの動的生成とタスク一括適用
開発プロジェクトの現場において、WBS(Work Breakdown Structure)の自動生成やスケジュール管理の効率化は、多くのエンジニアが挑み、そして「カレンダーの壁」に阻まれてきた難所だ。
「稼働日ベースでタスクの開始・終了日を計算したいのに、土日や祝日、さらにはプロジェクト独自の休日が無視されてガタガタになる」
「タスクごとにカスタムカレンダーを割り当てたいが、UIから手動でポチポチ設定するのは地獄だ」
Project VBA(MS ProjectのVBA)を扱う上で、カレンダー(Calendar)オブジェクトのライフサイクルと、タスク(Task)へのバインドメカニズムを理解していないコードは、実務の現場ではただの「動くゴミ」と化す。数千行のWBSを処理した瞬間にメモリリークを起こすか、意図しないスケジュール計算(カレンダーの継承バグ)を引き起こすからだ。
今回は、Project VBAにおける「プロジェクトカレンダーの動的生成」と「タスクへの一括適用」をテーマに、実務の現場で絶対に破綻しない堅牢な設計手法と、そのままプロダクション環境に投入できるコードを授けよう。
—
1. なぜ「力技のコード」は破綻するのか?
多くの初学者や、Excelマクロの延長でProject VBAに触れたエンジニアが書くコードには、決まって次のようなアンチパターンが見受けられる。
- 例外日(休日)のハードコーディング: コード内に `If` 文で直接祝日を羅列し、毎年メンテナンスが必要になる。
- タスクごとの非効率なループ: `ActiveProject.Tasks` を毎回頭から舐め回し、個別にカレンダープロパティを書き換える(COMオブジェクトの往復による致命的なパフォーマンス低下)。
- グローバルカレンダーの汚染: 既存の「標準」カレンダーを直接書き換え、他のプロジェクトやタスクに悪影響を及ぼす。
実務で求められるのは、「外部データ(CSVやDB)から稼働日・非稼働日を動的に読み込み、プロジェクト独自のベースカレンダーを生成し、対象のWBS階層やリソースに一瞬で適用する」という、クリーンかつハイパフォーマンスなアーキテクチャだ。
—
2. 堅牢な設計のための3つの極意
プロダクションコードを組み上げる前に、Project VBA特有の「重み」を理解しておかなければならない。
① カレンダーの「ベース(Base Calendar)」と「タスクカレンダー」の分離
Projectには、プロジェクト全体を支配する `ProjectCalendar` と、タスク個別に割り当てられる `Task.Calendar` が存在する。カスタムカレンダーを作る際は、必ず `ActiveProject.BaseCalendars` コレクションに対して新規作成を行い、それをタスクのプロパティにアサインする。
② 例外日の網羅と「例外上書き」の罠
Projectの `Exceptions.Add` メソッドは、既存の例外日と範囲が重複するとランタイムエラーを吐く。動的生成時には、既存の例外範囲をクリアするか、安全な日付範囲であることを担保するバリデーションが必須となる。
③ トランザクション的な一括処理(ScreenUpdatingの制御)
数千件のタスクに対してカレンダーや制約を操作する場合、描画や自動再計算(Calculation)が走るたびにProjectはフリーズ寸前になる。`Application.Calculation = pjCalculationManual` で計算を止め、処理後に一気に `pjCalculationAutomatic` に戻すのがプロの鉄則だ。
—
3. 実装:カレンダー動的生成&タスク一括適用エンジン
以下に、外部から取得した休日リストを基にカスタムカレンダーを生成し、特定の条件(例:特定のWBSプレフィックスを持つタスク群)に一括適用するプロフェッショナルコードを提示する。
Option Explicit
‘ ==============================================================================
‘ 処理名 : ApplyCustomCalendarToTasks
‘ 概要 : 動的にカスタムカレンダーを生成し、条件に合致するタスク群へ一括適用する
‘ ==============================================================================
Public Sub ApplyCustomCalendarToTasks()
‘ エラーハンドリングの標準化
On Error GoTo ErrorHandler
‘ パフォーマンス最適化のための設定退避
Dim originalCalc As Long
originalCalc = Application.Calculation
Application.Calculation = pjCalculationManual
Application.ScreenUpdating = False
Dim targetCalName As String
targetCalName = “PJ_Custom_2024”
‘ 1. 既存の同名カスタムカレンダーが存在する場合はクリーンアップ
Call RemoveExistingCalendar(targetCalName)
‘ 2. 新規ベースカレンダーの作成
Dim customCal As Calendar
Set customCal = ActiveProject.BaseCalendars.Add(targetCalName)
‘ 3. 基本的な稼働時間のセット(例:月~金 9:00〜18:00、土日休み)
Call SetupDefaultWorkingHours(customCal)
‘ 4. 動的な休日(例外日)の追加(本来はDBやCSVから動的配列で取得する)
Call AddDynamicExceptions(customCal)
‘ 5. 対象タスクへの一括適用(パフォーマンスを考慮したイテレーション)
Dim t As Task
Dim appliedCount As Long
appliedCount = 0
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
‘ 例:タスク名に特定のプレフィックスが含まれる、または特定のWBS階層の場合
‘ ここでは実務に合わせて条件をカスタマイズしてください
If Left(t.Name, 3) = “[Dev]” Then
‘ タスク独自のカレンダーとしてカスタムカレンダーを割り当て
Set t.Calendar = customCal
‘ タスク自体のスケジュール管理方法を「カレンダーに基づく」に強制
t.IgnoreResourceCalendar = True
appliedCount = appliedCount & 1 ‘ 処理カウンタ
End If
End If
Next t
‘ 変更を反映して計算を再開
Application.Calculation = originalCalc
Application.ScreenUpdating = True
MsgBox “カスタムカレンダー [” & targetCalName & “] の生成と、” & _
vbCrLf & “対象タスクへの適用が完了しました。”, vbInformation, “完了”
Exit Sub
ErrorHandler:
‘ 異常系の復旧処理
Application.Calculation = originalCalc
Application.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End Sub
‘ ==============================================================================
‘ 補助プロシージャ: 同名カレンダーの安全な削除
‘ ==============================================================================
Private Sub RemoveExistingCalendar(ByVal calName As String)
Dim bc As Calendar
On Error Resume Next
Set bc = ActiveProject.BaseCalendars(calName)
If Not bc Is Nothing Then
‘ 標準カレンダー(“標準”)は削除してはならないためガード
If bc.Name <> “標準” And bc.Name <> “夜勤” And bc.Name <> “24時間稼働” Then
bc.Delete
End If
End If
On Error GoTo 0
End Sub
‘ ==============================================================================
‘ 補助プロシージャ: 基本稼働時間の設定
‘ ==============================================================================
Private Sub SetupDefaultWorkingHours(ByRef targetCal As Calendar)
Dim dayIndex As Long
‘ 曜日ごとの設定 (1:日, 2:月, 3:火, 4:水, 5:木, 6:金, 7:土)
For dayIndex = 1 To 7
With targetCal.WeekDays(dayIndex)
If dayIndex = 1 Or dayIndex = 7 Then
‘ 土日は非稼働日
.Working = False
Else
‘ 平日は 9:00 – 18:00 (昼休憩 12:00 – 13:00 を想定する場合はWorkingTimesを設定)
.Working = True
.WorkTimes.Clear
.WorkTimes.Add From:=TimeSerial(9, 0, 0), To:=TimeSerial(12, 0, 0)
.WorkTimes.Add From:=TimeSerial(13, 0, 0), To:=TimeSerial(18, 0, 0)
End If
End With
Next dayIndex
End Sub
‘ ==============================================================================
‘ 補助プロシージャ: 動的例外日(祝日・会社休日)の追加
‘ ==============================================================================
Private Sub AddDynamicExceptions(ByRef targetCal As Calendar)
‘ 実務では、ここでDB接続やCSVファイルを読み込み、ループでExceptions.Addを実行する
‘ 例として固定の年末年始や特例休日を追加
With targetCal.Exceptions
‘ 例外日1: 創立記念日
.Add Type:=pjExceptionTypeByDate, From:=DateSerial(2024, 11, 1), To:=DateSerial(2024, 11, 1), Name:=”創立記念日”
‘ 例外日2: 年末年始休暇のブロック
.Add Type:=pjExceptionTypeByDate, From:=DateSerial(2024, 12, 28), To:=DateSerial(2025, 1, 5), Name:=”年末年始休暇”
End With
End Sub
—
4. ファイル・データベース連携の注意点(実務の罠)
このコードを実務の自動化パイプラインに組み込む際、以下のインフラストラクチャ的な制約に直面する。
1. 多重起動とファイルロック:
Projectファイル(`.mpp`)がSharePointやTeamsなどのクラウドストレージに同期されている環境でVBAを実行すると、オブジェクトの保存時や同期タイミングでCOM例外が発生する。ローカル環境に一度ファイルを同期・クローンしてから処理を行い、最後にアップロードするフローを設計すべきだ。
2. 外部DB(SQL Server等)との非同期連携:
休日のマスタデータを社内ニッチなDBから引く場合、`ADODB.Connection` を用いてVBA内でJSONやレコードセットを受け取り、それを上記の `AddDynamicExceptions` に流し込む形が最も堅牢である。ADOの接続文字列やタイムアウト設定は、プロジェクトの規模に合わせて必ず冗長化しておこう。
—
5. 総括
プロジェクトマネジメントの成否は、スケジュールの「正しさ」にかかっている。そして、その正しさは正確なカレンダー定義とタスクの依存関係の整合性からしか生まれない。
今回紹介した設計パターンとコードは、単なる「動くコード」ではなく、数千規模のタスクを抱えるエンタープライズ環境であっても破綻しないスケーラビリティを持っている。
あなたが開発する業務自動化ツールが、現場のプロジェクトマネージャーを無駄な手作業の地獄から救い出し、真に価値のあるマネジメント業務へと導く武器となることを期待する。
