【テクニカル・上級編】【中級者向け】カスタムフィールドの計算結果を静的な値に変換して保存する – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

動的計算の罠を回避せよ:MS Projectにおける「計算値の静的スナップショット」実装の極意

MS Projectにおけるカスタムフィールドの運用において、多くのエンジニアが陥る致命的な設計ミスがある。それは、「計算式(Formula)による動的な値」を、そのまま履歴管理や報告用データとして扱おうとすることだ。

カスタムフィールドに記述された数式は、プロジェクトのスケジュール、リソースのコスト、あるいは日付が変更されるたびに、その「結果」を再計算し、過去の値を上書きしてしまう。これは、プロジェクト管理における「時点(Snapshot)」の概念を根底から破壊する行為だ。

本稿では、シニアエンジニアやシステム管理者が直面する「計算結果の固定化(Static Value Conversion)」という課題に対し、単なるループ処理を超えた、メモリ効率と堅牢性を極限まで高めたアーキテクチャを提示する。

1. 課題の真髄:揮発性データからの脱却

MS Projectのカスタムフィールドには、大きく分けて2つの性質が存在する。

1. Dynamic (Formula-driven): 数式によって常に最新の状態を反映する。
2. Static (Value-driven): 手入力、あるいは一度書き込まれたら、明示的な操作なしには変化しない。

「先月末時点でのコスト見積もり」や「ベースライン承認時のリソース負荷」を記録したい場合、Dynamicなフィールドの結果を、特定のタイミングでStaticなフィールドへと「昇華(Freeze)」させるプロセスが不可欠となる。

このとき、単に `Task.Text1 = Task.Text2` と記述するだけでは、数千、数万のタスクを扱う大規模プロジェクトにおいては、COMオブジェクトのオーバーヘッドによるパフォーマンス低下、あるいはメモリリークによるアプリケーションの不安定化を招く。

2. 高度な実装戦略:パフォーマンスと整合性の両立

大規模なデータを扱う際、我々が考慮すべきは以下の3点である。

A. COMインターフェースの呼び出し回数の最小化

VBAからMS Projectのオブジェクトモデルを操作する際、プロパティへのアクセスはすべてCOM(Component Object Model)を介したプロセス間通信に近いコストを伴う。ループ内で不必要に `ActiveProject` や `Application` オブジェクトを再参照することは、パフォーマンスに対する自殺行為である。

B. 計算エンジンの制御

値を書き込む際、MS Projectの再計算エンジンが動いてしまうと、書き込み操作のたびにプロジェクト全体のリキャルキュレーションが走り、指数関数的に処理時間が増大する。`Calculation = pjManual` への切り替えは、必須の作法である。

C. メモリのライフサイクル管理

特に外部アプリケーション(Excelや独自の実行ファイル)からMS Projectを操作する場合、オブジェクトの解放が不適切だと、Projectのプロセスがバックグラウンドに残り続け、メモリを食いつぶす。`Set obj = Nothing` は基本だが、それ以上に「オブジェクトの参照範囲を最小限に留める」設計が求められる。

3. 実装例:Snapshot Engine

以下に、タスクのカスタムフィールドにおける「計算値から静的値への転送」を、プロフェッショナルなレベルで実装したコードを示す。

Option Explicit

' ==============================================================================
' Module: modSnapshotEngine
' Description: MS Project Custom Field Snapshot Engine
' Author: Legendary Chief Architect
' ==============================================================================

