Project VBAの世界へようこそ!依存関係の「循環参照」をグラフ理論で解く:再帰的探索によるデッドロックの完全検出
こんにちは!いつも業務自動化の旅を一緒に歩んでくれてありがとうございます。頼れる先輩エンジニアの私と一緒に、今日も一歩、プログラミングの奥深い世界へ踏み出してみましょう。
「マクロの記録」を卒業し、ExcelやAccessの自動化に慣れてくると、次に挑戦したくなるのがMicrosoft Project(Project VBA)ですよね。大規模なシステム開発や建設工事などのスケジュール管理(WBS)を劇的に効率化できる強力なツールです。
しかし、Project VBAを扱い始めると、誰もが必ず一度はぶつかる「大きな壁」があります。それが、タスク同士のリンク(依存関係)がぐるぐるとループしてしまう「循環参照(デッドロック)」の問題です。
「タスクAが終わらないとBが始められない。でも、Bが終わらないとCが始められない。そして、Cが終わらないとAが始められない……」
このような矛盾したスケジュールを設定してしまうと、Projectは計算不能に陥り、最悪の場合は画面がフローズン(フリーズ)してしまいます。
今回は、この複雑なパズルを「グラフ理論」というプログラミングの美しい知恵を使って、エレガントに、かつ絶対にフリーズしない方法で検出するデバッグツールを作ってみましょう!
「グラフ理論とか、再帰的探索なんて難しそう……」と思ったあなたも大丈夫。一歩ずつ、図解を交えながら優しく紐解いていきます。ここをクリアすれば、Project VBAの基本だけでなく、アルゴリズムの基礎もバッチリ身につきますよ!
—
1. 基礎知識:「依存関係」と「循環参照」をビジュアルで理解しよう
まずは、Microsoft Projectの中でタスクがどのように繋がっているのか、そして「循環参照」がなぜ発生するのかを目で見てみましょう。
通常のタスクリンク(正しい状態)
スケジュールが正常なときは、タスクは川の流れのように一方向に進みます。
[タスク1: 要件定義] ───> [タスク2: 基本設計] ───> [タスク3: 詳細設計]
循環参照(異常な状態:デッドロック)
しかし、うっかり設定を誤ると、以下のように矢印がぐるっと戻ってきてしまいます。
┌──────────────────────────────────────────┐
│ ▼
[タスク1: 要件定義] ───> [タスク2: 基本設計] ───> [タスク3: 詳細設計]
▲ │
└──────────────────────────────────────────┘
タスク3がタスク1を待ち、タスク1がタスク2を待ち、タスク2がタスク3を待つ……。これではいつまで経っても作業を始められませんよね。これが「循環参照」であり、スケジュールのデッドロックです。
Project VBAでは、タスクの繋がりを `Task.PredecessorTasks`(先行タスク) や `Task.SuccessorTasks`(後続タスク) というオブジェクトの集まり(コレクション)として取得できます。これらをプログラムで辿っていくことで、ループがないかをチェックします。
—
2. 魔法のアルゴリズム「深さ優先探索(DFS)」とは?
このループを見つけるために、今回は「深さ優先探索(Depth-First Search: DFS)」というグラフ理論のアルゴリズムを使います。
難しく考える必要はありません。イメージは「一本道の迷路を、足跡をつけながら突き当たりまで進むゲーム」です。
探索の3つのルール(状態管理)
タスクを調査するとき、私たちは各タスクに「色」をつけて管理します。
1. 未訪問(白い足跡): まだ一度も調べていないタスク。
2. 探索中(青い足跡): 現在、その先のタスクを調査している最中のタスク。
3. 完了(黒い足跡): その先に行き止まり(または合流)まで調べ尽くし、ループがなかったことが証明されたタスク。
どうやってループを見つけるの?
迷路を進んでいる最中(青い足跡のルート上)に、「あれ?すでに青い足跡がついているタスクにまた戻ってきたぞ!」となった瞬間、そこが「循環(ループ)」の発生地点です!
[タスク1 (青)] ──> [タスク2 (青)] ──> [タスク3 (青)]
▲ │
└─────────── (青い足跡に戻った!循環検出!) ┘
この「自分の先にあるものを、さらに深く辿っていく」処理をスマートに書くために、プログラミングの高等技術である「再帰呼び出し(自分自身の関数を自分で呼び出すこと)」を使います。
—
3. Project VBA特有の「落とし穴」を回避する
コードを書く前に、Project VBAを扱う上での重要なポイントを先輩からアドバイスしておきますね。Excel VBAとは少しお作法が違います。
① 空白行(Nothing)のハンドリング
Microsoft Projectのタスク一覧には、ユーザーがうっかり空けた「空行」が存在することがあります。これをそのまま処理しようとすると、`Object Required`(オブジェクトが必要です)というエラーでプログラムが止まってしまいます。
必ず `If Not (t Is Nothing) Then` というチェックを入れましょう。
② IDとUniqueIDの違い
Projectのタスクには `ID`(画面上の行番号)と `UniqueID`(タスク固有の識別番号)があります。行を入れ替えると `ID` は変わってしまいますが、 `UniqueID` は一生変わりません。プログラムで状態を管理するときは、常に `UniqueID` をキーに使うのが鉄則です。
—
4. 【完全版】循環参照を検出するVBAコード
それでは、実際に動くコードを見てみましょう!
このコードは、現在開いているプロジェクトのすべてのタスクをスキャンし、循環参照(ループ)が見つかった場合は、その原因となっているタスクの繋がりをイミディエイトウィンドウに分かりやすく出力します。
コードをコピーして、ProjectのVBAエディタ(`Alt + F11`)に貼り付けて使ってみてください。
(※ 実行する前に、VBAエディタの「ツール」 ➔ 「参照設定」で Microsoft Scripting Runtime(Dictionaryを使うため)にチェックを入れてくださいね)
Option Explicit
‘ タスクの探索状態を定義する定数
Private Const STATE_UNVISITED As Integer = 0 ‘ 未訪問
Private Const STATE_VISITING As Integer = 1 ‘ 探索中(現在ルート上にある)
Private Const STATE_VISITED As Integer = 2 ‘ 探索完了(安全確認済み)
‘ 探索状態を保持する辞書
Private taskStates As Scripting.Dictionary
‘ 循環の経路を追跡するための配列(スタック)
Private pathStack As Collection
”’
”’
Public Sub CheckCircularReferences()
Dim proj As Project
Set proj = ActiveProject
‘ 辞書とコレクションの初期化
Set taskStates = New Scripting.Dictionary
Set pathStack = New Collection
Dim t As Task
‘ 1. すべてのタスクを「未訪問(0)」として初期化
For Each t In proj.Tasks
If Not (t Is Nothing) Then
taskStates(t.UniqueID) = STATE_UNVISITED
End If
Next t
Dim hasLoop As Boolean
hasLoop = False
Debug.Print “=== 循環参照チェックを開始します ===”
‘ 2. すべてのタスクを起点として探索を開始
For Each t In proj.Tasks
If Not (t Is Nothing) Then
‘ 未訪問のタスクがあれば、そこから深さ優先探索(DFS)を開始
If taskStates(t.UniqueID) = STATE_UNVISITED Then
If FindLoopDFS(t) Then
hasLoop = True
End If
End If
End If
Next t
If Not hasLoop Then
Debug.Print “調査完了: 循環参照は検出されませんでした。安心してください!”
End If
Debug.Print “=== チェックを終了しました ===”
End Sub
”’
”’
”’ 現在調査中のタスク
”’
Private Function FindLoopDFS(currentTask As Task) As Boolean
Dim currentID As Long
currentID = currentTask.UniqueID
‘ 1. 現在のタスクを「探索中(1)」にする
taskStates(currentID) = STATE_VISITING
pathStack.Add currentTask ‘ 経路スタックに現在のタスクを追加
‘ 2. 後続のタスク(SuccessorTasks)を1つずつ探索する
Dim succTask As Task
For Each succTask In currentTask.SuccessorTasks
If Not (succTask Is Nothing) Then
Dim succID As Long
succID = succTask.UniqueID
‘ パターンA: 後続タスクが「探索中(1)」なら循環発生!
If taskStates(succID) = STATE_VISITING Then
PrintLoopPath succTask ‘ 循環のルートをきれいに表示
FindLoopDFS = True
‘ バックトラック(再帰を戻るための片付け)
pathStack.Remove pathStack.Count
taskStates(currentID) = STATE_VISITED
Exit Function
‘ パターンB: 後続タスクが「未訪問(0)」なら、さらに深く進む
ElseIf taskStates(succID) = STATE_UNVISITED Then
If FindLoopDFS(succID_To_Task(succID)) Then
FindLoopDFS = True
‘ バックトラック
pathStack.Remove pathStack.Count
taskStates(currentID) = STATE_VISITED
Exit Function
End If
End If
End If
Next succTask
‘ 3. このタスクから進める道はすべて安全だったため「探索完了(2)」にする
taskStates(currentID) = STATE_VISITED
pathStack.Remove pathStack.Count ‘ 経路スタックから除外
FindLoopDFS = False
End Function
”’
”’
Private Function succID_To_Task(uID As Long) As Task
Set succID_To_Task = ActiveProject.Tasks.UniqueID(uID)
End Function
”’
”’
Private Sub PrintLoopPath(conflictTask As Task)
Dim msg As String
msg = “【警告】循環参照を検出しました!” & vbCrLf & ” 経路: ”
‘ スタックに溜まった経路を矢印で繋いで出力する
Dim i As Integer
Dim startPrinting As Boolean
startPrinting = False
For i = 1 To pathStack.Count
Dim t As Task
Set t = pathStack(i)
‘ 循環の起点となるタスクから表示を開始する
If t.UniqueID = conflictTask.UniqueID Then
startPrinting = True
End If
If startPrinting Then
msg = msg & “[” & t.Name & ” (ID:” & t.ID & “)] ──> ”
End If
Next i
‘ 最後にぶつかったタスクを結合してループを閉じる
msg = msg & “[” & conflictTask.Name & ” (ID:” & conflictTask.ID & “)] (ループ!)”
Debug.Print msg
End Sub
—
5. コードの解説:ここがプロの技!
このコードには、ただループを回すだけではない「プロの設計」が散りばめられています。ポイントを絞って解説しますね。
ポイント①:Dictionaryを使った「3つの状態管理」
関数 `FindLoopDFS` の中で、タスクの状態を `0(未訪問)`, `1(探索中)`, `2(探索完了)` の3つの数値で管理しています。
一度調べ終わって「安全(=循環がない)」と分かったタスクは状態が `2` になり、次回以降の探索ではスキップされます。これにより、タスクが何千個もある大規模なプロジェクトでも、一瞬で処理が終わる極めて高速なプログラムになります。
ポイント②:経路を記憶する「スタック」の活用
`pathStack` というコレクションを使って、今どのルートを通ってきたかを記録しています。ループが見つかったとき、このスタックの中身を上から順に取り出すことで、「どのタスクとどのタスクが犯人なのか」を正確に突き止めて画面に表示することができます。
—
6. 実行するとどうなる?(出力結果のイメージ)
例えば、以下のようなループを作ってマクロを実行してみます。
- タスクID 2「設計書作成」の後続に、タスクID 3「レビュー」を設定
- タスクID 3「レビュー」の後続に、タスクID 2「設計書作成」を設定(ループ!)
イミディエイトウィンドウ(`Ctrl + G` で表示)には、以下のように綺麗に出力されます。
=== 循環参照チェックを開始します ===
【警告】循環参照を検出しました!
経路: [設計書作成 (ID:2)] ──> [レビュー (ID:3)] ──> [設計書作成 (ID:2)] (ループ!)
=== チェックを終了しました ===
これを見れば、どのタスクのリンクを修正すれば良いかが一目瞭然ですね!
—
まとめ:ここをクリアすれば、Project VBAはもう怖くない!
お疲れ様でした!
一見難しそうな「循環参照」も、「足跡をつけながら進む」というグラフ理論の考え方を使えば、こんなにスマートで強力なツールが作れるのです。
今回のポイントをおさらいしておきましょう。
- Project独自のオブジェクト(`UniqueID` や `SuccessorTasks`)の性質を理解すること。
- 再帰処理を使うときは、無限ループを防ぐために「状態管理(未訪問・探索中・完了)」をしっかり行うこと。
- 空白行を無視するなどの安全設計(エラーハンドリング)を怠らないこと。
これが理解できれば、Project VBAの基本だけでなく、難関と言われるデータ構造とアルゴリズムの基礎もバッチリマスターできています。自信を持ってくださいね!
実務でスケジュールが複雑に絡み合って動かなくなったときは、ぜひこのコードを使って、チームのピンチを救ってあげてください。
あなたの業務自動化ライフが、さらに輝かしいものになりますように。また次のステップでお会いしましょう!
