【テクニカル・上級編】【上級者向け】カスタムフィールドの数式内にVBAのユーザー定義関数(UDF)を擬似的に組み込む – Project VBA解析バイブル

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

MS Projectの限界を突破する:イベント駆動型「擬似UDF」によるカスタムフィールド制御の極致

MS Projectを利用した大規模プロジェクト管理において、標準のカスタムフィールド(Text1, Number1等)に用意された数式エンジンは、あまりにも脆弱だ。複雑なビジネスロジック、外部データベースとの照合、あるいは条件分岐の組み合わせが必要になった瞬間、エンジニアは「数式の壁」に突き当たる。

Excelであればユーザー定義関数(UDF)をセルに直接記述すれば済む話だが、MS ProjectのカスタムフィールドはVBA関数を直接参照できない。この制約は、多くの開発者が「諦める理由」として使ってきた。

しかし、真のアーキテクトは制約を言い訳にはしない。制約を理解し、オブジェクトのライフサイクルとイベントモデルをハックすることで、「カスタムフィールドの中にVBAのロジックが潜んでいる」かのような挙動を実現する。

本稿では、Project VBAにおけるイベント駆動型の「擬似UDF」実装術、およびその実装において回避すべき致命的なパフォーマンス・デッドロックについて、設計思想の深淵まで踏み込んで解説する。

1. アーキテクチャの設計思想:イベント・シンクロナイゼーション

MS Projectには、Excelのような「セルの再計算」という概念ではなく、「プロジェクト全体の計算(Calculate)」というイベントが存在する。

我々が目指すべきは、以下の設計パターンである。

1. Trigger: `Project_Calculate` または `Project_Change` イベントを検知する。
2. Guard: 無限再帰(Infinite Recursion)を防ぐための状態フラグを管理する。
3. Logic: VBAによる高度な計算ロジックを実行する。
4. Sync: 計算結果をカスタムフィールドに書き戻す。

ここで最も重要なのは、「カスタムフィールドへの書き込みが、再びCalculateイベントを誘発する」というProjectの仕様だ。これを制御できなければ、スタックオーバーフローによるアプリケーションのクラッシュ、あるいはメモリの暴走を招く。

2. 実装:堅牢な擬似UDFエンジン

以下に、実戦投入可能なレベルのコード構造を示す。単なるコードの提示ではない。再帰防止、オブジェクトの明示的解放、そして計算範囲の最適化を組み込んだアーキテクチャだ。

Module1: Core Logic (Standard Module)

Option Explicit

' 再帰呼び出しによる無限ループを防止するための静的フラグ
Private IsProcessing As Boolean

