【テクニカル・上級編】実務中級:Excelの「制約条件マスタ」からProjectへ情報を同期するデータ連携ツール – Project VBA解析バイブル

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

【極限の知見】MS Project 制御の真髄:Excel制約条件マスタの同期エンジンにおけるCOM最適化

プロジェクト管理の現場において、Excelは「思考と定義の場」であり、MS Projectは「計算とシミュレーションの場」である。この二つの異なる性質を持つアプリケーションを、VBAという細いパイプラインで接続する際、多くの開発者が致命的な設計ミスを犯す。

単純なループ処理によるデータの流し込み。それは、一見動作するように見えて、大規模なスケジュールにおいては「計算エンジンの無限再計算」と「COMオブジェクトのメモリリーク」という二つの地獄を招く。

本稿では、Excelの制約条件マスタからProjectのTaskオブジェクトへ、極めて高精度かつ低負荷にデータを同期するための、アーキテクチャレベルの知見を提示する。

1. 破壊的な計算エンジン:なぜ単純なループではいけないのか

MS Projectの最大の特徴であり、最大の弱点は、その動的なスケジュール計算エンジンにある。

タスクの制約条件(`ConstraintType`)を変更した瞬間、Projectはプロジェクト全体のクリティカルパスと日付を再計算しようとする。もし、1,000個のタスクに対してループ内で一つずつ制約条件を書き換えていくならば、Projectは1,000回の全プロジェクト再計算を試みる。これは計算量の爆発(Computational Explosion)を意味し、アプリケーションのフリーズ、あるいはメモリ不足によるクラッシュを誘発する。

制御の鉄則

同期処理を行う際は、必ず計算モードを「手動(Manual)」に切り替え、一連の書き込みが完了した後に一度だけ再計算を走らせる。これが、エンジニアとしての最低限の礼儀である。

2. COMオブジェクトのライフサイクル管理

VBAから外部アプリケーション(MS Project)を操作する場合、我々は「COM(Component Object Model)」というプロトコルを通じて、リモートプロセスを操作している。

ここで最も警戒すべきは、「参照カウントの不一致によるゴーストプロセス」だ。エラーが発生した際にオブジェクトの解放(`Set obj = Nothing`)を怠ると、Excelを閉じても `MSProject.exe` がタスクマネージャーに居座り続ける。これはメモリの断片化を招き、長期間稼働する社内システムにおいては致命的な不安定要素となる。

3. 実装:高信頼性制約条件同期エンジン

以下に、実務に耐えうる設計思想に基づいたコードを示す。単なるコードの提示ではない。エラーハンドリング、計算制御、そしてメモリ管理を統合した「同期エンジン」の雛形である。

Option Explicit

' --- Windows API: プロセス制御や高精度な時間計測が必要な場合に使用 ---
' 今回はパフォーマンス計測のために、高精度タイマーの概念を念頭に置く
Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long

