計算エンジンの呪縛を解く:MS Projectにおけるカスタムフィールド更新の極限最適化
MS Projectを用いた大規模プロジェクト管理システムの構築において、シニアエンジニアが必ず直面する「壁」がある。それは、数千、数万のタスクやリソースが複雑に絡み合った依存関係(Dependency Tree)の中で、カスタムフィールドを更新しようとした瞬間に発生する、絶望的なまでのパフォーマンスの低下だ。
多くの開発者は、`Application.Calculation = pjCalculationManual` を設定することで、この問題を解決したつもりになる。しかし、それはあくまで「初歩的な回避策」に過ぎない。計算エンジンが持つ再帰的な計算トリガー、COMインターフェースを介したオブジェクトへのアクセスオーバーヘッド、そしてメモリ上に展開される計算ツリーの肥大化。これらを知り尽くさない限り、真に堅牢で高速なシステムは構築できない。
本稿では、Projectの計算エンジンを実質的に「バイパス」し、計算負荷を最小化しながらカスタムフィールドを高速に書き込む、アーキテクトレベルの知見を伝授する。
1. なぜ「Manual Calculation」だけでは不十分なのか
Projectの計算エンジンは、単なる計算機ではない。あるフィールドの値が書き換わった瞬間、そのフィールドを参照しているすべての依存関係をスキャンし、再計算の必要性を判定する「インテリジェントなエンジン」だ。
問題は、カスタムフィールドに「数式(Formula)」が設定されている場合、あるいは「数式を参照している別のフィールド」が存在する場合である。たとえ `pjCalculationManual` に設定していても、COMオブジェクトを通じて値を書き込む操作そのものが、内部的なイベント(`OnFieldChange` 等)を誘発し、エンジンの再計算準備プロセスを走らせてしまうことがある。
特に、大量のタスクに対してループ処理を行う際、この「微細なトリガー」の累積が、指数関数的な遅延を招く。我々が目指すべきは、エンジンの「思考」を停止させ、純粋な「データ書き込み」のみを完遂させることである。
2. 戦略的アプローチ:計算ツリーのデカップリング
極限のパフォーマンスを引き出すための戦略は、以下の3点に集約される。
1. データ型の分離(Decoupling Data Types): 計算式を持つフィールド(Formula Field)に直接値を書き込もうとするのは愚策である。書き込み専用の「Raw Dataフィールド(TextやNumber)」を用意し、計算は一括で行う。
2. 書き込みのバッチ化(Batching Updates): プロパティへのアクセス回数を最小化する。
3. UIスレッドの凍結(UI Thread Freezing): Windows APIを用いて、描画更新によるオーバーヘッドを物理的に遮断する。
3. 実装:極限のフィールド更新エンジン
以下に、大規模データ処理に特化した、カスタムフィールド更新用のコアモジュールを示す。ここでは、単なるVBAの範疇を超え、メモリ管理とプロセス制御を意識した設計を行っている。
Option Explicit
' Windows API: 描画更新の強制停止(UIのフリーズによる高速化)
' 非常に強力だが、使用後は必ず解除しなければならない。
Private Declare PtrSafe Function LockWindowUpdate Lib "user32" (ByVal hwndLock As LongPtr) As Long
''' <summary>
''' Projectの計算エンジンと描画プロセスを制御し、
''' カスタムフィールドを極限まで高速に更新する。
''' </summary>
Public Sub HighSpeedFieldUpdate()
Dim app As MSProject.Application
Set app = MSProject.Application
Dim t As Task
Dim targetField As Long
' 例として Text1 を使用。実際にはプロジェクト定義に合わせた定数を使用すること
targetField = pjText1
On Error GoTo ErrorHandler
' --- 1. 環境の凍結 ---
' 計算モードをマニュアルへ。これだけでは不十分。
app.Calculation = pjCalculationManual
app.ScreenUpdating = False
' Windows APIによるウィンドウロック(描画リソースの解放)
' Projectのメインウィンドウハンドルを取得してロックする
LockWindowUpdate app.hWnd
' --- 2. 高速書き込みフェーズ ---
' オブジェクトの参照コストを下げるため、TaskCollectionを直接回す
' 大規模データの場合、Taskオブジェクトの生成を最小限にする必要がある
Debug.Print "Update Started: " & Now
' 実践的なテクニック:
' 計算式を含むフィールドではなく、必ず「計算結果を格納するための純粋なText/Numberフィールド」を更新すること
For Each t In app.ActiveProject.Tasks
If Not t Is Nothing Then
' 直接プロパティにアクセス。
' ここで複雑なロジックを挟む場合は、事前にメモリ上の配列(Variant Array)で
' 計算を済ませてから、一気に書き込むのが定石である。
t.Text1 = "DATA_" & t.ID ' 擬似的なデータ更新
End If
Next t
' --- 3. 環境の復元と再計算のトリガー ---
' 全ての書き込みが完了した「後」に、一度だけ計算を許可する
app.Calculation = pjCalculationAutomatic
app.ScreenUpdating = True
LockWindowUpdate 0 ' APIによるロック解除
Debug.Print "Update Completed: " & Now
MsgBox "更新完了。計算エンジンを再起動しました。", vbInformation
Exit Sub
ErrorHandler:
' エラー発生時でも、必ずロックを解除し、計算モードを戻す(極めて重要)
app.Calculation = pjCalculationAutomatic
app.ScreenUpdating = True
LockWindowUpdate 0
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
4. アーキテクトの視点:メモリとライフサイクル
シニアエンジニアがコードを読む際、単に「動くか」ではなく「リソースをどう扱っているか」を見る。
オブジェクトの明示的解放
上記のコードでは `For Each t In app.ActiveProject.Tasks` を使用しているが、もしループ内でさらに `Resource` オブジェクトや、別の `Sub` を呼び出してオブジェクトを生成している場合、ループの終わりに `Set t = Nothing` を明示的に行う、あるいはスコープを極限まで狭めることが、メモリリーク(Projectプロセスが肥大化し、最終的にクラッシュする現象)を防ぐ鍵となる。
計算式の設計(The “Shadow Field” Pattern)
私が設計する大規模システムでは、以下のパターンを徹底する。
1. `Field_Raw` (Text/Number): VBAから直接書き込むフィールド。計算式は一切持たない。
2. `Field_Calculated` (Formula): `[Field_Raw] * 1.1` のような式を持つフィールド。
VBAは `Field_Raw` だけを高速に書き込み、最後に `app.Calculation = pjCalculationAutomatic` を叩くことで、Projectの計算エンジンに「一括での再計算」を委ねる。これにより、書き込み中の「書き込み→即再計算」という最悪の $O(n^2)$ シナリオを回避し、$O(n)$ の線形時間での更新を実現できる。
結論
Project VBAにおけるパフォーマンスの追求は、単なるコードの書き換えではない。それは、「Projectという巨大な計算エンジンの動作原理を理解し、その動作をいかに制御(コントロール)するか」という、システムアーキテクチャの戦いである。
計算エンジンを敵とするのではなく、計算のタイミングを完全に支配下に置くこと。それが、伝説的なシステムを構築するエンジニアに求められる資質である。
