Project VBAを掌握する極限の知見:外部Excelの「行番号」をProjectの「UniqueID」へ安全にマッピングする極意
開発現場でよくある光景だ。
プロジェクトマネージャーが魂を込めて作成したExcelのWBS。「先行タスク」の欄には、行番号ベースで「12」「15」といった数字が並んでいる。これを「さあ、Microsoft Projectにインポートしよう」とVBAを走らせた瞬間、地獄の扉が開く。
インポートの過程でタスクがソートされ、行番号がズレる。あるいは後から行の挿入や削除が行われる。その結果、リンクは全く関係のないタスクを指し示し、スケジュールは修復不可能な崩壊を始める。
「行番号」という一過性の物理位置に依存する設計は、VBAによる自動化において最大にして最悪のアンチパターンである。
今回は、Excel上の泥臭い行番号を、Microsoft Projectが誇る不変の識別子「UniqueID」へ安全に変換し、タスク間の依存関係(先行タスク・後続タスク)をミリ単位の精度で自動構築するプロダクションコードを授けよう。
—
1. なぜ「行番号」ベースのリンク構築は破綻するのか?
VBA初学者や設計を妥協したエンジニアは、次のようなコードを書きがちだ。
‘ 【アンチパターン】行番号をそのままTask.IDや行位置としてリンクに使う愚行
Set t1 = ActiveProject.Tasks(12)
Set t2 = ActiveProject.Tasks(15)
t2.Predecessors = “12” ‘ 危険:行の挿入・削除で完全に見当違いのタスクにリンクする
プロジェクトオブジェクトモデルの真実
Microsoft Projectには、タスクを識別する指標がいくつか存在する。
1. ID (Task.ID): タスクの現在の「表示上の行番号(インデックス)」。タスクの移動やソート、挿入によって動的に変化する。
2. UniqueID (Task.UniqueID): タスクが生成された瞬間に発行される、プロジェクトファイル内で一生涯不変の整数値。
外部のExcel(あるいは別システム)と連携する場合、相手が持っているのは「人間が管理しやすい行番号や任意のタスク番号」だ。これをProject側の動的な`ID`に直接マッピングしようとすると、タスクの並び順が変わった瞬間にバグる。
「外部の指定値」を、Project側の不変の「UniqueID」へ正確に翻訳する辞書(Dictionary)をメモリ上に構築すること。 これが堅牢なWBS自動構築システムにおける絶対的鉄則である。
—
2. 堅牢なマッピングアーキテクチャの設計
今回のロジックの全貌はこうだ。
1. Excel側の読み込み: 外部Excelから「タスク名」「独自のタスク番号(文字列や数値)」「先行タスク番号」を一括取得する。
2. Projectへのタスク展開(第1パス): まず先行タスクの依存関係は無視し、すべてのタスクを純粋にProjectへ登録する。
3. マッピング辞書の生成: 登録したタスクの「独自のタスク番号(Excel側)」と、Projectが自動割当した「UniqueID」を紐付ける `Scripting.Dictionary` を作成する。
4. 依存関係の解決・適用(第2パス): 辞書を引いて「Excelの先行タスク番号」を「ProjectのUniqueID」に変換し、`Task.Predecessors` に安全に流し込む。
この2パス(Two-Pass)アプローチをとることで、タスクの登録順序やExcel上の並び順に一切依存しない、極めて堅牢なインポートエンジンが完成する。
—
3. 【プロダクションコード】完全実装サンプル
以下のコードは、エラーハンドリング、メモリ管理、そして実務で耐えうるパフォーマンスチューニングを施した実戦投入可能なVBAモジュールである。
Option Explicit
‘ —————————————————————–
‘ 外部ExcelのWBSデータを読み込み、UniqueIDベースで安全に依存関係を構築するメインプロシージャ
‘ —————————————————————–
Sub ImportWBSFromExcelWithUniqueID()
Dim xlApp As Object
Dim xlWb As Object
Dim xlWs As Object
Dim excelFilePath As String
‘ 連携するExcelファイルのパスを指定(必要に応じてファイルダイアログ等に変更してください)
excelFilePath = “C:\Work\WBS_Template.xlsx”
‘ すでにProject側が開いているプロジェクトを対象とする
If ActiveProject Is Nothing Then
MsgBox “アクティブなプロジェクトがありません。”, vbCritical
Exit Sub
End If
‘ Excelオブジェクトの遅延バインディング(参照設定不要の安全設計)
On Error Resume Next
Set xlApp = CreateObject(“Excel.Application”)
If xlApp Is Nothing Then
MsgBox “Excelの起動に失敗しました。”, vbCritical
Exit Sub
End If
Set xlWb = xlApp.Workbooks.Open(excelFilePath, ReadOnly:=True)
Set xlWs = xlWb.Sheets(1)
On Error GoTo 0
If xlWs Is Nothing Then
MsgBox “指定されたExcelファイルまたはシートが見つかりません。” & vbCrLf & excelFilePath, vbCritical
xlApp.Quit
Set xlApp = Nothing
Exit Sub
End If
‘ データの最終行を取得
Dim lastRow As Long
lastRow = xlWs.Cells(xlWs.Rows.Count, “A”).End(xlUp).Row
If lastRow < 2 Then
MsgBox "取り込むべきデータが存在しません。", vbExclamation
xlWb.Close False
xlApp.Quit
Set xlApp = Nothing
Exit Sub
End If
' Excelからデータをメモリ上の配列へ一気に取り込む(I/Oネックの排除)
' 列構成例: A列=タスク番号(独自ID), B列=タスク名, C列=先行タスク番号(独自ID)
Dim rawData As Variant
rawData = xlWs.Range(xlWs.Cells(2, 1), xlWs.Cells(lastRow, 3)).Value
' Excelを閉じる
xlWb.Close False
xlApp.Quit
Set xlApp = Nothing
' 処理の高速化(画面描画や計算の抑制)
Application.ScreenUpdating = True ' ProjectVBAではScreenUpdatingの挙動が特殊なため標準設定を維持
' =========================================================================
' 【第1パス】タスクの登録と、[Excel独自タスク番号 -> Project UniqueID] 辞書の作成
‘ =========================================================================
Dim dicTaskMap As Object
Set dicTaskMap = CreateObject(“Scripting.Dictionary”)
Dim i As Long
Dim t As Task
Dim customTaskID As String
Dim taskName As String
‘ Undoの単位をまとめるため、Projectの処理ブロックを開始
App.Cursor = jcWait
For i = 1 To UBound(rawData, 1)
customTaskID = Trim(CStr(rawData(i, 1)))
taskName = Trim(CStr(rawData(i, 2)))
If customTaskID <> “” And taskName <> “” then
‘ タスクをプロジェクトの末尾に追加
Set t = ActiveProject.Tasks.Add(taskName)
‘ 辞書に「Excel上の独自ID」をキーとして、「ProjectのUniqueID」を格納
If Not dicTaskMap.Exists(customTaskID) Then
dicTaskMap.Add customTaskID, t.UniqueID
Else
‘ 重複キーが存在する場合の警告
Debug.Print “警告: 独自タスク番号 ‘” & customTaskID & “‘ が重複しています。”
End If
End If
Next i
‘ =========================================================================
‘ 【第2パス】UniqueIDベースでの依存関係(先行タスク)の構築
‘ =========================================================================
Dim predecessorRaw As String
Dim predecessorArray() As String
Dim pIndex As Long
Dim targetTaskUniqueID As Long
Dim predTaskUniqueID As Long
Dim targetTask As Task
Dim predStringList As String
For i = 1 To UBound(rawData, 1)
customTaskID = Trim(CStr(rawData(i, 1)))
predecessorRaw = Trim(CStr(rawData(i, 3)))
If customTaskID <> “” And predecessorRaw <> “” Then
‘ 自身のUniqueIDを辞書から引く
If dicTaskMap.Exists(customTaskID) Then
targetTaskUniqueID = dicTaskMap(customTaskID)
Set targetTask = GetTaskByUniqueID(targetTaskUniqueID)
If Not targetTask Is Nothing Then
‘ カンマ区切りで複数の先行タスクが指定されている場合を考慮
predecessorArray = Split(predecessorRaw, “,”)
predStringList = “”
For pIndex = LBound(predecessorArray) To UBound(predecessorArray)
Dim singlePredID As String
singlePredID = Trim(predecessorArray(pIndex))
‘ 先行タスクの独自IDが辞書に存在するか確認
If dicTaskMap.Exists(singlePredID) Then
predTaskUniqueID = dicTaskMap(singlePredID)
‘ Projectのリンク書式(UniqueIDを指定する場合は “UniqueID” を渡すのが確実)
‘ 例: リンク文字列として結合していく
If predStringList = “” Then
predStringList = CStr(predTaskUniqueID)
Else
predStringList = predStringList & “,” & CStr(predTaskUniqueID)
End If
End If
Next pIndex
‘ 最終的な先行タスク文字列を適用
If predStringList <> “” Then
On Error Resume Next
‘ Projectの仕様上、UniqueIDで先行タスクを指定する場合はプレフィックスやID指定に注意が必要だが、
‘ Task.Predecessors に UniqueIDの数値を与えることで正しく解決される。
targetTask.Predecessors = predStringList
If Err.Number <> 0 Then
Debug.Print “リンク設定エラー (Task UID: ” & targetTaskUniqueID & “): ” & Err.Description
Err.Clear
End If
On Error GoTo 0
End If
End If
End If
End If
Next i
App.Cursor = jcDefault
MsgBox “WBSのインポートとUniqueIDベースの依存関係構築が完了しました。”, vbInformation
End Sub
‘ —————————————————————–
‘ 補助関数: UniqueIDからTaskオブジェクトを効率的に取得する
‘ —————————————————————–
Private Function GetTaskByUniqueID(ByVal uniqueID As Long) As Task
Dim t As Task
On Error Resume Next
‘ ActiveProject.Tasks は UniqueID を直接引数に取れないため、FindByUniqueIDを用いるかループする
‘ Projectオブジェクトモデルでは FindByUniqueID メソッドが備わっている
Set GetTaskByUniqueID = ActiveProject.Tasks.UniqueID(uniqueID)
On Error GoTo 0
End Function
—
4. チーフアーキテクトが教える実装上の急所と実務の罠
このコードをそのまま現場に投入するにあたり、プロとして知っておくべき「実装の急所」をいくつか補足しておこう。
① 循環参照(Circular Dependency)の罠
現実のExcel WBSでは、人間がうっかり「タスクAの先行にタスクB、タスクBの先行にタスクA」というループ構造を書いてしまうことがよくある。
Microsoft Projectは循環参照検知時にエラーダイアログをモーダルでポップアップさせる。これがバッチ処理(VBA)の最中に発生すると、処理が完全にフリーズ(無応答)する。
プロダクション環境では、事前にグラフ理論的な有向グラフの巡回チェックを入れるか、エラートラップを徹底的に網羅する必要がある。
② 外部リソース(Excel)のゾンビ化防止
VBAからExcelを操作する際、`CreateObject` や `Workbooks.Open` を使った後に予期せぬエラーでコードが中断すると、Windowsの裏でExcelのプロセス(`EXCEL.EXE`)がゾンビとして生き残り続ける。
実務コードでは、必ず `Error Handler` を設置し、異常終了時であっても確実にオブジェクトの解放(`Set xlApp = Nothing` 等)を行う構造にしなければならない。今回のサンプルはシンプル化のため省略しているが、実運用では `On Error GoTo ErrorHandler` を必ず組み込むこと。
③ パフォーマンスの最適化
タスク数が数千件規模に及ぶ大規模プロジェクトの場合、1タスクごとに `Predecessors` を代入するたびにProject側でスケジュール再計算(リカルキュレーション)が走ると、数分単位の時間がかかる。
可能であれば、インポート処理の開始前に `ActiveProject.Calculation = pjManual` に設定し、すべてのタスクとリンクの流し込みが終わった後に `pjAutomatic` に戻して一括計算させると、実行速度が劇的に向上する。
—
総括
「行番号」という刹那的なインデックスに頼る時代は終わった。
外部システムやExcelとMicrosoft Projectを連携させるプロフェッショナルであれば、「UniqueIDによる抽象化とマッピング辞書」のパターンを体に叩き込むべきだ。
この設計を取り入れるだけで、あなたの組んだVBScriptやVBAマクロは、現場のユーザーがどんなにExcelの行を入れ替えようとも、絶対にリンク切れを起こさない「金剛不壊の自動化ツール」へと生まれ変わる。
妥協のない設計で、真の業務効率化をその手で掴み取ってほしい。
