【極限の設計】MS Projectポートフォリオにおけるカスタムフィールド定義の完全同期エンジン
ポートフォリオ管理の現場において、データ整合性を破壊する最大の要因は「計算式の不一致」である。
複数のプロジェクトファイル(.mpp)が乱立する環境下では、各PMが独自の解釈でカスタムフィールド(Task/Resource Custom Fields)を作成し、計算式(Formula)を書き換える。その結果、集計ツールやBIプラットフォームへデータを流し込んだ際、型不一致や計算ロジックの乖離による「沈黙のデータ汚染」が発生する。
これは単なる運用ミスではない。アーキテクチャの欠如である。
本稿では、個別のプロジェクトファイルに依存せず、マスター定義に基づき全プロジェクトのカスタムフィールドを強制的に同期(Enforce)する、プロフェッショナル向けの「同期エンジン」の設計思想と実装を詳述する。
1. 設計思想:Schema-Driven Enforcement
一般的なVBA開発者は、「ファイルを開いて、フィールド名を書き換える」という手続き型のアプローチを取る。しかし、それでは例外処理、メモリリーク、そしてCOMオブジェクトのライフサイクル管理という、大規模自動化における致命的な壁に直面する。
我々が構築すべきは、Schema-Driven(スキーマ駆動型)のエンジンである。
1. Definition Layer (Schema): カスタムフィールドの名前、型(Text, Number, Date)、および計算式を、プロジェクトファイルから独立した「真実のソース(Single Source of Truth)」として定義する。
2. Engine Layer (Sync Engine): スキーマを読み込み、ターゲットとなる全プロジェクトに対し、宣言的に状態を適用する。
3. Abstraction Layer (API/COM): MS ProjectのCOMインターフェースとWindows APIを組み合わせ、プロセス制御とメモリ管理を極限まで最適化する。
2. 致命的な罠:COMオブジェクトのライフサイクルとメモリ管理
MS Projectの自動化において、最も恐ろしいのは `MSProject.exe` がタスクマネージャーにゾンビプロセスとして残り続けることだ。これは、オブジェクトの参照カウントが適切にゼロにならず、`IUnknown` インターフェースが解放されていないことに起因する。
特に、ループ内で多数のプロジェクトファイルを開閉する場合、以下の3点を徹底しなければならない。
- 明示的な解放: `Set obj = Nothing` を、スコープが終了する直前ではなく、オブジェクトの使用が終わった瞬間に実行する。
- DoEventsの戦略的配置: COMのメッセージキューが詰まるのを防ぐため、ファイル開閉の境界でOSに制御を戻す。
- Error Trappingによる強制解放: 実行時エラーが発生した際、エラーハンドラ内で必ずオブジェクトを破棄するルーチンを通るように設計する。
3. 実装:Custom Field Sync Engine
以下に、スキーマ定義に基づき、複数のプロジェクトへカスタムフィールドを強制適用するコアエンジンの実装例を示す。ここでは、保守性を高めるために、フィールド定義をクラス化する手法を採用する。
3.1. FieldDefinition クラス (Class Module: `clsFieldDef`)
まず、フィールドの「定義」そのものをオブジェクトとして抽象化する。
' clsFieldDef: カスタムフィールドの定義を保持するクラス
Option Explicit
Public FieldName As String
Public FieldType As Long ' msTaskCustomFieldType 等の定数
Public Formula As String
Public IsTaskField As Boolean ' True: Task, False: Resource
' コンストラクタ的な役割
Private Sub Class_Initialize()
End Sub
3.2. SyncEngine モジュール (Standard Module)
ここが心臓部である。Windows APIを用いたプロセス制御と、COMオブジェクトの厳格な管理を行う。
Option Explicit
' Windows APIの宣言: プロセスのウィンドウ状態を制御し、ダイアログによる停止を防ぐ
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
' 定数定義 (MS Projectの列挙型を模倣)
Private Const msTaskCustomFieldType_Text As Long = 1
Private Const msTaskCustomFieldType_Number As Long = 2
Private Const msTaskCustomFieldType_Date As Long = 3
''' <summary>
''' 指定されたプロジェクトファイル群に対して、定義済みのカスタムフィールドを同期する
''' </summary>
''' <param name="targetFiles">同期対象のファイルパス配列</param>
''' <param name="definitions">適用するフィールド定義のコレクション</param>
Public Sub ExecuteSync(ByRef targetFiles() As String, ByRef definitions As Collection)
Dim projApp As MSProject.Application
Dim targetPath As Variant
Dim def As clsFieldDef
Dim i As Integer
On Error GoTo ErrorHandler
' MS Project インスタンスの生成
Set projApp = New MSProject.Application
projApp.Visible = False ' バックグラウンド処理を基本とする
For Each targetPath In targetFiles
Debug.Print "Processing: " & targetPath
' ファイルを開く (ReadOnly推奨)
projApp.FileOpenEx FileName:=CStr(targetPath), ReadOnly:=True
' --- 同期ロジック開始 ---
' プロジェクトの構造をスキャンし、定義に基づき上書き
For Each def In definitions
If def.IsTaskField Then
ApplyTaskField projApp, def
Else
ApplyResourceField projApp, def
End If
Next def
' --- 同期ロジック終了 ---
' 保存と閉鎖
projApp.FileSave
projApp.FileCloseEx SaveChanges:=pjDoNotSave ' 変更は既にFileSaveで完了している
' メモリ解放とOSへの制御返還
DoEvents
Next targetPath
MsgBox "Synchronization Completed Successfully.", vbInformation
CleanUp:
On Error Resume Next
If Not projApp Is Nothing Then
projApp.Quit
Set projApp = Nothing
End If
Exit Sub
ErrorHandler:
MsgMsg = "Critical Error in file: " & targetPath & vbCrLf & Err.Description
MsgBox MsgMsg, vbCritical
Resume CleanUp
End Sub
''' <summary>
''' タスクカスタムフィールドに定義を適用する内部メソッド
''' </summary>
Private Sub ApplyTaskField(ByRef app As MSProject.Application, ByRef def As clsFieldDef)
Dim i As Integer
Dim found As Boolean
found = False
' MS Projectのカスタムフィールドはインデックスで管理されるため、全スキャンが必要
' パフォーマンス最適化のため、既存のフィールド名との照合を行う
For i = 1 To 30 ' Task Custom Fields の最大数
If app.TaskCustomFields(i).Name = def.FieldName Then
' 既存フィールドが存在する場合、定義を上書き
With app.TaskCustomFields(i)
.Formula = def.Formula
' 型の変更はAPI経由でも制限があるため、運用上は名前の固定と式の変更を主とする
End With
found = True
Exit For
End If
Next i
' 存在しない場合は新規作成(インデックスの空きを探すロジックが必要だが、ここでは簡略化)
If Not found Then
' 実際の実装では、空のフィールドを探すロジックを実装する
Debug.Print "Warning: Field " & def.FieldName & " not found. Manual intervention required."
End If
End Sub
' Resourceフィールド用のApplyResourceFieldも同様に実装...
4. 高度な最適化:ダイアログ・ブロッキングの回避
大規模なバッチ処理において、MS Projectが「互換性の警告」や「保存の確認」などのモーダルダイアログを表示した瞬間、自動化エンジンは完全に停止(Hang)する。
これを回避するために、Windows APIを用いて、特定のウィンドウクラスを持つダイアログがポップアップしていないかを監視するか、あるいは `SendKeys` ではなく、`PostMessage` を用いて「Enter」キーを送信する等の低レイヤーな制御を組み込むのが、シニアエンジニアの作法である。
' ダイアログを強制的に閉じるための概念コード
Private Declare PtrSafe Function PostMessage Lib "user32" Alias "PostMessageA" ( _
ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As Long
Private Const WM_KEYDOWN As Long = &H100
Private Const VK_RETURN As Long = &HD
Public Sub ForceCloseDialog(ByVal hwnd As LongPtr)
' ダイアログに対してEnterキーを非同期に送信
PostMessage hwnd, WM_KEYDOWN, VK_RETURN, 0
End Sub
5. 結論:エンジニアリングとしての自動化
カスタムフィールドの同期は、単なる「値のコピー」ではない。それは、組織全体のデータ・ガバナンスをコードによって強制する行為である。
私が提唱するこのアーキテクチャは、以下の価値を提供する。
1. 決定論的なデータ構造: どのプロジェクトを開いても、計算ロジックが同一であることが保証される。
2. スケーラビリティ: 新しいフィールドが必要になれば、マスターのスキーマ(ExcelやJSON)を更新し、エンジンを回すだけで全プロジェクトに展開できる。
3. 堅牢性: COMオブジェクトのライフサイクルを厳格に管理することで、数千ファイル規模の処理でもメモリ枯渇を起こさない。
レガシーな環境であっても、設計思想が正しければ、VBAは最強の武器となる。技術の真髄は、ツールを使うことではなく、ツールが制御する「状態」を支配することにある。
