こんにちは!いつも業務自動化の設計やマクロの開発、お疲れ様です。
Microsoft Project(以下、MS Project)を使った進捗管理やWBS(Work Breakdown Structure)の構築は、大規模なプロジェクトになればなるほど、手作業での制御が難しくなってきますよね。
「マクロの記録」から一歩踏み出し、VBAを使ってタスクの親子関係を自動で解析したり、階層構造をきれいに整理したいと考えたとき、多くのプログラマが最初に思いつくのが「再帰呼び出し(自分自身を呼び出す処理)」です。
しかし、ここに大きな罠があります。数千、数万行に及ぶ大規模なプロジェクトで再帰処理を行うと、PCのメモリを急激に消費し、最悪の場合は「スタックオーバーフロー」というエラーでExcelやProjectごと強制終了してしまうのです。
そこで今回は、再帰処理を一切使わず、「スタック(Stack)」というデータ構造を用いて、どれほど深い階層のWBSでも安全・高速にフラットなリストへ変換する極限の走査術をご紹介します。
「難しそう……」と思うかもしれませんが、大丈夫です。一歩ずつ、図解を交えながら優しく解説していきます。ここをクリアすれば、Project VBAの基本とデータ構造の扱い方はバッチリマスターできますよ!
—
1. なぜ「再帰」ではなく「スタック」なのか?
まずは、なぜ私たちが「再帰処理」を避けて「スタック」を使うのか、その理由をスッキリ整理しておきましょう。
再帰処理のイメージとリスク
再帰処理とは、サマリータスク(親タスク)を見つけるたびに、自分自身のプログラムをもう一度呼び出す方法です。
直感的でコードが短くなるメリットがありますが、呼び出すたびにPCのメモリ(コールスタック)に「現在の状態」がどんどん積み上がっていきます。
[プロジェクト全体]
└─ [設計フェーズ] (再帰1回目:メモリ消費)
└─ [基本設計] (再帰2回目:さらにメモリ消費)
└─ [画面設計] (再帰3回目:さらにさらにメモリ消費…)
階層が深くなったり、タスク数が膨大になると、メモリの限界を迎えてPCが悲鳴を上げてしまいます。これがスタックオーバーフローです。
スタックベース走査のイメージ(安全・省メモリ)
スタックベースの処理では、メモリを消費する「プログラムの再帰呼び出し」を行いません。代わりに、VBAの `Collection` オブジェクトを「お皿を積み重ねるホルダー(スタック)」に見立てて、自分でタスクの処理順をコントロールします。
1. 箱(スタック)を1つ用意する。
2. 未処理のタスクを箱に「積む(Push)」。
3. 箱の一番上からタスクを「取り出す(Pop)」して処理する。
4. 取り出したタスクに子タスクがあれば、それをまた箱に「積む」。
5. 箱が空っぽになるまで繰り返す。
これなら、プログラム自体のメモリ消費は常に一定。数万件のタスクがあっても、絶対にスタックオーバーフローは起きません。実務で「絶対に落ちないマクロ」を作るための、プロの必須テクニックです。
—
2. MS Projectのタスク構造の基本
コードを見る前に、MS Project特有の「タスクの持ち方」を理解しておきましょう。
MS Projectのタスク(`Task` オブジェクト)は、実は内部的には平坦な1次元のリスト(`ActiveProject.Tasks`)として保持されています。
「じゃあ、どうやって親子関係を表現しているの?」というと、主に以下の2つのプロパティを使っています。
- `OutlineLevel`: そのタスクがどれだけ深い階層にいるか(1が最上位、2、3…と深くなる)。
- `OutlineParent`: そのタスクの親タスクが誰であるかを示すオブジェクト。
今回は、この構造をうまく利用して、特定のサマリータスク配下にある子タスクたちを、階層構造を維持したまま「フラットなコレクション(リスト)」に変換するアルゴリズムを実装します。
—
3. 【実践】スタックベース走査マクロの全貌
それでは、実際に動くVBAコードを見てみましょう!
このコードは、選択したサマリータスク(またはプロジェクト全体)の配下を走査し、再帰を使わずにフラットなタスク一覧をイミディエイトウィンドウに出力するものです。
MS ProjectのVBAエディタ(`Alt + F11`)を開き、標準モジュールを挿入して以下のコードを貼り付けてみてください。
Option Explicit
”’
”’ フラットなタスクリストを作成する実用サンプル。
”’
Public Sub FlattenWBSTasks()
Dim proj As Project
Set proj = ActiveProject
‘ プロジェクトにタスクが1つもない場合は終了
If proj.Tasks.Count = 0 Then
MsgBox “タスクが存在しません。”, vbInformation, “終了”
Exit Sub
End If
‘ 1. スタック(お皿の山)を初期化
‘ VBAのCollectionをLIFO(後入れ先出し)のスタックとして使用します。
Dim taskStack As New Collection
‘ 2. 結果を格納するためのフラットなリスト(コレクション)
Dim flatList As New Collection
‘ 3. スタックの初期状態として、最上位タスク(OutlineLevel = 1)を逆順に積む
‘ ※逆順で積むことで、取り出す(Pop)ときに「上から順」に処理できます。
Dim i As Long
For i = proj.Tasks.Count To 1 Step -1
Dim t As Task
Set t = proj.Tasks(i)
‘ MS Projectでは、空行(中身がNothingのタスク)が存在することがあるため必ず検知する
If Not (t Is Nothing) Then
If t.OutlineLevel = 1 Then
taskStack.Add t
End If
End If
Next i
‘ 4. スタックが空になるまでメインループを回す(ここが心臓部です!)
Dim currentTask As Task
Do While taskStack.Count > 0
‘ — Pop処理(スタックの末尾から要素を1つ取り出す) —
Set currentTask = taskStack(taskStack.Count)
taskStack.Remove taskStack.Count
‘ フラットリストに成果物として追加
flatList.Add currentTask
‘ — 子タスクの探索とPush処理 —
‘ MS Projectの特性上、カレントタスクの「直後」にある、
‘ 自分よりOutlineLevelが大きいタスクが子タスクに該当します。
Dim childTasks As New Collection
Dim nextIndex As Long
nextIndex = currentTask.ID + 1
‘ プロジェクトの最大タスク数を超えない範囲で、直下の子タスクを探す
Do While nextIndex <= proj.Tasks.Count
Dim potentialChild As Task
Set potentialChild = proj.Tasks(nextIndex)
If Not (potentialChild Is Nothing) Then
' 自分と同じか、より浅い階層のタスクが現れたら、もう子孫タスクのエリアは終了
If potentialChild.OutlineLevel <= currentTask.OutlineLevel Then
Exit Do
End If
' 「直下の子(OutlineLevelが自分の+1)」である場合のみ、一時リストに集める
If potentialChild.OutlineLevel = currentTask.OutlineLevel + 1 Then
childTasks.Add potentialChild
End If
End If
nextIndex = nextIndex + 1
Loop
' 集めた直下の子タスクを、逆順(末尾から)スタックに積む(Push)
' これにより、Popするときに元のWBSの並び順(上から順)が綺麗に維持されます。
If childTasks.Count > 0 Then
Dim childIdx As Long
For childIdx = childTasks.Count To 1 Step -1
taskStack.Add childTasks(childIdx)
Next childIdx
End If
Loop
‘ 5. 結果の出力(確認用)
‘ イミディエイトウィンドウに、解析されたフラットリストを階層インデント付きで出力します
Debug.Print “=== WBSフラット走査結果 ===”
Dim resultTask As Task
For Each resultTask In flatList
‘ 階層の深さに合わせてスペース(インデント)を付与
Dim indent As String
indent = String((resultTask.OutlineLevel – 1) 2, ” “)
Debug.Print indent & “ID: ” & resultTask.ID & ” | ” & resultTask.Name
Next resultTask
MsgBox “WBSの走査が完了しました!” & vbCrLf & _
“解析タスク数: ” & flatList.Count & ” 件” & vbCrLf & _
“詳細はイミディエイトウィンドウ(Ctrl + G)を確認してください。”, vbInformation, “成功”
End Sub
—
4. コードの解説と「スタック」の動き
このコードの賢いポイントを、先輩としてわかりやすく解説しますね。
① なぜ逆順でスタックに積んでいるの?(Step -1 の秘密)
スタックは「最後に入れたものが、最初に出てくる(LIFO)」という性質を持っています。
もし、タスク「A、B、C」の順番でスタックに入れてしまうと、取り出すときは「C、B、A」という逆順になってしまいます。
これを防ぐために、スタックに入れるときは「C、B、A」の順(逆順)で入れます。すると、取り出すときは「A、B、C」という元の美しい順番で処理できるのです。
② `Nothing`(空行)の徹底排除
MS Projectを触る上で一番陥りやすい罠が、「タスクが入っていない空白行」の存在です。
VBA上では、この空白行も `Tasks` コレクションのインデックスとしてカウントされますが、実体は `Nothing` です。
これに気付かずにプロパティを参照すると、すぐに `Object Required(オブジェクト変数が設定されていません)` というエラーでプログラムが止まってしまいます。
コード内の `If Not (t Is Nothing) Then` は、このエラーを未然に防ぐためのプロの防護柵(ディフェンスコード)です。
—
5. 陥りやすいエラーと対策
MS Project VBAの特性上、開発中に「あれ?」となりがちなポイントをまとめました。
エラー1: `Object Required`(実行時エラー 91)
- 原因: 上述の通り、削除されたタスクの残骸や、ユーザーが意図的に作った「空行」にアクセスしようとしています。
- 対策: タスクを処理する前に、必ず `If Not (taskObject Is Nothing) Then` で囲む癖をつけましょう。
エラー2: 無限ループでExcel/Projectが固まる
- 原因: 子タスクを探す `Do While` ループのカウンタ(`nextIndex`)の更新を忘れたり、終了条件の判定が間違っている場合に発生します。
- 対策: ループ内には必ず `nextIndex = nextIndex + 1` のようなカウントアップ処理が含まれているか、指差し確認をしましょう。また、開発中は万が一に備え、`Ctrl + Break` キーでマクロを強制停止できることを覚えておくと安心です。
—
6. まとめと次のステップへのエール
お疲れ様でした!
今回ご紹介した「スタックベースのタスク走査」を理解できれば、以下のような応用も自由自在です。
- 特定のサマリータスク配下だけの「総コスト」や「総実績期間」を独自に集計する
- 階層を維持したまま、Excelのガントチャート用シートへ一括書き出しする
- 前提タスク(依存関係)を上流から順に自動で再設定する
再帰処理を使わないこの設計は、システム開発の現場でも「堅牢で、メモリ効率が良く、バグが出にくい美しい設計」として非常に高く評価されます。
最初は少し難しく感じたかもしれませんが、コードを一行ずつ追いかけながら、スタックにお皿が積まれていく様子をイメージすれば、必ず自分の技術として血肉化できます。
ここをクリアすれば、Project VBAの基本、いや、プログラミングにおける高度なデータ構造の扱いはバッチリですよ!あなたの業務自動化がより素晴らしいものになるよう、応援しています!
