【テクニカル・上級編】【中級者向け】カスタムフィールドの数式にVBA関数を組み込む際の限界と回避策 – Project VBA解析バイブル

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

Beyond the Formula: MS Projectにおける「ハイブリッド計算」の設計思想

MS Projectのカスタムフィールド(Custom Fields)には、強力な数式(Formula)エンジンが備わっている。しかし、プロジェクトの規模が数千タスクを超え、ロジックが単なる算術演算を超えて「外部データとの照合」や「複雑な条件分岐」、「再帰的な依存関係の判定」を必要とし始めたとき、組み込みの数式エンジンは無力なガラスの天井となる。

多くの開発者は、この壁に突き当たった際に、無理に数式を複雑化させてパフォーマンスを劣化させるか、あるいは諦めて手動更新へと逃げてしまう。だが、真のアーキテクトが取るべき道は、「計算ロジックを数式から切り離し、VBAによる制御下に移譲する」ことである。

本稿では、Projectの数式エンジンの限界を定義し、VBAを用いてカスタムフィールドに値を「注入」する、いわゆる「ハイブリッド計算(Hybrid Calculation)」の設計思想と、その実装における極限の最適化手法について解説する。

1. 組み込みFormulaエンジンの限界と死角

ProjectのFormulaプロパティは、あくまでProject内部のオブジェクトモデル(`Task`, `Resource`等)に基づいた限定的な式評価器である。以下のケースにおいて、Formulaは決定的に破綻する。

1. 外部コンテキストの欠如: SQL Serverの特定テーブルの値や、Web APIから取得した最新の単価情報を参照することは不可能である。
2. 計算コストの指数関数的増大: 数式内で複雑な`IIf`関数を多用したり、他のタスクの属性を動的に参照したりすると、再計算(Recalculation)のたびに計算グラフ全体が走り、UIのフリーズを招く。
3. 非決定的なロジック: 外部のDLL呼び出しや、複雑なアルゴリズム(例:モンテカルロ・シミュレーションに基づくリスク値の算出)を組み込むことはできない。

これらを解決するために、我々は「Formulaに計算させる」のではなく、「VBAで計算した結果を、静的な値としてCustomFieldに書き込む」というアプローチを採用する。

2. ハイブリッド計算のアーキテクチャ設計

ハイブリッド計算を実装する際、単に`For Each`でタスクを回すだけでは、プロフェッショナルの仕事とは言えない。以下の3つの設計原則を遵守すべきである。

I. 計算(Logic)と書き込み(I/O)の分離

計算ロジックを、Projectオブジェクトモデルに依存しない純粋な関数(Pure Function)として分離せよ。これにより、単体テストが可能になり、将来的なVB.NETへの移行や、Web API化へのパスが確保される。

II. 最小限のオブジェクト操作

ProjectのCOMオブジェクト(`Task`オブジェクト等)へのアクセスは非常に高コストである。ループ内でプロパティを読み書きする回数を極限まで減らすことが、パフォーマンスの鍵となる。

III. 状態の決定論的制御

計算を実行するタイミングを制御せよ。Projectの「再計算」イベントに紐付けるのは危険だ。計算が走るたびに大規模なロジックがトリガーされると、ユーザーの操作性を著しく損なう。明示的な「更新ボタン」または「特定のイベント」による実行を推奨する。

3. 実装の極致:高精度プロファイリングとメモリ管理

以下に、大規模プロジェクトでも耐えうる、ハイブリッド計算の実装パターンを示す。ここでは、Windows APIを用いて計算時間を高精度に計測し、メモリリークを防ぐためのオブジェクト解放を徹底したコードを提示する。

Option Explicit

' Windows API: 高精度タイマーを使用して計算パフォーマンスを測定する
Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCounter As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long

