遅延評価の罠を排せ:MS Project VBAにおけるカスタムフィールド再計算の完全制御
プロジェクト・マネジメント・ソフトウェアとしてのMicrosoft Projectは、極めて強力な計算エンジンを内蔵している。しかし、そのエンジンは「怠惰(Lazy)」である。
VBAを通じてカスタムフィールド(Task/Resource Custom Fields)の値を書き換えた際、その値に依存する他の計算式フィールドが即座に更新されない――。この「データの不整合(Stale Data)」は、大規模なスケジューリング・システムを構築するシニアエンジニアが必ず直面する、最も忌むべき問題の一つだ。
本稿では、単なる `Application.Calculate` の呼び出しといった初歩的な手法は一切排除する。オブジェクトのライフサイクル、依存関係ツリーの再構築、そしてWindows APIを介したUIスレッドへの介入まで、Projectの計算エンジンを完全に掌握するための極限の知見を共有する。
1. なぜ `Application.Calculate` では不十分なのか
多くの開発者は、値の更新後に `Application.Calculate` を実行すれば事足りると考える。しかし、これは大規模プロジェクト、特に複雑なカスタムフィールドの数式(Formula)が連鎖している環境下では、しばしば失敗する。
Projectの計算エンジンは、計算コストを最小化するために、変更があったと認識されないノードの再計算をスキップする傾向がある。VBAからプロパティを直接操作した場合、Projectの内部的な「Dirty Flag(再計算が必要であることを示すフラグ)」が適切にセットされないケースが存在するのだ。
結果として、コード上では更新が完了しているはずなのに、後続のロジックでその値を参照すると、古い値が返ってくる。この「非決定論的な挙動」こそが、自動化システムの信頼性を根底から揺るがす。
2. 計算エンジンを強制駆動する3つの戦略
エンジニアは、問題の深刻度とパフォーマンスのトレードオフに応じて、以下の3つのレイヤーを選択しなければならない。
レイヤーI:`ExecuteMso` によるUIコマンドの模倣
最も確実性が高いのは、ユーザーがリボンから「再計算」を実行するのと同等のプロセスを、COMインターフェース経由で叩き込むことだ。
' UIレベルの計算トリガーを強制する
' Application.Calculate よりも強力に依存関係を走査させる
Application.CommandBars.ExecuteMso "Calculate"
`ExecuteMso` は、アプリケーションの内部コマンドを直接呼び出す。これにより、VBAのプロパティ操作によって無視されがちな「計算が必要な状態」が、UIコンテキストを通じて強制的に認識される。
レイヤーII:`Application.Refresh` によるリソース同期
タスクのカスタムフィールドだけでなく、リソース割り当て(Assignment)やリソースの計算フィールドが絡む場合、タスク単体の計算では不十分である。プロジェクト全体の整合性を取るためには、リソース情報の同期を含めたリフレッシュが必要となる。
レイヤーIII:Windows API による描画・メッセージ介入(極限の制御)
極めて稀なケースだが、大量のデータ更新後にProjectのUIスレッドがハングアップしたように見える、あるいは計算結果が画面に反映されない場合、Windows APIを用いてメッセージキューを強制的に処理させる必要がある。
3. 実装:堅牢な再計算制御プロシージャ
以下に、実戦投入可能なレベルの、メモリ管理とエラーハンドリングを徹底した再計算制御モジュールを示す。単に計算するだけでなく、オブジェクトのライフサイクルを管理し、メモリリークを防ぐ設計思想に基づいている。
Option Explicit
' Windows APIの宣言: UIスレッドのメッセージ処理を強制するために使用
' プロジェクトが計算中に「応答なし」に見える現象を回避する
If VBA7 Then
Private Declare PtrSafe Sub DoEvents Lib "kernel32" ()
Else
Private Declare Sub DoEvents Lib "kernel32" ()
End If
''' <summary>
''' プロジェクト全体の整合性を強制的に確保するプロシージャ
''' </summary>
''' <param name="forceFullRefresh">Trueの場合、リソース同期を含むフルリフレッシュを実行</param>
Public Sub ForceRecalculateProject(ByVal forceFullRefresh As Boolean)
Dim app As MSProject.Application
Set app = MSProject fact.Application
On Error GoTo ErrorHandler
' 1. 計算コストの最適化: 画面更新を停止し、処理を集中させる
' ただし、ExecuteMsoを使用する場合はUIスレッドとの兼ね合いに注意が必要
' 2. 計算エンジンの強制トリガー
' 標準的なCalculateではなく、Msoコマンドを使用して依存関係を強制走査
app.CommandBars.ExecuteMso "Calculate"
' 3. 必要に応じたリソース同期
If forceFullRefresh Then
' リソースとタスクの割り当て関係を再計算
app.Refresh
End If
' 4. Windowsメッセージキューの解放
' 大規模な計算直後のメモリ・スタックの安定化を図る
DoEvents
Exit Sub
ErrorHandler:
' ログ出力やエラーハンドリングのロジックをここに記述
Debug.Print "Critical Error in ForceRecalculateProject: " & Err.Description
Err.Raise Err.Number, "ForceRecalculateProject", "Calculation trigger failed."
End Sub
''' <summary>
''' 大規模データ更新時のメモリ最適化を考慮した一括更新パターン
''' </summary>
Public Sub HighPerformanceUpdatePattern()
Dim t As Task
Dim proj As Project
Set proj = MSProject.Application.ActiveProject
' 高速化の定石: 処理中の表示更新を抑制
' ※Project VBAではExcelほど明確なScreenUpdatingはないが、
' 大規模更新時はオブジェクトへのアクセス頻度を最小限に抑える設計が肝要
On Error GoTo Cleanup
' --- データ更新フェーズ ---
For Each t In proj.Tasks
If Not t Is Nothing Then
' 例: Text1に値をセットし、それに基づく計算フィールド Text2 を更新させたい
t.Text1 = "Update_" & t.ID
End If
Next t
' --- 計算強制フェーズ ---
' 更新ループの「外」で一度だけ強力なトリガーをかける
' ループ内で毎回計算を走らせるのは、計算量のオーダーを O(n^2) に跳ね上げる愚策である
ForceRecalculateProject True
Cleanup:
' オブジェクトの明示的な解放
' 循環参照を防ぎ、メモリリークを回避するアーキテクトの作法
Set t = Nothing
Set proj = Nothing
If Err.Number <> 0 Then
MsgBox "Update failed. Error: " & Err.Description, vbCritical
End If
End Sub
4. アーキテクチャ上の要諦:パフォーマンスと整合性の均衡
伝説的なエンジニアが常に意識するのは、「計算のタイミング」である。
1. 計算のバッチ化(Batching):
タスクをループで回しながら、その中で毎回 `Calculate` を呼び出すことは、致命的なパフォーマンス低下を招く。計算エンジンは、グラフ全体の依存関係を解析するため、1回の呼び出しに多大なコストがかかるからだ。更新は「データ入力フェーズ」と「計算フェーズ」に明確に分離せよ。
2. メモリの整合性:
大規模なプロジェクトにおいて、VBAで作成した一時的なオブジェクト(Task, Resource, Assignment等)は、必ず `Set obj = Nothing` によって明示的に解放せよ。ProjectのCOMインターフェースは、参照カウントの管理が極めて繊細であり、不適切な解放はメモリリークだけでなく、計算エンジンの動作不安定化(不可解なクラッシュ)を招く。
3. レガシー環境への配慮:
システム間連携(ExcelからProjectへのデータ流し込みなど)を行う場合、Projectがバックグラウンド(非表示状態)で動作していることが多い。非表示状態での `ExecuteMso` は、環境によっては予期せぬ挙動を示すことがある。その場合は、`Application.Calculate` と `Application.Refresh` を組み合わせた、より低レイヤーなアプローチを検討せよ。
結言
MS Projectのカスタムフィールド制御における真の技術力は、コードの短さではなく、「計算エンジンの内部動作をいかに予測し、制御下に置くか」という点に集約される。
「動かない」のではなく「計算が追いついていない」のだ。その本質を理解したとき、君の自動化システムは、単なるスクリプトから、堅牢なエンタープライズ・ソリューションへと昇華する。
