【テクニカル・上級編】外部Excelの「先行タスク番号」をProjectの「UniqueID」へ動的マッピングする変換ロジック – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見:外部Excelの「行番号」依存をUniqueIDへ昇華する動的マッピングの実装

現場のプロジェクトマネージャーや情シス担当者であれば、一度は絶望したことがあるだろう。Excelで美しく(あるいは泥臭く)作られたWBS。そこには「先行タスク:5行目」といった、儚くも脆い行番号ベースの依存関係が記述されている。

これをMicrosoft Projectへインポートした瞬間、悲劇が起きる。ユーザーが1行挿入しただけで、あるいはソートをかけただけで、すべての先行タスクのリンクが狂い、ガントチャートは崩壊の道をたどる。行番号とは、動的なデータ構造の前では「砂上の楼閣」に過ぎないのだ。

真に堅牢なシステム間連携を実現するためには、Excel上の相対的な「行番号」を、Projectがタスクの誕生から消滅まで一意に追跡し続ける永続的な識別子`UniqueID`へと動的に変換し、リンクを構築しなければならない。

今回は、Project VBAのメモリモデル、オブジェクトのライフサイクル、そしてCOMの暗部を知り尽くしたアーキテクトの視点から、この「行番号 to UniqueID」変換ロジックの極限実装を解説する。

1. アーキテクチャの核心:なぜ行番号ではなくUniqueIDなのか

Microsoft Projectのオブジェクトモデルにおいて、タスクを指し示す方法は主に2つある。
1. `Index`: 現在のビューにおける表示順序。並び替えや行の挿入・削除で容易に変動する。
2. `ID`: アウトライン番号とは異なり、プロジェクト内での通し番号。これも行の挿入・削除で再割り当てされる。
3. `UniqueID`: タスクが作成された瞬間に発行され、タスクが削除されるまで決して変わらない不変の整数値

先行タスク(Predecessors)のリンクをプログラムから張る場合、`Task.TaskDependencies.Add` メソッドを使用するが、ここで指定する先行タスク側を指し示すには `UniqueID` を使うのが唯一の安全策である。

外部Excel(行番号ベース)から読み込んだデータを正確にProjectへ流し込むためには、以下の2ステップを踏む必要がある。
1. Excelの行番号と、Project上で新規作成されたタスクの `UniqueID` を紐付けるインメモリの高速ルックアップテーブル(Dictionary)の構築。
2. そのテーブルを元にした、依存関係(TaskDependencies)の安全な解決。

2. 実装:堅牢性とパフォーマンスを極めたVBAコード

以下のコードは、実務の巨大プロジェクト(数千行規模のWBS)を想定し、エラーハンドリング、オブジェクトの明示的解放、そしてDictionaryによるO(1)の高速ルックアップを実装したプロダクション・グレードのモジュールである。

Option Explicit

‘ ——————————————————————————–
‘ 外部Excelの行番号ベース依存関係を、MS ProjectのUniqueIDリンクへ動的変換するメインプロシージャ
‘ ——————————————————————————–
Sub ImportWBSFromExcelWithDependencies()
Dim xlApp As Object
Dim xlWb As Object
Dim xlWs As Object
Dim lastRow As Long
Dim i As Long

‘ パスは環境に合わせて変更すること
Const EXCEL_PATH As String = “C:\WBS_Data\ProjectWBS.xlsx”

‘ 画面描画と自動計算を停止し、COMパフォーマンスを極限まで引き上げる
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual

On Error GoTo ErrorHandler

‘ — 1. 外部Excelの遅延バインディング接続 —
Set xlApp = CreateObject(“Excel.Application”)
xlApp.Visible = False
Set xlWb = xlApp.Workbooks.Open(EXCEL_PATH, ReadOnly:=True)
Set xlWs = xlWb.Sheets(1)

‘ データ最終行の取得(A列:タスク名基準)
lastRow = xlWs.Cells(xlWs.Rows.Count, “A”).End(1).Row
If lastRow < 2 Then Err.Raise 9999, , "有効なWBSデータが存在しません。" ' --- 2. マッピング用Dictionaryの生成(行番号 -> Project UniqueID) —
‘ VBAのEarly Bindingを利用するため、参照設定に “Microsoft Scripting Runtime” が必要です
Dim rowToUidMap As Object
Set rowToUidMap = CreateObject(“Scripting.Dictionary”)

Dim tsk As Task
Dim excelRow As Long
Dim taskName As String
Dim taskDuration As String

‘ — 3. 第一次パス:タスクの作成とUniqueIDのキャプチャ —
For i = 2 To lastRow
excelRow = i
taskName = xlWs.Cells(i, 1).Value .Value ‘ A列: タスク名
taskDuration = xlWs.Cells(i, 2).Value .Value ‘ B列: 期間