''' <summary>
''' プロジェクト全体のカスタムフィールドをハイブリッド計算で更新する
''' </summary>
Public Sub ExecuteHybridCalculation()
    Dim startTime As Currency
    Dim endTime As Currency
    Dim freq As Currency
    Dim taskObj As Task
    Dim totalTasks As Long
    
    ' 高精度タイマーの準備
    QueryPerformanceFrequency freq
    QueryPerformanceCounter startTime
    
    ' パフォーマンス最適化: 画面更新と再計算の停止
    ' これを行わないと、書き込みのたびにProjectの計算エンジンが走り、致命的な遅延を招く
    On Error GoTo ErrorHandler
    Application.ScreenUpdating = False
    Application.Calculation = pjManual 
    
    ' 1. データの事前準備(例:外部DBやAPIからのデータ取得を想定)
    ' ここでは擬似的にDictionaryを使用
    Dim externalData As Object
    Set externalData = GetExternalReferenceData() ' 外部データ取得関数
    
    ' 2. 計算ループの実行
    totalTasks = ActiveProject.Tasks.Count
    
    ' 効率的なループ: For Eachは便利だが、インデックス制御の方が
    ' 特定の条件下ではメモリ管理の観点で明示的である
    Dim i As Long
    For i = 1 To totalTasks
        Set taskObj = ActiveProject.Tasks(i)
        
        ' Nullタスク(空行)のスキップ
        If Not taskObj Is Nothing Then
            ' --- 計算ロジックの分離 ---
            ' 複雑なロジックは外部関数へ。ここでは「計算」と「書き込み」を分ける
            Dim calculatedValue As Double
            calculatedValue = CalculateComplexLogic(taskObj, externalData)
            
            ' --- 書き込み(I/O) ---
            ' Text1, Number1 などのカスタムフィールドへ直接値を注入
            ' 数式(Formula)は空にしておくことが重要。そうしないと数式と値が衝突する
            taskObj.Text1 = "Hybrid_Calc" ' 識別用フラグ
            taskObj.Number1 = calculatedValue
        End If
        
        ' オブジェクトの明示的な解放(大規模ループでのメモリ圧迫防止)
        Set taskObj = Nothing
    Next i

    ' 後処理
    Application.Calculation = pjAutomatic
    Application.ScreenUpdating = True
    
    ' 経過時間の算出
    QueryPerformanceCounter endTime
    MsgBox "計算完了。" & vbCrLf & _
           "処理タスク数: " & totalTasks & vbCrLf & _
           "実行時間: " & Format((endTime - startTime) / freq, "0.000") & " 秒", vbInformation

CleanUp:
    Set externalData = Nothing
    Exit Sub

ErrorHandler:
    Msgের "致命的なエラーが発生しました: " & Err.Description
    Resume CleanUp
End Sub

''' <summary>
''' 外部データ(DBやAPI)をシミュレートして取得する
''' </summary>
Private Function GetExternalReferenceData() As Object
    ' 実務ではここでの MSXML2.XMLHTTP や ADODB.Connection の使用が想定される
    Dim dict As Object
    Set dict = CreateObject("Scripting.Dictionary")
    dict.Add "Project_Alpha", 1.25
    dict.Add "Project_Beta", 0.95
    Set GetExternalReferenceData = dict
End Function

''' <summary>
''' 純粋な計算ロジック(Pure Function)
''' </summary>
Private Function CalculateComplexLogic(t As Task, data As Object) As Double
    ' ここに数式では不可能な、複雑な条件分岐や外部データ参照を記述する
    ' 例: タスク名に特定のキーワードが含まれ、かつ外部データが特定の条件を満たす場合
    Dim multiplier As Double
    multiplier = 1.0
    
    If data.Exists(t.Name) Then
        multiplier = data(t.Name)
    End If
    
    ' 複雑な計算例
    CalculateComplexLogic = t.Duration * multiplier * 0.5
End Function

4. エンジニアが陥る「メモリの罠」と回避策

大規模なプロジェクトを扱う際、最も恐ろしいのは「COMオブジェクトのリーク」である。

Project VBAにおいて、`Set taskObj = ActiveProject.Tasks(i)` のようにオブジェクトを割り当てた際、ループの終わりに `Set taskObj = Nothing` を明示的に行わなければ、参照カウントが適切に減少しない場合がある。特に、`Task` オブジェクトを内部の `Resource` オブジェクトや、さらにその先の `Assignment` オブジェクトへと辿っていくようなネストされた処理を行う場合、メモリ使用量は指数関数的に増大し、最終的に `Out of Memory` でシステムがクラッシュする。

プロフェッショナルの鉄則:

  • スコープを最小限にする: オブジェクトの生存期間(Lifetime)を、その計算に必要な最小限のブロック内に閉じ込める。
  • 大量データの集計には配列(Array)を使え: `Task` オブジェクトを直接触るのではなく、一度必要なプロパティ(`Start`, `Finish`, `Duration`等)をすべて `Variant` 型の配列に吸い上げ、メモリ上の配列に対して計算を行ってから、最後に一括して書き戻す。これが、1万タスクを超えるプロジェクトにおける唯一の正解である。

5. 結論:アーキテクトとしての視座

MS Projectのカスタムフィールドを単なる「計算結果の置き場」と見るか、「ロジックの実行環境」と見るか。この視点の差が、システムの堅牢性と拡張性を決定づける。

組み込みのFormulaは、使い捨ての簡易的な計算には最適である。しかし、企業の基幹システムとして、あるいは高度なプロジェクト管理ツールとして運用するのであれば、計算ロジックをVBA(あるいはそれ以上のレイヤー)へ引き出し、「計算の決定論的制御」を実現せよ。

メモリ、時間、そしてオブジェクトのライフサイクル。これらを完全に掌握したとき、君の書くコードは単なるスクリプトを超え、真の「システム」へと昇華する。

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