プロジェクト横断の要塞:別ファイル間タスク依存関係をVBAで完全自動制御する極意
エンタープライズ環境のPMOや大規模システム管理の現場において、単一のMicrosoft Projectファイルで全社規模のWBSを管理するなどという幻想は、ととうに破綻している。リソースプールの枯渇、メモリ溢れ、そして何より権限管理の観点から、プロジェクトは細分化され、それぞれが独立した`.mpp`ファイルとして分散配置されるのが定石だ。
しかし、ここに悪名高き「プロジェクト横断の依存関係(Cross-Project Links)」の壁が立ちはだかる。
別ファイルのタスクを先行タスク(Predecessor)として設定する場合、MS ProjectのGUIではファイル間参照パスが不安定になりやすく、パスが変わった瞬間にリンクが切断される。さらに、数千行規模のWBSにおいて、これらをマニュアルで結びつける作業は人災の温床となる。
今回は、Project VBAのオブジェクトモデルの深層と、COMのライフサイクル管理の鉄則を熟知した者だけが扱える、「外部プロジェクトのタスクUIDを動的に解決し、堅牢なクロスプロジェクトリンクを全自動で生成するアーキテクチャ」をここに解き明かす。
—
1. 外部タスクリンクのメカニズムとVBAの限界
MS Projectにおける外部依存関係は、内部的には `Task.Predecessors.Add` メソッドを用いるが、先行タskが別ファイルにある場合、その指定方法は通常の `[TaskID]` ではなく、ファイルパスと外部タスクのUniqueIDの組み合わせ、または外部プロジェクトがリンクされた状態でのユニークな識別子に依存する。
ここでVBAエンジニアが陥る最大の罠が、COMオブジェクトの参照リークと遅延バインディングの不確実性だ。
複数ファイルを同時に開閉する処理において、`ActiveProject` や暗黙的なインスタンス参照を行えば、裏で`msp.exe`のプロセスがゾンビ化し、メモリを食潰すか、最悪の場合ファイルがロックされて修復不可能になる。
厳格なチーフアーキテクトとして、我々は以下の鉄則を遵守しなければならない。
1. すべてのApplicationおよびProjectオブジェクトは明示的な変数で捕捉し、最後に `Nothing` を代入して解放する。
2. 外部プロジェクトは「読み取り専用(ReadOnly)」かつ「バックグラウンド(Visible = False)」で安全にロードする。
3. タスクID(ID)ではなく、変更されない一意の識別子(UniqueID)をベースにリンクを構築する。
—
2. 実装アーキテクチャ:クロスプロジェクト・リンク自動生成エンジン
以下のコードは、親プロジェクト(自ファイル)の特定タスクに対し、別ファイル(先行プロジェクト)の指定タスクを外部先行タスクとしてプログラムから動的にリンクさせる実用プロシージャである。
Option Explicit
‘ ==============================================================================
‘ 外部プロジェクトのタスクを自プロジェクトのタスクに依存関係として自動設定する
‘ @param targetTaskName : 自プロジェクト側の後続タスク名
‘ @param externalFilePath: 先行タスクが存在する外部Projectファイルの絶対パス
‘ @param externalTaskUID : 先行タスクのUniqueID(行番号IDではないことに注意)
‘ ==============================================================================
Public Sub LinkToExternalTask(ByVal targetTaskName As String, ByVal externalFilePath As String, ByVal externalTaskUID As Long)
Dim appPrj As MSProject.Application
Dim prjCurrent As MSProject.Project
Dim prjExternal As MSProject.Project
Dim tskTarget As MSProject.Task
Dim tskExternal As MSProject.Task
Dim isExternalOpen As Boolean
Dim externalProjectName As String
Dim predecessorString As String
‘ エラーハンドリングによるメモリリーク防止の要塞化
On Error GoTo ErrorHandler
‘ 実行中のProjectアプリケーションインスタンスを取得
Set appPrj = ActiveProject.Application
Set prjCurrent = appPrj.ActiveProject
isExternalOpen = False
externalProjectName = GetFileNameFromPath(externalFilePath)
‘ 1. 外部プロジェクトがすでに開かれているか走査する
Dim p As MSProject.Project
For Each p In appPrj.Projects
If StrComp(p.FullName, externalFilePath, vbTextCompare) = 0 Then
Set prjExternal = p
isExternalOpen = True
Exit For
End If
Next p
‘ 2. 開かれていない場合は、バックグラウンドで安全にオープンする
If Not isExternalOpen Then
‘ ファイル存在確認
If Dir(externalFilePath) = “” Then
Err.Raise 53, “LinkToExternalTask”, “外部プロジェクトファイルが見つかりません: ” & externalFilePath
End If
‘ バックグラウンドかつ読み取り専用で開く(UIの描画負荷と競合を防ぐ)
appPrj.FileOpenEx Name:=externalFilePath, ReadOnly:=True, Notify:=False
Set prjExternal = appPrj.ActiveProject
‘ ウィンドウを非表示状態に保つ(パフォーマンスの極限最適化)
appPrj.WindowHide
‘ 元のプロジェクトにフォーカスを戻す
prjCurrent.Activate
End If
‘ 3. 外部プロジェクトから対象タスクをUniqueIDで特定
On Error Resume Next
Set tskExternal = prjExternal.Tasks.UniqueID(externalTaskUID)
On Error GoTo ErrorHandler
If tskExternal Is Nothing Then
Err.Raise 91, “LinkToExternalTask”, “指定された外部タスクUID (” & externalTaskUID & “) が見つかりません。”
End If
‘ 4. 自プロジェクトから後続タスクを名前で検索
Set tskTarget = Nothing
Dim tsk As MSProject.Task
For Each tsk In prjCurrent.Tasks
If Not tsk Is Nothing Then
If tsk.Name = targetTaskName Then
Set tskTarget = tsk
Exit For
End If
End If
Next tsk
If tskTarget Is Nothing Then
Err.Raise 91, “LinkToExternalTask”, “自プロジェクト内に対象タスクが見つかりません: ” & targetTaskName
End If
‘ 5. クロスプロジェクト・リンクの構築
‘ MS Projectにおける外部先行タスクの構文: ‘[ファイルパス]\UniqueID’
‘ 例: ‘C:\Projects\PredecessorPrj.mpp\15’
predecessorString = “‘” & externalFilePath & “\” & tskExternal.UniqueID & “‘”
‘ 既存の先行タスク文字列に影響を与えず追加、あるいは直接Predecessors.Addを使用
tskTarget.Predecessors.Add predecessorString
MsgBox “外部依存関係の構築に成功しました。” & vbCrLf & _
“後続: ” & tskTarget.Name & vbCrLf & _
“先行: ” & prjExternal.Name & ” (ID: ” & tskExternal.ID & “)”, vbInformation, “architectural success”
CleanUp:
‘ — オブジェクトの明示的解放(メモリリークの完全排除) —
Set tskTarget = Nothing
Set tskExternal = Nothing
Set prjExternal = Nothing
Set prjCurrent = Nothing
Set appPrj = Nothing
Exit Sub
ErrorHandler:
MsgBox “致命的エラーが発生しました [” & Err.Number & “]: ” & Err.Description, vbCritical, “VBA Architectural Error”
Resume CleanUp
End Sub
‘ 補助関数: フルパスからファイル名を取得
Private Function GetFileNameFromPath(ByVal fullPath As String) As String
Dim pos As Long
pos = InStrRev(fullPath, “\”)
If pos > 0 Then
GetFileNameFromPath = Mid(fullPath, pos + 1)
Else
GetFileNameFromPath = fullPath
End If
End Function
—
3. コードの深層解説:なぜこの実装が必要なのか
外部タスク指定の構文的罠
MS ProjectのVBAエンジンは、`Predecessors.Add` メソッドに渡す文字列のフォーマットに対して非常に厳格だ。同一ファイル内であればタスクID(例: `”12″`)だけで動作するが、外部ファイルの場合は 絶対パスとUniqueIDをシングルクォーテーションで囲む 必要がある。
さらに厄介なことに、環境によっては相対パスが解決できずにリンク切れを起こすため、本コードでは一貫して `externalFilePath`(絶対パス)を強制している。
ゾンビプロセスの防止と `WindowHide`
外部ファイルをバックグラウンドで開く際、デフォルトの `FileOpenEx` はGUI上にプロジェクトウィンドウを描画してしまう。これが数千行規模のファイル群で発生すると、画面のちらつきだけでなく、COMの描画コンテキストがクラッシュする原因になる。
`appPrj.WindowHide` を即座に挟むことで、ユーザーに意識させず、メモリとCPUの消費を最小限に抑えたサイレント・リンク解決を実現している。
ID と UniqueID の厳密な使い分け
シニアエンジニアであれば常識だが、MS Projectのタスクには「ID(行の移動や挿入で変動する)」と「UniqueID(作成時に発行され、一生変わらない)」が存在する。
外部プロジェクトのタスクを参照する場合、行挿入によってIDがズレるため、絶対に `UniqueID` を使用しなければならない。上記のコードでは `prjExternal.Tasks.UniqueID(externalTaskUID)` を用いて、このリスクを完全にハイドレートしている。
—
4. チーフアーキテクトからの提言:運用の自動化へ向けて
このVBAモジュールを単体のマクロとして実行するだけでは、真のエンタープライズ自動化とは言えない。
これをベースにして、例えばSharePointやGitで管理されたWBSのJSON定義ファイル、あるいはDB上のマッピングテーブルを読み込み、夜間バッチやキックオフ時に一括で全プロジェクトの依存関係を再構築するパイプラインを構築すべきだ。
レガシーなVBAであっても、オブジェクトライフサイクルの管理とCOMの挙動を完全に掌握していれば、モダンなCI/CDパイプラインに匹敵する堅牢なインフラ自動化の歯車として機能させることができる。
妥協のないコードだけが、巨大プロジェクトの複雑性を制圧する。