''' <summary>
''' 指定されたカスタムフィールドの計算結果を、別の静的フィールドへ一括コピーする。
''' </summary>
''' <param name="sourceField">コピー元となるフィールド(計算式が入っているフィールド)</param>
''' <param name="targetField">コピー先となるフィールド(値を固定するためのフィールド)</param>
Public Sub SnapshotTaskFields(ByVal sourceField As CustomField, ByVal targetField As CustomField)
    Dim proj As Project
    Dim t As Task
    Dim startTime As Double
    
    ' オブジェクトのセットアップ
    Set proj = ActiveProject
    startTime = Timer

    On Error GoTo ErrorHandler

    ' --- パフォーマンス最適化のフェーズ ---
    ' 1. 再計算を停止し、描画更新を抑制する
    Application.Calculation = pjManual
    Application.ScreenUpdating = False
    
    ' 2. 画面のちらつきを抑え、処理速度を向上させるためのステータス表示
    Application.StatusBar = "Snapshot process started... Please wait."

    ' --- メイン処理フェーズ ---
    ' TaskCollectionを直接ループ。大規模データの場合、この反復がボトルネックとなる。
    ' COMのオーバーヘッドを最小化するため、Taskオブジェクトを一度だけ取得する。
    For Each t In proj.Tasks
        ' 空白タスク(サマリータスク等を含む)のハンドリング
        If Not t Is Nothing Then
            ' 計算結果(Formulaの結果)を静的な値(Value)として転送
            ' ここでは Text1 と Text2 を例とするが、引数により動的に制御可能
            ' プロパティへのアクセスを最小限に留める
            TransferValue t, sourceField, targetField
        End If
    Next t

    ' --- クリーンアップフェーズ ---
    ' 計算モードを自動に戻す
    Application.Calculation = pjAutomatic
    Application.ScreenUpdating = True
    Application.StatusBar = False
    
    MsgBox "Snapshot completed in " & Round(Timer - startTime, 2) & " seconds.", vbInformation

ExitSub:
    ' 明示的なオブジェクト解放
    Set proj = Nothing
    Exit Sub

ErrorHandler:
    Application.Calculation = pjAutomatic
    Application.ScreenUpdating = True
    MsgBox "Critical Error in Snapshot Engine: " & Err.Description, vbCritical
    Resume ExitSub
End Sub

''' <summary>
''' 単一タスクに対する値の転送ロジック。
''' 内部関数として切り出すことで、将来的な型チェックやログ出力を容易にする。
''' </summary>
Private Sub TransferValue(ByRef t As Task, ByVal src As CustomField, ByVal tgt As CustomField)
    ' 実務上の注意: CustomFieldの型(Text, Number, Date)に合わせた適切なプロパティ選択が必要
    ' ここでは汎用性の高い Text プロパティを例示する
    
    ' 実際の運用では、src.Type に基づいて Switch 文などで分岐させるのがアーキテクトの流儀である
    Select Case src.Type
        Case pjCustomFieldType: ' Text系
            t.Text(tgt.FieldNumber) = t.Text(src.FieldNumber)
        Case pjCustomFieldTypeNumber: ' 数値系
            t.Number(tgt.FieldNumber) = t.Number(src.FieldNumber)
        Case pjCustomFieldTypeDate: ' 日付系
            t.Date(tgt.FieldNumber) = t.Date(src.FieldNumber)
        ' 必要に応じて追加
    End Select
End Sub

4. 極限の知見:レガシー環境とシステム間連携における注意点

このコードを実戦投入する際、シニアエンジニアが留意すべき「現場の真実」を伝えておく。

Windows APIによるメモリ監視の必要性

もしこの処理を、Excel VBAから `CreateObject(“MSProject.Application”)` を用いて外部制御している場合、エラーハンドリングを誤ると、エラー発生時にProjectのインスタンスがメモリ上に「ゾンビ」として残り続ける。
これは、Windows APIの `GetProcessMemoryInfo` 等を用いて、プロセスが正常に終了したか、あるいはメモリが異常に膨張していないかを監視する監視ロジックを、呼び出し側のシステムに組み込むべきであることを意味する。

データ整合性の保証(Transactional Integrity)

上記のコードは、途中でエラーが発生した場合、それまでの書き込みをロールバックする機能を持っていない。
真にクリティカルなミッション(例:財務報告用データの作成)においては、まず「一時的な作業用プロジェクト」にデータをコピーし、そこでスナップショット処理を完遂させた後、メインプロジェクトへインポートするという、二段構えのトランザクション設計を推奨する。

スケールへの配慮

数万タスク規模のプロジェクトでは、`For Each t In proj.Tasks` のループ自体が限界を迎えることがある。その場合、タスクのインデックスを `For i = 1 To proj.Tasks.Count` で回す方が、特定のCOM実装においては高速な場合がある。また、`DoEvents` を適宜挟むことで、OSレベルでのハングアップを防ぎ、ユーザーへの応答性を確保する配慮も忘れてはならない。

結論

カスタムフィールドの動的な値を静的に固定する作業は、一見単純なコピー作業に見える。しかし、それを「堅牢なシステム」として実装するためには、MS Projectの内部挙動、COMの特性、そしてメモリ管理への深い理解が不可欠だ。

「動くコード」を書くのは初級者だ。「止まらない、狂わない、そして速いコード」を書くことこそが、我々プロフェッショナルに課せられた責務である。

タイトルとURLをコピーしました