こんにちは!Project VBAの世界へようこそ。
Microsoft Project(以下、MS Project)のマクロを自分で書いて、業務を自動化しようと一歩を踏み出したのですね。素晴らしい挑戦です!
MS Projectは、Excelと似ているようで実は全く異なる「独自の思想」を持ったアプリケーションです。そのため、Excel VBAの感覚でコードを書くと、高確率で「タスクの順序やIDがめちゃくちゃにズレてしまう」という不思議な現象に直面します。
特に、ループ処理の中でタスクを新しく「挿入(追加)」したり「削除」したりする時に、この問題は牙を剥きます。
「あれ? 3行目にタスクを入れたはずなのに、なぜか別のタスクが書き換わってしまった…」
「ループが途中でスキップされて、処理されないタスクがある…」
安心してください。これはあなただけの失敗ではなく、Project VBAに挑戦するすべての人が一度は通る「登竜門」なのです。
この記事では、この「インデックスズレ問題」がなぜ発生するのかという仕組みから、プロのエンジニアが実務で必ず使っている「逆順ループ」や「コレクションキャッシュ戦略」という完全回避テクニックまで、優しく、かつ深く解説します。
ここをクリアすれば、Project VBAの基本はバッチリですよ。さあ、一緒にマスターしていきましょう!
—
1. なぜズレる?「ID」と「UniqueID」の決定的な違い
まずは、MS Projectにおけるタスクの「背番号」の仕組みを理解しましょう。ここが最大のポイントです。
MS Projectのタスクには、大きく分けて2つの識別子(キー)が存在します。
| 識別子 | 特徴 | 例え話 |
| :— | :— | :— |
| ID | 画面に表示されている「行番号」。タスクの挿入・削除、並べ替えによって動的に変動する。 | 学校のクラスでの「出席番号」(転校生が来ると後ろの人の番号がズレる) |
| UniqueID | タスクが作成された瞬間に割り振られる「不変の識別子」。絶対に変動しない。 | 国が発行する「マイナンバー」や学校の「学籍番号」(一生変わらない) |
ズレが発生するメカニズム
例えば、以下のように5つのタスクが並んでいるとします。
[ID: 1] タスクA
[ID: 2] タスクB
[ID: 3] タスクC
[ID: 4] タスクD
[ID: 5] タスクE
ここで、上から順番にループ(`For i = 1 To 5`)を回して、「IDが3(タスクC)の直前に、新しいタスクを挿入する」という処理を行うとどうなるでしょうか?
1周目(i = 1)と 2周目(i = 2)
問題なく「タスクA」「タスクB」が処理されます。
3周目(i = 3)
「タスクC」の手前に、新しい「タスクX」を挿入します。
すると、MS Projectは自動的にID(行番号)を再計算します。
[ID: 1] タスクA
[ID: 2] タスクB
[ID: 3] タスクX <-- 【新しく挿入された!】
[ID: 4] タスクC <-- 【元々のID:3が、4にズレた!】
[ID: 5] タスクD <-- 【元々のID:4が、5にズレた!】
[ID: 6] タスクE <-- 【元々のID:5が、6にズレた!】
4周目(i = 4)
プログラムは次に「ID: 4」のタスクを処理しようとします。
しかし、IDが再計算された結果、ID: 4は「タスクC」になっています。
「あれ? タスクCはさっき3周目で処理するはずだったのに、また処理することになってしまった!」
逆に、元々ID: 4だった「タスクD」は、ID: 5に押し出されたため、このままだと処理がスキップされたり、想定外の挙動を引き起こしたりします。
これが、タスク挿入時に発生する「インデックスズレ(IDズレ)の罠」です。
—
2. 回避術その1:王道の「逆順(Step -1)ループ」
この問題を解決する最もシンプルでエレガントな方法が、「下から上に向かってループを回す(逆順ループ)」ことです。
なぜ逆順だとズレないのか?
先ほどの例を、今度は下(お尻)から順番に処理してみましょう(`For i = 5 To 1 Step -1`)。
【処理前】
[ID: 1] タスクA
[ID: 2] タスクB
[ID: 3] タスクC
[ID: 4] タスクD
[ID: 5] タスクE
1周目(i = 5)〜 2周目(i = 4)
下から順に「タスクE」「タスクD」を安全に処理します。
3周目(i = 3)
ここで「タスクC(ID: 3)」の直前に「タスクX」を挿入します。
【挿入後】
[ID: 1] タスクA
[ID: 2] タスクB
[ID: 3] タスクX <-- 【新しく挿入された!】
[ID: 4] タスクC <-- 【IDがズレた】
[ID: 5] タスクD <-- 【IDがズレた】
[ID: 6] タスクE <-- 【IDがズレた】
4周目(i = 2)
次に処理するのは「ID: 2(タスクB)」です。
タスクを挿入したことによってIDがズレたのは、「挿入した位置(ID: 3)よりも後ろ(下)のタスクだけ」です。
これから処理する予定の「ID: 2」より上のタスク(タスクA、B)は、一切影響を受けていません!
そのため、何の問題もなく次の処理へと進むことができます。
【実践】逆順ループのVBAコード
それでは、実際のコードを見てみましょう。
このマクロは、「マイルストーン(期間が0日)のタスクを見つけたら、その直前に『前準備』というタスクを自動挿入する」という実用的な例です。
Sub InsertTasksSafety_ReverseLoop()
Dim pj As Project
Set pj = ActiveProject
Dim i As Long
Dim currentTask As Task
Dim newTask As Task
‘ 画面更新を一時停止して処理を高速化します
ScreenUpdating = False
On Error GoTo ErrorHandler
‘ 【ポイント1】後ろ(最大値)から前(1)に向かって Step -1 でループします
For i = pj.Tasks.Count To 1 Step -1
Set currentTask = pj.Tasks(i)
‘ 【ポイント2】MS Projectの空白行(Nothing)をスキップする安全設計
If Not (currentTask Is Nothing) Then
‘ 条件:もしタスクがマイルストーン(期間が0日)だったら
If currentTask.Milestone = True Then
‘ 【ポイント3】Before引数を使って、現在のタスクの直前に挿入
‘ これにより、新タスクが現在のIDを乗っ取り、既存タスクは下に押し出されます
Set newTask = pj.Tasks.Add(Name:=”【自動挿入】前準備タスク”, Before:=i)
‘ 挿入したタスクの初期設定(例として期間を1日に設定)
newTask.Duration = “1d”
‘ 分かりやすいように色を変えておきましょう(任意)
SelectRow Row:=i, RowRelative:=False
Font32Ex Color:=RGB(0, 128, 0) ‘ 緑色に設定
End If
End If
Next i
‘ 画面更新を再開します
ScreenUpdating = True
MsgBox “タスクの挿入が安全に完了しました!”, vbInformation, “完了”
Exit Sub
ErrorHandler:
ScreenUpdating = True
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End Sub
—
3. 回避術その2:プロ仕様の「UniqueIDキャッシュ戦略」
「逆順ループは便利だけど、WBS(階層構造)のレベルを判定しながら複雑にタスクを追加したり、別のリストと突き合わせたりする場合は、頭が混乱してしまう…」
そんな時のために、より堅牢でプロ仕様のアプローチをご紹介します。それが「UniqueID(ユニークID)キャッシュ戦略」です。
これは、変更されやすい「行番号(ID)」を直接ループで回すのではなく、「絶対に変わらないUniqueID」を一度コレクション(一時的なリスト)に退避(キャッシュ)させ、それを元に処理を行う手法です。
イメージ図
1. 最初にプロジェクト全体のタスクをスキャンし、処理対象の「UniqueID」だけをノート(Collection)に書き留める。
2. ノートに書かれた「UniqueID」を上から順に読み出す。
3. UniqueIDを頼りにタスクを特定し、挿入や削除を実行する(行番号がどれだけズレても、UniqueIDでピンポイントに狙い撃ちできるため、絶対に迷子になりません)。
【実践】UniqueIDキャッシュのVBAコード
Sub InsertTasksSafety_CacheStrategy()
Dim pj As Project
Set pj = ActiveProject
‘ 処理対象のUniqueIDを格納するコレクション
Dim targetUniqueIDs As New Collection
Dim t As Task
‘ 画面更新を停止
ScreenUpdating = False
‘ — ステップ1: 処理対象の「UniqueID」を安全にキャッシュする —
For Each t In pj.Tasks
If Not (t Is Nothing) Then
‘ 例:タスク名に「【重要】」と入っているタスクを対象にする
If InStr(t.Name, “【重要】”) > 0 Then
‘ UniqueIDをコレクションに追加
targetUniqueIDs.Add t.UniqueID
End If
End If
Next t
‘ — ステップ2: キャッシュしたUniqueIDを元に処理を実行する —
Dim uID As Variant
Dim targetTask As Task
Dim newTask As Task
For Each uID In targetUniqueIDs
‘ UniqueIDから現在のタスクオブジェクトを「ピンポイント特定」します
‘ これにより、行番号(ID)がどれだけズレていても正確にヒットします
Set targetTask = pj.Tasks.UniqueID(uID)
If Not (targetTask Is Nothing) Then
‘ 対象タスクの「現在の行番号」を取得して、その直前に挿入
Dim currentID As Long
currentID = targetTask.ID
Set newTask = pj.Tasks.Add(Name:=”[確認用] ” & targetTask.Name & “の事前チェック”, Before:=currentID)
newTask.Duration = “2h” ‘ 2時間
newTask.PercentComplete = 0
End If
Next uID
‘ 画面更新を再開
ScreenUpdating = True
MsgBox “キャッシュ戦略によるタスク挿入が完了しました!”, vbInformation, “完了”
End Sub
—
4. 陥りやすいエラーと対策チェックリスト
Project VBAを扱う上で、誰もが一度は引っかかる落とし穴とその対策をまとめました。デバッグ時の参考にしてくださいね。
① `Nothing` エラー(エラー番号: 91 / 424)
- 原因: MS Projectのタスク一覧(`Tasks` コレクション)には、「空白行」が含まれることがあります。空白行をオブジェクトとして操作しようとすると、プログラムが強制終了します。
- 対策: タスクを取り出したら、必ず `If Not (currentTask Is Nothing) Then` で囲んで、実体が存在するかチェックしてください。
② 無限ループの発生
- 原因: 通常の「昇順ループ(`For i = 1 To Tasks.Count`)」の中でタスクを挿入し続けると、ループの終了条件である `Tasks.Count` が増え続け、いつまでも処理が終わらない、または意図しないタスクを永久に作り続けることになります。
- 対策: タスクを「追加・挿入」する場合は、この記事で紹介した「逆順ループ」か「UniqueIDキャッシュ」を徹底してください。
③ 画面がチカチカして処理が遅い
- 原因: タスクを挿入するたびに、MS Projectがガントチャートの描画やスケジュール(日付)の再計算を行うため、動作が非常に重くなります。
- 対策: 処理の開始時に `ScreenUpdating = False` を行い、終了時に `True` に戻しましょう。これだけで処理速度が数倍〜数十倍に跳ね上がります。
—
まとめ:Project VBAを掌握する第一歩を踏み出しましたね!
今回は、MS Project VBAの開発で最大の壁となる「タスク挿入時のインデックスズレ問題」について解説しました。
- 行番号(ID)は動いてしまう「席の番号」。
- UniqueIDは一生変わらない「名前(マイナンバー)」。
- タスクを追加・削除するときは、「お尻から処理する(逆順ループ)」か、「UniqueIDをメモしておく(キャッシュ戦略)」。
この2つのアプローチをマスターすれば、もうタスク操作でコードが暴走することはありません。Project VBAの基本構造は完全にあなたの手の中にあります。
「マクロの記録」から一歩踏み出し、このようにロジックを自分で組み立てられるようになると、業務自動化の幅は一気に広がりますよ。
あなたのProject VBA開発が、楽しくて実りあるものになりますように。応援しています!