If Trim(taskName) <> “” Then
‘ Projectにタスクを追加
Set tsk = ActiveProject.Tasks.Add(Name:=taskName)

‘ 期間の設定(例:日単位)
If IsNumeric(taskDuration) Then
tsk.Duration = taskDuration 480 ‘ 1日 = 8時間 = 480分
End If

‘ 【核心】Excelの「行番号」と、Projectが払い出した「UniqueID」を紐付ける
rowToUidMap.Add excelRow, tsk.UniqueID

‘ オブジェクト変数の解放(ループ内でのメモリリーク防止)
Set tsk = Nothing
End If
Next i

‘ — 4. 第二次パス:依存関係(先行タスク)の解決とリンク構築 —
Dim predecessorExcelRow As Variant
Dim currentUid As Long
Dim targetTask As Task
Dim predTaskUid As Long

For i = 2 To lastRow
excelRow = i
predecessorExcelRow = xlWs.Cells(i, 3).Value ‘ C列: 先行タスクの「Excel行番号」

‘ 該当行に先行タスクが指定されている場合のみ処理
If Not IsEmpty(predecessorExcelRow) And IsNumeric(predecessorExcelRow) Then
If rowToUidMap.Exists(excelRow) And rowToUidMap.Exists(CLng(predecessorExcelRow)) Then

currentUid = rowToUidMap(excelRow)
predTaskUid = rowToUidMap(CLng(predecessorExcelRow))

‘ UniqueIDからタスクオブジェクトを逆引きして取得
Set targetTask = GetTaskByUniqueID(currentUid)

If Not targetTask Is Nothing Then
‘ 先行タスクの追加 (Finish-to-Startがデフォルト)
On Error Resume Next ‘ 既にリンクが存在する場合のエラー回避
targetTask.TaskDependencies.Add From:=GetTaskByUniqueID(predTaskUid), Type:=pjFinishToStart
On Error GoTo ErrorHandler
End If

Set targetTask = Nothing
End If
End If
Next i

MsgBox “WBSのインポートおよび依存関係の動的マッピングが正常に完了しました。”, vbInformation, “完了”

CleanUp:
‘ — 5. 厳格なリソース解放とクリーンアップ —
On Error Resume Next
If Not xlWb Is Nothing Then xlWb.Close False
If Not xlApp Is Nothing Then xlApp.Quit
Set xlWs = Nothing
Set xlWb = Nothing
Set xlApp = Nothing
Set rowToUidMap = Nothing
Set targetTask = Nothing
Set tsk = Nothing

‘ アプリケーション設定の復元
Application.ScreenUpdating = True
Application.Calculation = pjCalculationAutomatic
Exit Sub

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

‘ ——————————————————————————–
‘ UniqueIDからタスクオブジェクトを高速取得するヘルパー関数
‘ ——————————————————————————–
Private Function GetTaskByUniqueID(ByVal uniqueID As Long) As Task
Dim t As Task
On Error Resume Next
Set GetTaskByUniqueID = ActiveProject.Tasks.UniqueID(uniqueID)
On Error GoTo 0
End Function

3. シニアエンジニアが注目すべきアーキテクチャのポイント

1. 二段階パス(Two-Pass Algorithm)の採用

データベースやコンパイラ設計の基本原則であるが、依存関係を持つツリー構造をインポートする際、すべてのノード(タスク)が実体としてメモリ上に存在していなければ、後方のノードから前方のノードへリンクを張ることはできない。
このコードでは、第1パスで全タスクを生成してUniqueIDを確定させ、第2パスでDictionaryをルックアップしながらリンクを張るという堅牢な構造を採用している。

2. ループ内におけるメモリリークの徹底排除

VBAにおいて、`For` ループ内でCOMオブジェクト(`Task` など)を繰り返し生成すると、背後にあるCOMラッパーの参照カウンタが肥大化し、最悪の場合ExcelやProjectのプロセスがクラッシュする。
ループの末尾で必ず `Set tsk = Nothing` を実行し、ガベージコレクションの負担を軽減させる配慮が、数千行規模の処理を安定させる。

3. トランザクショナルな環境設定の制御

`Application.ScreenUpdating = False` と `Application.Calculation = pjCalculationManual`。
これを怠ると、タスクが1行追加されるたびにProjectは全体のスケジュール再計算とガントチャートの再描画を走らせ、処理時間が線形から指数関数的に悪化する。この制御により、処理速度を数十倍に跳ね上げることが可能だ。

終わりに:レガシーの呪縛を断ち切れ

「行番号」という人間都合の曖昧なインデックスに依存したシステム連携は、いつの時代も現場の疲弊を生む温床となる。
Projectが持つ本来のデータ構造(`UniqueID`)の本質を理解し、VBAを単なる「マクロ記録の延長」ではなく「厳格なシステム統合レイヤー」として扱うこと。それこそが、真に自動化を極めたエンジニアの仕事である。

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