Project VBAの罠を撃ち抜く:タスク挿入・削除時の「インデックスズレ」完全回避術
Microsoft Project(以下、MS Project)を用いた業務自動化、特にWBS(Work Breakdown Structure)の自動生成や外部データ(Excel/基幹システム)との連携ツールをVBAで構築する際、全開発者が必ず直面する「巨大な壁」があります。
それが、タスクの挿入・削除時に発生する「インデックス(ID)の動的シフト(ズレ)」です。
Excelの行削除と同じ感覚で安易に前方ループ(`For i = 1 To Tasks.Count`)を回した瞬間、あなたのコードは無限ループに陥るか、あるいはプロジェクトデータを破壊する凶器へと変貌します。
本記事では、MS Projectの内部オブジェクトモデルの挙動を解き明かし、このインデックスズレを「完全かつエレガントに回避」するための設計思想と、そのままプロダクション環境に投入できる堅牢なVBAコードを提示します。
—
1. なぜExcel感覚のコードはプロジェクトを崩壊させるのか?
MS Projectにおける「タスクの識別子」には、極めて重要な2つのプロパティが存在します。この違いを理解していないことが、すべてのバグの元凶です。
| プロパティ名 | 特徴 | 挙動 |
| :— | :— | :— |
| `Task.ID` | 動的・相対的 | 表示上の行番号(1から始まる連番)。タスクの挿入・削除・並び替えによってリアルタイムに変動する。 |
| `Task.UniqueID` | 静的・絶対的 | タスク生成時に割り当てられる一意のシステムキー。タスクの順序が変わっても絶対に変動しない。 |
前方ループが引き起こす「悪夢のメカニズム」
例えば、以下のような処理を前方ループで行うとします。
「マイルストーン(特定条件)タスクの『直前』に、新しい準備タスクを挿入する」
‘ 【アンチパターン】絶対にやってはいけない前方ループ
Dim i As Long
For i = 1 To ActiveProject.Tasks.Count
If ActiveProject.Tasks(i).PercentComplete > 0 Then
‘ 条件に合致したタスクの前に新規タスクを挿入
ActiveProject.Tasks.Add Name:=”準備タスク”, Before:=i
End If
Next i
このコードを実行すると、何が起きるでしょうか?
1. `i = 5` のタスクが条件に合致し、その「前(`Before:=5`)」に新しいタスクを挿入します。
2. 挿入された新タスクが新たな `ID = 5` となり、元の `ID = 5` のタスクは `ID = 6` にシフト(押し下げ) されます。
3. 次のループ `i = 6` では、シフトされた「元のタスク」を再度スキャンすることになり、再びその前に新タスクを挿入します。
4. これが繰り返され、無限ループ(タスクの無限増殖)が発生します。
削除処理(`Task.Delete`)の場合も同様です。前方ループで削除を行うと、削除した瞬間に以降のタスクIDが繰り上がるため、「処理されないタスク(スキップ)」と「存在しないインデックスへのアクセス(実行時エラー)」が同時に発生します。
—
2. 解決策1:逆順ループ(Backward Looping)による直接制御
最もシンプルかつ確実なアプローチは、コレクションの末尾から先頭に向かって処理を行う「逆順ループ(`Step -1`)」です。
末尾から処理を行う場合、タスクを挿入・削除して影響を受けるのは「現在のインデックス(`i`)よりも後ろ(数値が大きい方)」のタスクのみです。これから処理する「前方(数値が小さい方)」のタスクのIDは一切変動しないため、インデックスの整合性が完全に保たれます。
‘ 逆順ループによる安全なタスク削除の基本構造
Dim i As Long
For i = ActiveProject.Tasks.Count To 1 Step -1
‘ 空タスク(Nothing)のハンドリングを怠らないこと
If Not (ActiveProject.Tasks(i) Is Nothing) Then
If ActiveProject.Tasks(i).PercentComplete = 100 Then
ActiveProject.Tasks(i).Delete
End If
End If
Next i
—
3. 解決策2:プロフェッショナルの選択「コレクション・キャッシュ戦略」
逆順ループは単一プロジェクト内の単純な操作には有効ですが、「外部システム(Excelやデータベース)からインポートしたデータに基づいて、複雑な親子関係(WBS)を構築・更新する」といった高度なユースケースでは限界があります。
そこで採用すべきなのが、「コレクション・キャッシュ戦略」です。
アーキテクチャの概要
1. 更新・挿入・削除の対象となるタスクを、まずは静的なコレクション(`Scripting.Dictionary` や専用配列)に `UniqueID` をキーとしてキャッシュ(一時保存) する。
2. キャッシュ構築フェーズでは、プロジェクトのタスク構成を一切変更しない。
3. 実行フェーズにおいて、キャッシュした `UniqueID` を元に、`ActiveProject.Tasks.UniqueID(Key)` メソッドを用いてピンポイントでタスクを操作する。
これにより、動的なIDシフトの影響を100%排除し、さらに処理速度を劇的に向上させることが可能です。
—
4. 堅牢なプロダクションコード(MS Project VBA)
以下に、実務でそのまま使用できる、エラーハンドリングとパフォーマンスチューニングを極限まで施したプロダクションコードを提示します。
このコードは、「特定の条件(例:リソースが未割り当てのタスク)を満たすタスクの前に、警告タスクを挿入する」 という処理を、キャッシュ戦略を用いて安全に実行します。
Option Explicit
”’
”’
Public Sub SafeInsertTasksProc()
‘ パフォーマンス向上のため画面更新と自動計算を抑止
On Error GoTo ErrorHandler
ToggleEngineState Enable:=False
Dim targetProj As Project
Set targetProj = ActiveProject
‘ 1. 処理対象のUniqueIDを格納するキャッシュ(コレクション)を生成
‘ ※ Microsoft Scripting Runtime への参照設定が必要(非事前バインディングでも動作するようObjectで定義)
Dim taskCache As Object
Set taskCache = CreateObject(“Scripting.Dictionary”)
Dim t As Task
For Each t In targetProj.Tasks
‘ MS Project特有の「空白行(Nothing)」をスキップ
If Not (t Is Nothing) Then
‘ 条件:リソースが未割り当て、かつサマリータスクではない
If t.ResourceNames = “” And t.Summary = False Then
‘ UniqueIDをキーとしてキャッシュに登録
taskCache.Add Key:=t.UniqueID, Item:=t.Name
End If
End If
Next t
‘ 2. キャッシュに基づき、安全にタスクを挿入
Dim uID As Variant
Dim currentTask As Task
Dim newTask As Task
For Each uID In taskCache.Keys
‘ UniqueIDから直接タスクオブジェクトを参照(IDズレの影響を受けない)
Set currentTask = targetProj.Tasks.UniqueID(uID)
If Not (currentTask Is Nothing) Then
‘ 対象タスクの直前に新規タスクを挿入
‘ AddメソッドのBefore引数には、常に最新の「Taskオブジェクト」または「現時点のID」を渡す
Set newTask = targetProj.Tasks.Add( _
Name:=”【要確認】リソース未割当: ” & currentTask.Name, _
Before:=currentTask.ID _
)
‘ 新規挿入したタスクの属性をセット
With newTask
.Duration = “0d” ‘ マイルストーンとして設定
.OutlineLevel = currentTask.OutlineLevel ‘ 元のタスクと同じインデント階層にする
‘ 必要に応じて追加のメタデータを設定
End With
End If
Next uID
‘ 正常終了
MsgBox “タスクの安全な挿入処理が完了しました。処理件数: ” & taskCache.Count & ” 件”, vbInformation, “処理完了”
ExitPoint:
‘ 抑止していたエンジンを必ず復旧させる(トランザクションの保証)
ToggleEngineState Enable:=True
Set taskCache = Nothing
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました。” & vbNewLine & _
“エラー番号: ” & Err.Number & vbNewLine & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
Resume ExitPoint
End Sub
”’
”’
Private Sub ToggleEngineState(ByVal Enable As Boolean)
With Application
.ScreenUpdating = Enable
If Enable Then
.Calculation = pjAutomatic
Else
.Calculation = pjManual
End If
End With
End Sub
—
5. 設計者が語る、現場でのデータベース・外部ファイル連携の注意点
基幹システムやExcelの工程表からMS Projectへデータをインポートし、WBSを自動構築するツールを開発する場合、さらに以下の設計配慮が必要です。
① WBS階層構造(OutlineLevel)の再構築順序
タスクを挿入・削除すると、インデントレベル(`OutlineLevel`)の整合性が崩れるリスクがあります。
タスクを挿入する際は、「親タスク(Summary Task)を先に作成し、その直後に子タスクを挿入していく」 という「トップダウン順」で処理を組み立てるのがセオリーです。この際も、作成した親タスクの `UniqueID` を保持し続け、子タスクの追加先を絶対参照できるように設計します。
② 外部キー(External Key)の永続化
Excel等の外部データベースとMS Projectを双方向連携させる場合、MS Project側の `UniqueID` を外部データベース側に保存(あるいはその逆)してください。
次回更新(同期)時は、`ID`(行番号)ではなく、この外部キーとUniqueIDのマッピングテーブルをメモリ上に展開し、差分比較(Upsert処理:存在すれば更新、なければ挿入、リストになければ削除)を実行します。これを行うだけで、同期バグは根絶されます。
—
6. まとめ:堅牢なコードが、プロジェクトを成功に導く
MS Project VBAにおける開発は、一般的なExcel VBAに比べてオブジェクトモデルがセンシティブであり、スケジュール計算エンジンがリアルタイムに動作しているため、一瞬の設計ミスがデータの全損失やアプリの強制終了を招きます。
- `ID` は使い捨てるもの。信頼すべきは `UniqueID`。
- 直接操作なら「逆順ループ(`Step -1`)」。
- 複雑な処理なら「キャッシュ(`Dictionary`)戦略」。
この3つの原則を脳裏に刻み、美しく堅牢な自動化ツールを構築してください。あなたの書いたコードが、プロジェクトマネジメントの現場を劇的に変える強力な武器になることを確信しています。