''' <summary>
''' プロジェクトの計算イベントをハンドリングするメインエントリポイント
''' </summary>
Public Sub HandleProjectCalculate(ByVal pj As Project)
    ' 既に処理中の場合は即座に抜ける(再帰防止の要)
    If IsProcessing Then Exit Sub
    
    On Error GoTo ErrorHandler
    
    ' 処理開始フラグを立てる
    IsProcessing = True
    
    ' 処理の最適化:画面更新を停止(ProjectにはExcelのScreenUpdatingに相当するプロパティがないため、
    ' 計算負荷を抑えるためにオブジェクト操作を最小限にする設計が必要)
    
    Dim t As Task
    Dim calcResult As Double
    
    ' 全タスクを走査(大規模プロジェクトでは、変更があったタスクのみを特定するロジックへの昇華が望ましい)
    For Each t In pj.Tasks
        ' 空のタスク(削除済みなど)をスキップ
        If Not t Is Nothing Then
            If Not t.Start Is Nothing Then
                
                ' --- 擬似UDFロジックの開始 ---
                ' ここに複雑な計算を記述。例:コストと期間に基づく独自の重み付け計算
                calcResult = ExecuteComplexLogic(t)
                ' --- 擬似UDFロジックの終了 ---
                
                ' 計算結果をカスタムフィールド(Number1)に書き戻す
                ' ※ここでの書き込みが再びCalculateイベントをトリガーする
                If t.Number1 <> calcResult Then
                    t.Number1 = calcResult
                End If
                
            End If
        End If
    Next t

CleanUp:
    ' 処理終了フラグを戻す
    IsProcessing = False
    Exit Sub

ErrorHandler:
    ' 実運用ではログ出力やエラー管理システムへの連携を行う
    Debug.Print "Error in HandleProjectCalculate: " & Err.Description
    Resume CleanUp
End Sub

''' <summary>
''' 複雑なビジネスロジックをカプセル化した関数(これが擬似UDFの本体)
''' </summary>
Private Function ExecuteComplexLogic(ByRef targetTask As Task) As Double
    ' パフォーマンス最適化のため、引数はByRefで渡し、オブジェクトのコピーを避ける
    ' ここでは例として、タスクの工数とリソースコストを組み合わせた計算を行う
    
    Dim cost As Double
    Dim work As Double
    
    work = targetTask.Work ' [Hour]
    
    ' リソース情報の取得(ここでのオブジェクトアクセスはコストが高い)
    ' 必要最小限のプロパティのみにアクセスする
    On Error Resume Next
    cost = targetTask.Cost
    If Err.Number <> 0 Then cost = 0
    On Error GoTo 0
    
    ' 独自の非線形計算アルゴリズム
    ExecuteComplexLogic = (work * 0.7) + (cost * 0.3)
End Function

ThisProject: Event Binding

Option Explicit

''' <summary>
''' ProjectのCalculateイベントをフックする
''' </summary>
Private Sub Project_Calculate()
    ' 標準モジュールのハンドラを呼び出す
    Call HandleProjectCalculate(Me)
End Sub

3. 極限の最適化:シニアエンジニアが注視すべき3つの領域

この実装を大規模なエンタープライズ環境(数千タスク、数百リソース)で運用する場合、単純な実装ではパフォーマンスの壁にぶつかる。

① オブジェクトのライフサイクルとメモリ管理

VBAにおいて、`Task` オブジェクトや `Resource` オブジェクトの参照はメモリを消費する。ループ内で大量のオブジェクトを生成・操作する場合、スコープを限定し、不要になったオブジェクトは明示的に `Set t = Nothing` する習慣を持つべきだが、Projectのコレクション走査においては、コレクション自体がオブジェクトを保持しているため、「いかに新しいオブジェクトを作成せずに、既存のプロパティのみを読み書きするか」が鍵となる。

② 計算範囲の限定(Delta Calculation)

上記のコードは、プロジェクトのあらゆる計算(日付変更、リソース割り当て変更など)のたびに全タスクを走査する。これは非効率の極みだ。
真に高度なシステムでは、`Project_Change` イベントを利用し、「どのプロパティが変更されたか」を判定、あるいは「現在選択されているタスク(`ActiveSelection`)」のみを対象にするなど、計算のデルタ(差分)を特定するロジックを組み込む。

③ Windows APIによるプロファイリング

計算負荷が問題となる場合、Windows APIの `GetTickCount` を用いて、ロジックの実行時間をマイクロ秒単位で計測し、ボトルネックとなっているタスク属性を特定する仕組みを構築せよ。

' Windows APIの宣言例

If VBA7 Then

Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long

Else

Declare Function GetTickCount Lib "kernel32" () As Long

End If

4. 結論:制約を支配する

MS Projectのカスタムフィールドに数式が書けないという事実は、エンジニアにとって「不自由」ではなく、「制御権の移譲」である。

イベントモデルを正しく理解し、再帰の罠を回避するガードレールを敷き、オブジェクトの挙動を完全に掌握したとき、あなたはProjectの標準機能を超えた、独自の「業務特化型プロジェクト管理エンジン」を構築することができる。

技術の真髄は、仕様の隙間にこそ存在する。

*執筆者: Chief Architect*

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