Microsoft Project VBAの世界へようこそ。私はこの領域の深淵までを知り尽くしたチーフアーキテクトだ。
君が今取り組もうとしている「実績工数(Actual Work)の入力に伴う残工数の自動再計算」は、一見すると単純な算術処理に見えるだろう。しかし、実務の最前線では、この設計の甘さがプロジェクト全体のスケジュールを崩壊させ、再計算の無限ループを引き起こし、最終的には「使い物にならないゴミツール」を生み出す原因となる。
リファレンスをなぞるだけの退屈なコードは捨てろ。今日は、プロフェッショナルが実戦で採用する「イベント駆動型・整合性重視」のアーキテクチャを伝授する。
—
1. なぜ「標準機能」に任せてはいけないのか
Microsoft Projectには標準で「進捗率の更新に合わせて実績工数を計算する」機能がある。しかし、なぜ我々はVBAを書くのか?
それは、「現場の現実は引き算では解決しない」からだ。
実績が5時間入ったからといって、残りが単純に「当初予算 – 5時間」になるとは限らない。現場では「5時間やったが、実はあと10時間必要だと判明した(工数膨張)」あるいは「効率化により残りは2時間で済む」といった不連続な事態が頻発する。
この「実績入力」というトリガーを捉え、ビジネスロジックに基づいた残工数の再配分と、それに伴う終了予定日のリアルタイム・シミュレーションを行うこと。これこそが、PMOが求める真の自動化だ。
—
2. アーキテクチャの急所:イベントの二重発生を防ぐ
Project VBAで`AssignmentChange`イベントを扱う際、初心者が必ず陥る罠がある。「値の書き換えが、さらなる変更イベントを誘発する」無限ループだ。
Excel VBAの`Application.EnableEvents = False`に相当する機能がProjectには存在しない(厳密にはApplicationレベルでの制御が貧弱だ)。そのため、自前で「再入防止フラグ(State Management)」を実装する必要がある。
—
3. 実装:プロフェッショナル・コード
以下のコードは、クラスモジュールを利用して特定のプロジェクトの変更を監視し、実績工数が更新された瞬間に残工数を動的に再計算するテンプレートだ。
3.1. クラスモジュール:`clsProjectWatcher`
‘—————————————————————————————
‘ Module : clsProjectWatcher
‘ Purpose : アサインメントの変更を監視し、ビジネスロジックを注入する
‘—————————————————————————————
Option Explicit
Private WithEvents App As MSProject.Application
Private isProcessing As Boolean ‘ 再入防止フラグ
Private Sub Class_Initialize()
Set App = MSProject.Application
End Sub
‘ アサインメント(リソース割り当て)が変更された際に発火
Private Sub App_ProjectAssignmentChange(ByVal pj As Project, ByVal asgn As Assignment)
‘ 1. 二重処理の防止
If isProcessing Then Exit Sub
On Error GoTo ErrorHandler
isProcessing = True
‘ 2. 実績工数(ActualWork)が入力されたかチェック
‘ ※ここでは簡略化のため常に再計算ロジックを通すが、本来は直前の値との比較を推奨
Debug.Print “— Assignment Change Detected: ” & asgn.ResourceName & ” on ” & asgn.TaskName
‘ 3. 残工数の再計算ロジック
‘ 例:実績が入力されたら、残工数を「(総工数 – 実績) × リスク係数」で再算出する等の独自ロジック
‘ ここではシンプルに、実績入力に合わせて残工数を保護しつつ終了予定日を再計算させる
‘ [重要] Projectの計算エンジンを一時停止させる(パフォーマンス対策)
App.Calculation = pjManual
‘ ロジックの例:実績が更新されたら、そのタスクの残りの期間に均等に再配分する
‘ もし残工数が0以下になるならタスクを完了とする
If asgn.ActualWork > 0 Then
‘ ここに高度なビジネスルールを記述
‘ 例:asgn.RemainingWork = (asgn.Work – asgn.ActualWork) 1.2 ‘ 20%のバッファ追加
‘ リアルタイム影響算出
‘ 残工数を変更すると、ProjectのエンジンがFinish Dateを自動延伸させる
End If
‘ 4. 計算の実行
pj.Recalculate
App.Calculation = pjAutomatic
Debug.Print “New Finish Date: ” & asgn.Finish
CleanUp:
isProcessing = False
Exit Sub
ErrorHandler:
‘ ログ出力(本番ではDBやテキストログへ)
Debug.Print “Error in App_ProjectAssignmentChange: ” & Err.Description
Resume CleanUp
End Sub
3.2. 標準モジュール:`modInitializer`
‘—————————————————————————————
‘ Module : modInitializer
‘ Purpose : クラスのインスタンス化とライフサイクル管理
‘—————————————————————————————
Option Explicit
Private pWatcher As clsProjectWatcher
Public Sub StartProjectMonitoring()
If pWatcher Is Nothing Then
Set pWatcher = New clsProjectWatcher
MsgBox “プロジェクト・リアルタイム監視を開始しました。”, vbInformation
End If
End Sub
Public Sub StopProjectMonitoring()
Set pWatcher = Nothing
MsgBox “監視を停止しました。”, vbInformation
End Sub
—
4. 現場で差がつく「設計の極意」
4.1. パフォーマンスの重みを知れ
Projectの`Recalculate`は非常に重い処理だ。数百のタスクがあるプロジェクトで、1文字打つたびに再計算を走らせれば、ユーザー体験は最悪になる。
- 対策: `App_ProjectAssignmentChange` 内で、変更されたフィールドが `ActualWork` であるかどうかを厳密に判定せよ。
4.2. 外部データベース連携のタイミング
実績工数を入力した瞬間にSQL ServerやOracleへ書き込みに行くのは非効率だ。
- ベストプラクティス: `Project_BeforeSave` イベントを利用し、変更があったアサインメントのIDをコレクションに保持しておき、保存時に一括でUpdateをかける「遅延書き込み」を採用せよ。
4.3. ステータス日付(Status Date)の意識
実績工数を入れた際、その実績は「いつ」行われたものか? Projectには「ステータス日付」という概念がある。
実績を入力した際、「ステータス日付以前に実績を固定し、ステータス日付以降に残工数を再スケジュールする」処理をコードに組み込まなければ、過去の期間に残工数が取り残されるという、実務上ありえないスケジュールが出来上がってしまう。
‘ 残工数をステータス日付以降に移動させる(プロの書き方)
pj.StatusDate = Date ‘ 本日を基準とする
asgn.Task.RescheduleWork (pj.StatusDate)
—
5. 終わりに
Project VBAは、単なる自動化ツールではない。それは「プロジェクトの未来を予測するエンジン」を構築する行為だ。
今回紹介したイベント制御と再入防止のテクニックは、堅牢なシステムを構築するための最低限の作法に過ぎない。君が次に挑むべきは、入力された実績の傾向から、将来の遅延リスクを確率的に算出するモンテカルロ・シミュレーションの統合かもしれない。
コードの向こう側にいるPM(プロジェクトマネージャー)が、そのツールによって「今、何をすべきか」を即座に判断できるか。常にその視点を忘れないでほしい。
健闘を祈る。