''' <summary>
''' Excelの制約条件マスタをMS Projectへ同期する同期エンジン
''' </summary>
Public Sub SynchronizeConstraintsFromExcel()
    Dim projApp As MSProject.Application
    Dim targetWb As Workbook
    Dim wsMaster As Worksheet
    Dim lastRow As Long, i As Long
    Dim taskID As Long
    Dim constraintStr As String
    Dim constraintType As MSProject.TaskConstraintType
    Dim startTime As Long
    
    ' パフォーマンス計測開始
    startTime = GetTickCount()
    
    Set targetWb = ThisWorkbook
    Set wsMaster = targetWb.Sheets("ConstraintMaster")
    
    ' 1. MS Projectへの接続と状態制御
    On Error Resume Next
    Set projApp = GetObject(, "MSProject.Application")
    If projApp Is Nothing Then
        MsgBox "MS Projectが起動していません。", vbCritical
        Exit Sub
    End If
    On Error GoTo ErrorHandler

    ' 2. 【極めて重要】計算エンジンの停止
    ' これを行わない限り、大規模プロジェクトでは処理が完了しない
    projApp.Calculation = pjManual
    
    ' 画面更新の停止(描画負荷の軽減)
    projApp.ScreenUpdating = False

    lastRow = wsMaster.Cells(wsMaster.Rows.Count, "A").End(xlUp).Row

    ' 3. 同期ループ
    For i = 2 To lastRow ' 1行目はヘッダーと想定
        taskID = wsMaster.Cells(i, 1).Value
        constraintStr = wsMaster.Cells(i, 2).Value ' 例: "ASAP", "ALAP", "StartNoEarlierThan"
        
        ' 制約条件文字列をProjectの列挙型に変換
        constraintType = MapStringToConstraint(constraintStr)
        
        ' Project側のタスクに対して適用
        ' TaskIDによる直接アクセスは、インデックスによるアクセスより堅牢である
        On Error Resume Next
        With projApp.ActiveProject.Tasks(taskID)
            If Not .IsNothing Then
                .ConstraintType = constraintType
                ' 必要に応じて日付(ConstraintDate)も同期するロジックをここに含める
            Else
                Debug.Print "Task ID " & taskID & " が見つかりません。"
            End If
        End With
        If Err.Number <> 0 Then
            Debug.Print "Error at Task ID " & taskID & ": " & Err.Description
            Err.Clear
        End If
        On Error GoTo ErrorHandler
    Next i

    ' 4. 計算エンジンの再開と再計算の実行
    projApp.Calculation = pjAutomatic
    projApp.Calculate ' 全タスクの整合性を確保
    projApp.ScreenUpdating = True

    MsgBox "同期完了。経過時間: " & (GetTickCount() - startTime) & " ms", vbInformation

CleanUp:
    ' 5. 【極めて重要】オブジェクトの明示的解放
    ' 参照を確実に切り、メモリ上のゴーストプロセスを防ぐ
    Set projApp = Nothing
    Exit Sub

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

''' <summary>
''' Excel上の文字列をMS ProjectのTaskConstraintType列挙型にマッピングする
''' </summary>
Private Function MapStringToConstraint(ByVal strType As String) As MSProject.TaskConstraintType
    Select Case UCase(Trim(strType))
        Case "ASAP": MapStringToConstraint = pjTaskConstraintAsSoonAsPossible
        Case "ALAP": MapStringToConstraint = pjTaskConstraintAsLateAsPossible
        Case "SNET": MapStringToConstraint = pjTaskConstraintStartNoEarlierThan
        Case "SFN":  MapStringToConstraint = pjTaskConstraintStartNoFinish
        Case "SNO":  MapStringToConstraint = pjTaskConstraintStartNoFinish
        ' 必要に応じて追加
        Case Else
            MapStringToConstraint = pjTaskConstraintAsSoonAsPossible
    End Select
End Function

4. アーキテクトの視点:さらなる最適化への道

上記のコードは、実務における「標準的な堅牢性」を備えている。しかし、シニアエンジニアとして、さらにその先を目指すなら、以下の3点に留意せよ。

I. データバッチングの検討

もし同期対象が数万件に及ぶ場合、COM経由の1件ずつのプロパティ書き込みすらオーバーヘッドとなる。この場合、Excel側でデータを一時的に配列(Variant Array)に格納し、Project側へは「カスタムフィールド」や「一時的な外部データ接続」を経由して、バルクインサート(一括挿入)に近い挙動を模索すべきである。

II. エラーの局所化(Error Isolation)

上記のコードでは `On Error Resume Next` を局所的に使用している。これは、「1つのタスクの欠落が、システム全体の同期失敗を招いてはならない」という設計思想に基づいている。データの整合性と、処理の継続性のバランスをどう取るか。これは業務要件に依存する。

III. レガシー環境への配慮

社内システムにおいては、MS Projectのバージョンが混在していることが多々ある。列挙型(`pjTaskConstraintType`)の定数値がバージョン間で変更されることは稀だが、APIの挙動が変わる可能性は常に考慮せよ。定数は直接数値を書かず、必ずライブラリの列挙型を使用すること。これが、保守性を担保する唯一の道である。

結言

ExcelとProjectの連携は、単なるデータのコピーではない。それは、「静的な計画」を「動的な計算モデル」へと変換するプロセスである。

エンジニアが制御すべきは、単なるコードではなく、アプリケーションの背後で蠢く計算エンジンとメモリの挙動そのものである。この知見を武器に、堅牢で、高速で、そして何より「予測可能な」システムを構築せよ。

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