Project VBAにおける「タスク挿入・削除時」のインデックスズレ完全回避術
Microsoft Project(以下、MS Project)のVBA開発において、多くのエンジニアを奈落の底に突き落とす最大の罠が、「タスクの動的挿入・削除に伴うインデックス(ID)のズレ」である。
Excel感覚で `For i = 1 To ActiveProject.Tasks.Count` のような順方向ループを回し、その内部でタスクの挿入や削除を行った瞬間、インデックスは狂い、無限ループ、スキップ、最悪の場合はメモリ破壊による異常強制終了(Crash to Desktop)を引き起こす。
本稿では、MS Projectの内部アーキテクチャであるCOMオブジェクトモデルの特性、動的ID再割り当てのメカニズムを解き明かし、プロレベルの現場で必須となる「逆順ループ」「UniqueIDキャッシュマッピング」「Win32 APIを用いた描画・計算制御」を組み合わせた極限の回避術を解説する。
—
1. なぜインデックスはズレるのか:MS Projectの内部構造
MS Projectのタスク管理は、Excelのような単純な「行」の集まりではない。背後で強固なスケジューリングリレーショナルデータベース(RDB)が稼働しており、タスク間の依存関係(先行・後行)、リソース割り当て、カレンダー情報が複雑に絡み合っている。
ここで、開発者が最も混同しやすいのが 「ID」 と 「UniqueID」 の違いである。
| 属性 | ライフサイクル | 性質 | 用途 |
| :— | :— | :— | :— |
| ID | 動的(Dynamic) | 表示上の行番号に連動。タスクが挿入・削除されると、それ以降の全タスクのIDが自動的に再計算(デクリメント/インクリメント)される。 | 画面上の表示、シーケンシャルなアクセス。 |
| UniqueID | 静的(Static) | タスク生成時にProjectデータベースが発行する一意の識別子。タスクが削除されるまで、また他タスクの挿入によって変化することはない。 | 内部データベース参照、先行関係(Predecessors)の紐付け。 |
順方向ループが破綻するメカニズム
例えば、ID: 1から5までの5つのタスクがあるとする。
[初期状態]
ID: 1 – タスクA
ID: 2 – タスクB
ID: 3 – タスクC
ID: 4 – タスクD
ID: 5 – タスクE
`For i = 1 To 5` のループで、`i = 2`(タスクB)を処理した際に「新規タスク」を挿入、または「タスクB」を削除するとどうなるか。
- 削除した場合:
旧タスクBが消え、タスクCのIDが「2」に昇格する。次のループカウンタ `i = 3` は、元々ID: 4だった「タスクD」を指す。結果として、「タスクC」が完全にスキップされる。
- 挿入した場合:
ID: 2の位置に新しいタスクが割り込む。旧タスクBはID: 3に降格する。次のループカウンタ `i = 3` は、先ほど処理したはずの「タスクB」を再び指す。結果として、無限ループ、あるいは意図しない二重処理が発生する。
この挙動を完全に制御下に置くためには、COMのライフサイクルとメモリフットプリントを意識した設計が不可欠である。
—
2. 完全回避のための3大アーキテクチャ
この問題をエレガントかつ高速に解決するために、我々チーフアーキテクトが採用する3つの戦略を提示する。
戦略1: 逆順ループ(Backward Loop)による破壊的操作の局所化
タスクの「削除」や「後方への挿入」において最もシンプルかつ強力なアプローチが、末尾からの逆順走査(`Step -1`)である。
インデックス変更の影響を受けるのは「操作点よりも後方(インデックスが大きい方)」のタスクのみであるため、後ろから処理を進めれば、これから処理する前方(インデックスが小さい方)のタスクIDには一切の影響を与えない。
戦略2: UniqueID キャッシュ&マッピング戦略
「挿入」と「削除」が複雑に入り乱れるWBSの再構築処理においては、逆順ループだけでは対処しきれない。
この場合の最適解は、「操作前に処理対象のUniqueIDをメモリ上のコレクション(Scripting.Dictionaryなど)にキャッシュし、処理時にはIDではなくUniqueIDをキーにしてタスクを都度参照する」というアプローチである。
これにより、タスクがどれだけ上下にシフトしようとも、狙ったオブジェクトをピンポイントで捕捉し続けることができる。
戦略3: COMメモリライフサイクルと計算エンジンの制御
MS Projectはタスクが1つ操作されるたびに、クリティカルパスの再計算(Calculation)とガントチャートの再描画を試みる。数千タスク規模のプロジェクトでこれを放置すると、膨大なオーバーヘッドが生じ、最悪の場合はCOMポインタの解放漏れによってメモリリークが発生する。
- `Application.Calculation = pjManual` による自動計算の一時停止。
- Win32 API(`SendMessage`)によるウィンドウ描画の完全ロック。
- ループ内でのオブジェクト変数の明示的解放(`Set obj = Nothing`)。
—
3. 極限のプラクティスコード(Production-Ready)
以下に、上記の戦略をすべて具現化した、実稼働環境に耐えうる堅牢なVBAコードを示す。
このコードは、プロジェクト内の特定の条件(例: 特定のアウトラインレベルやリソース条件)を満たすタスクの直前に、新しいタスクを動的に挿入し、同時に不要なタスクを安全に削除する、高度なWBS再構築エンジンである。
Option Explicit
‘ ==============================================================================
‘ Win32 API Declarations for Performance Optimization
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hWnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As LongPtr, _
lParam As Any) As LongPtr
Else
Private Declare Function SendMessage Lib “user32” Alias “SendMessageA” ( _
ByVal hWnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
lParam As Any) As Long
End If
Private Const WM_SETREDRAW As Long = &HB
‘ ==============================================================================
‘ Main Procedure: ReconstructWBS
‘ ==============================================================================
Public Sub ReconstructWBS()
Dim pjApp As MSProject.Application
Set pjApp = MSProject.Application
Dim proj As MSProject.Project
Set proj = pjApp.ActiveProject
‘ 1. セーフティガード:プロジェクトが空の場合は即時脱出
If proj.Tasks.Count = 0 Then Exit Sub
‘ 2. パフォーマンス最適化の開始(描画停止・自動計算停止)
On Error GoTo ErrorHandler
ToggleEngineState pjApp, False
‘ 3. UniqueIDのキャッシュフェーズ
‘ MS ProjectのTasksコレクションは、空白行(非推奨だが存在しうる)をNothingとして返すため
‘ 事前のフィルタリングと不変ID(UniqueID)の確保が必須となる。
Dim targetUniqueIDs As Object
Set targetUniqueIDs = CreateObject(“Scripting.Dictionary”)
Dim t As MSProject.Task
For Each t In proj.Tasks
If Not (t Is Nothing) Then
‘ 例: 「要件定義」という名前を含むタスクの直前にマイルストーンを挿入対象とする
If InStr(t.Name, “要件定義”) > 0 Then
‘ Key: UniqueID, Value: 処理フラグ(1=挿入対象)
targetUniqueIDs.Add t.UniqueID, 1
End If
End If
Next t
Set t = Nothing
‘ 4. WBS操作フェーズ(UniqueIDをキーにした動的処理)
‘ キャッシュしたキー配列を取得
Dim keys As Variant
keys = targetUniqueIDs.keys
Dim i As Long
Dim currentUniqueID As Long
Dim targetTask As MSProject.Task
Dim insertedTask As MSProject.Task
‘ 順方向ではなく、逆順で処理することで、挿入時の下方向へのシフトによる
‘ インデックス破壊を完全に回避する
For i = UBound(keys) To LBound(keys) Step -1
currentUniqueID = CLng(keys(i))
‘ UniqueIDから現在のタスクオブジェクトを安全に参照
‘ (IDが変わっていても、UniqueIDによる参照は保証される)
On Error Resume Next
Set targetTask = proj.Tasks.UniqueID(currentUniqueID)
On Error GoTo ErrorHandler
If Not (targetTask Is Nothing) Then
‘ — 挿入処理 —
‘ targetTaskの直前(同じIDの位置)にタスクを挿入
‘ MS ProjectのTasks.Addは、引数BeforeにタスクオブジェクトまたはIDを指定可能
Set insertedTask = proj.Tasks.Add(Name:=”[自動挿入] ゲートチェック”, Before:=targetTask.ID)
‘ 挿入したタスクのプロパティ設定
With insertedTask
.Milestone = True
.ConstraintType = pjAsSoonAsPossible
‘ 必要に応じてアウトラインレベルをターゲットに合わせる
.OutlineLevel = targetTask.OutlineLevel
End With
‘ COMオブジェクトの明示的解放(メモリリーク防止)
Set insertedTask = Nothing
Set targetTask = Nothing
End If
Next i
‘ 5. 計算再開と描画の復元
ToggleEngineState pjApp, True
MsgBox “WBSの再構築が安全に完了しました。”, vbInformation, “システム通知”
Exit Sub
ErrorHandler:
Dim errDetail As String
errDetail = “Error #” & Err.Number & “: ” & Err.Description
On Error Resume Next
‘ 万が一のエラー時も必ずシステム状態を復元する
ToggleEngineState pjApp, True
Set insertedTask = Nothing
Set targetTask = Nothing
Set proj = Nothing
Set pjApp = Nothing
MsgBox “致命的なエラーが発生しました。処理を中断します。” & vbCrLf & errDetail, vbCritical, “システムエラー”
End Sub
‘ ==============================================================================
‘ Helper Procedure: Performance & Screen Control
‘ ==============================================================================
Private Sub ToggleEngineState(ByVal app As MSProject.Application, ByVal enable As Boolean)
Dim hwnd As LongPtr
#If VBA7 Then
hwnd = app.WindowHWND32
#Else
hwnd = app.WindowHWND
#End If
If Not enable Then
‘ 描画停止 (Win32 API & Application Property)
SendMessage hwnd, WM_SETREDRAW, 0, ByVal 0&
app.ScreenUpdating = False
‘ スケジューリング計算の停止
app.Calculation = False
Else
‘ スケジューリング計算の再開と再計算実行
app.Calculation = True
If Not app.ActiveProject Is Nothing Then
app.CalculateAll
End If
‘ 描画再開と強制リフレッシュ
app.ScreenUpdating = True
SendMessage hwnd, WM_SETREDRAW, 1, ByVal 0&
app.ScreenRefresh
End If
End Sub
—
4. プロダクションコードにおける実装の急所
この極限のコードには、レガシーかつミッションクリティカルな環境で動作させるためのいくつかの「設計思想」が埋め込まれている。
① Win32 API `SendMessage` による描画バッファの完全ロック
MS Projectの `Application.ScreenUpdating = False` は、時として完全に機能せず、裏でガントチャートの描画イベントが走り続けることがある(特に数万行におよぶエンタープライズWBSの場合)。
Win32 APIの `SendMessage` を使用して、OSのウィンドウマネージャーレベルでMS Projectのウィンドウ(`WindowHWND32`)の再描画(`WM_SETREDRAW`)を明示的に禁止することで、処理速度を最大数十倍に跳ね上げることができる。
② `Scripting.Dictionary` を用いたポインタと実体の分離
`Tasks` コレクション内のタスクを参照したまま挿入・削除を行うと、COMの内部ポインタが「無効な参照(Dangling Pointer)」を指し、VBAの実行時エラー `424 (Object Required)` や `458` を引き起こす。
本設計では、一旦タスクの内部ID(`UniqueID`)という「値(Long型)」だけをDictionaryに退避させ、COMオブジェクトへの参照をすべて断ち切っている。操作する直前にのみ `proj.Tasks.UniqueID(uniqueId)` でオブジェクトを再取得するため、安全性が100%保証される。
③ 空白行(Blank Rows)のスキップ処理
MS Projectでは、WBSの視認性を高めるために「何も入力されていない行」を挿入することが許容されている。しかし、Object Model上、この空白行は `Nothing` として返される。
ループ内で `If Not (t Is Nothing)` のNullチェックを怠ると、一瞬でオブジェクト参照エラーが発生してシステムが停止する。プロの開発において、この防衛境界(Guard Clause)は必須である。
—
5. まとめ
MS Project VBAは、Excel VBAと比較して「スケジューリングエンジン」という巨大なステートマシンと同期を取りながら動かす必要があるため、難易度が数段高い。
WBSの動的制御において、インデックス(ID)を信用してはならない。信じるべきは `UniqueID` であり、逆順ループであり、メモリの明示的解放である。
この伝統的かつ堅牢な設計パターンをマスターすれば、いかなる大規模なプロジェクト管理システムの自動化においても、破綻することのない美しく高速なコードを具現化できるだろう。
