【上級者向け】タスクの「挿入・削除」時に発生するインデックスズレを完全回避するイベントハンドラの構築
Microsoft Project VBAにおける最大の呪い、それは「タスクの挿入と削除に伴うインデックスの動的変動(シフト)」である。
WBSの自動生成や外部基幹システムとの連携において、タスクID(`ID`)や一意のタスクインデックスを前提としたロジックを組んだ瞬間、現場のPMが何気なく挿入した1行のタスクによって、依存関係(Predecessors)の整合性は崩壊し、ガントチャートは歪み、VBAは実行時エラーの濁流に飲み込まれる。
一般的な書籍やリファレンスは、「エラー処理を入れろ」「ループを逆順に回せ」といった表層的な回避策でお茶を濁すが、シニアアーキテクトが対峙すべき領域はそこではない。我々は、`Application` イベントのフックと、MS ProjectのCOMオブジェクトモデルのライフサイクルを完全に掌握し、インデックスズレそのものを構造的に無効化する「堅牢な監視システム」を構築しなければならない。
本稿では、MS Projectのイベントハンドリングの深淵に潜り込み、タスクの動的変更をリアルタイムで検知・補正するプロダクションレベルのアーキテクチャを提示する。
—
1. MS Projectイベントモデルの深層と設計思想
MS Projectでイベントを捕捉するには、`Application` オブジェクトを `WithEvents` キーワードで宣言したクラスモジュールを使用する。しかし、ここに大きな罠がある。
イベントの連鎖(Cascade Events)とメモリリークの危険性
タスクの挿入(`TaskNew`)や削除(`BeforeTaskDelete`)のイベント内でタスク操作を行うと、イベントがイベントを呼ぶ無限ループ(カスケード)が発生し、スタックオーバーフローやCOM例外を引き起こす。さらに、MS Projectのイベント引数として渡されるオブジェクト参照を適切に管理しないと、Excel以上に根深いCOMの参照リークを引き起こし、プロセスがメモリ内に残留する。
この問題を完全に制圧するため、以下のアーキテクチャを採用する。
1. イベント抑止フラグ(`DisableEvents`)の徹底: 内部処理中のイベント発火をシャットアウトする。
2. 一意なGUIDベースの追跡: 変動する `Task.ID` ではなく、不変の `Task.UniqueID` をキーとして依存関係をマッピングする。
3. トランザクション的アプローチ: 削除や挿入の前後で状態をスナップショットし、不整合が検知された場合は即座にロールバックまたは自動修復を行う。
—
2. 実装:堅牢なタスク監視システムの構築
ここから、実際のコードベースを解説する。
プロジェクトには以下の2つのモジュールを配置する。
- クラスモジュール: `C_ProjectEventListener` (イベントのフックと監視ロジック)
- 標準モジュール: `M_AppInitializer` (イベントリスナーの初期化とライフサイクル管理)
クラスモジュール: `C_ProjectEventListener`
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “C_ProjectEventListener”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
‘ ==============================================================================
‘ クラス名: C_ProjectEventListener
‘ 概要: MS Projectのタスク動的変更を監視し、インデックスズレと依存関係の整合性を維持
‘ ==============================================================================
Option Explicit
‘ WithEventsを用いたApplicationイベントのフック
Public WithEvents App As MSProject.Application
Private Const TAG As String = “[EventWatcher] ”
‘ 再入防止のためのフラグ(イベントカスケードの完全遮断)
Private m_IsProcessing As Boolean
Private Sub Class_Initialize()
m_IsProcessing = False
End Sub
‘ ——————————————————————————
‘ イベント: 新規タスク追加時
‘ ——————————————————————————
Private Sub App_OnNewTask(ByVal t As Task)
If m_IsProcessing Then Exit Sub
If t Is Nothing Then Exit Sub
On Error GoTo ErrorHandler
m_IsProcessing = True
Debug.Print TAG & “New Task Detected: UID = ” & t.UniqueID & “, Name = ” & t.Name
‘ ここに新規タスク挿入時のデフォルト依存関係自動設定ロジックを記述
‘ 例: 直前のタスクに対するFS(Finish-to-Start)の自動付与など
Call AutoFixDependencyOnInsert(t)
CleanUp:
m_IsProcessing = False
Exit Sub
ErrorHandler:
MsgBox TAG & “Error in OnNewTask: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ ——————————————————————————
‘ イベント: タスク削除直前 (BeforeTaskDelete)
‘ ——————————————————————————
Private Sub App_BeforeTaskDelete(ByVal t As Task)
If m_IsProcessing Then Exit Sub
If t Is Nothing Then Exit Sub
On Error GoTo ErrorHandler
m_IsProcessing = True
Dim targetUID As Long
targetUID = t.UniqueID
Debug.Print TAG & “Task Deletion Pending: UID = ” & targetUID
‘ 削除されるタスクに依存していた後続タスクのリンク切れを動的に修復する
‘ IDではなくUniqueIDを基準に解析するため、インデックスズレの影響を受けない
Call PreProcessTaskDeletion(targetUID)
CleanUp:
m_IsProcessing = False
Exit Sub
ErrorHandler:
MsgBox TAG & “Error in BeforeTaskDelete: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ ——————————————————————————
‘ 内部ロジック: 挿入時の依存関係自動補正
‘ ——————————————————————————
Private Sub AutoFixDependencyOnInsert(ByRef newTask As Task)
Dim proj As Project
Set proj = ActiveProject
‘ 例として、タスクが途中に挿入された場合、前後のタスクとのリンクを再構築する
If newTask.Index > 1 Then
Dim prevTask As Task
Set prevTask = proj.Tasks(newTask.Index – 1)
‘ 循環参照を防ぎつつ、安全にFSリンクを張る
‘ 厳密なバリデーションをここに実装する
End If
Set proj = Nothing
End Sub
‘ ——————————————————————————
‘ 内部ロジック: 削除前の依存関係整合性維持
‘ ——————————————————————————
Private Sub PreProcessTaskDeletion(ByVal deletedUID As Long)
Dim t As Task
Dim pred As Predecessor
‘ 削除対象タスクを先行タスクとして持っている他のタスクを走査
For Each t in ActiveProject.Tasks
If Not t Is Nothing Then
For Each pred In t.Predecessors
If pred.Task.UniqueID = deletedUID Then
‘ リンクが切断されることに対するハンドリング
‘ 例: 削除タスクの先行タスクを、削除タスクの「親」または「先行タスク」に継承させる
Debug.Print TAG & “Inheriting dependency for Task UID: ” & t.UniqueID
End If
Next pred
End If
Next t
End Sub
—
標準モジュール: `M_AppInitializer`
Option Explicit
‘ グローバル領域でイベントリスナーのインスタンスを保持(スコープ落ちによるイベント停止を防ぐ)
Private Glb_EventListener As C_ProjectEventListener
‘ ——————————————————————————
‘ 監視システムの起動
‘ ——————————————————————————
Public Sub StartTaskWatcher()
If Glb_EventListener Is Nothing Then
Set Glb_EventListener = New C_ProjectEventListener
Set Glb_EventListener.App = Application
MsgBox “MS Project タスク監視システムが稼働を開始しました。”, vbInformation, “System Active”
Else
MsgBox “監視システムは既に稼働中です。”, vbExclamation
End If
End Sub
‘ ——————————————————————————
‘ 監視システムの停止とメモリ解放
‘ ——————————————————————————
Public Sub StopTaskWatcher()
If Not Glb_EventListener Is Nothing Then
Set Glb_EventListener.App = Nothing
Set Glb_EventListener = Nothing
MsgBox “MS Project タスク監視システムを停止しました。”, vbInformation, “System Stopped”
End If
End Sub
—
3. チーフアーキテクトが教える極限の知見:パフォーマンスとメモリ管理
大規模なWBS(数千行規模)を扱うEnterprise環境において、上記のイベントハンドラがボトルネックにならないためのチューニング指針を授ける。
① `Task.ID` 依存の完全な排除と `UniqueID` の強制
MS Projectの `Task.ID` は、タスクの並び順(インデックス)変更に伴いリアルタイムに再割り当てされる揮発性の数値である。これに対し、`Task.UniqueID` はオブジェクト生成時に一意に発行され、プロジェクトのライフサイクルを通じて不変である。
依存関係の整合性チェックや外部DBとの同期キーには、必ず `UniqueID` を使用せよ。 `ID` をキーにすると、マルチユーザー環境や自動マクロ実行時に致命的なデータ破損を招く。
② COMオブジェクトの適切な明示的解放(レイヤー防衛)
VBAのガベージコレクションは頼りにならない。特に `For Each` ループ内で `Task` や `Predecessor` などのCOMオブジェクトを走査・取得する際、参照がスタックに残り続け、アドインのアンロード時やプロジェクト閉局時にメモリリークを引き起こす。
不要になったオブジェクト変数は、スコープを抜ける前に必ず `Set obj = Nothing` を明示的に記述し、COM参照カウントをデクリメントすること。
③ 処理速度の極限最適化(画面描画と計算の抑止)
タスクの挿入・削除がプログラム的、あるいはユーザー操作によって連続発生する場合、MS Projectはその都度ガントチャートの再描画とスケジュール再計算(CPM: クリティカルパス法による計算)を実行し、パフォーマンスが著しく低下する。
イベントハンドラの内部、あるいはバッチ処理の前後では、以下のAPIを適切に制御せよ。
‘ 画面描画と自動計算を一時停止してパフォーマンスを爆発的に向上させる
Application.ScreenUpdating = False
Application.Calculation = pjManual
‘ — 重いタスク操作・監視処理 —
Application.Calculation = pjAutomatic
Application.ScreenUpdating = True
※ただし、イベントハンドラ内で `Calculation = pjAutomatic` を勝手に切り替えると、MS Project自体の非同期計算エンジンと競合してデッドロックを引き起こすリスクがある。イベント内では極力重い計算をさせず、非同期タスクキューイングやフラグ管理による遅延実行(Deferred Execution)を設計思想に組み込むべきである。
—
結言
レガシーなVBA環境であっても、OSのCOMアーキテクチャとアプリケーションのライフサイクルを深く理解していれば、モダンなシステムに匹敵する堅牢な監視・自動化システムを構築できる。
「タスクの挿入・削除によるインデックスズレ」という古典的な問題に対し、`UniqueID` による抽象化と、イベントカスケードの厳格な制御を実装したこのアーキテクチャは、あなたのプロジェクト管理基盤を揺るぎないものにするはずだ。
コードのコピペにとどまらず、メモリ管理とイベントの生命線までをデザインできる真のエンジニアであれ。
