こんにちは!プロジェクトマネジメントの現場で、日々数千行に及ぶWBS(Work Breakdown Structure)と格闘していませんか?
「マクロの記録」から一歩進んで、VBAでプロジェクトを自在にコントロールしようとすると、必ずぶ
つかる壁があります。それが「階層構造の走査」です。
特に、階層が何重にも深くなった巨大なWBSをVBAで処理しようとした瞬間、Excelとは比べものにならない独特のトラップにハマります。「あれ、急に動作が重くなった…」「突然Excel(Project)ごとフリーズした…」そんな経験はありませんか?
今回は、MS Project VBAのオブジェクトモデルの裏側にある「ライフサイクル」と「メモリの重み」を紐解きながら、スタックオーバーフローを華麗に回避し、巨大プロジェクトを秒速で処理するための「再帰呼び出しの極意」を伝授します。
ここをクリアすれば、あなたのProject VBAのスキルは間違いなくプロの領域に達しますよ。一緒にマスターしていきましょう!
—
1. なぜ、深いWBSの処理でProject VBAはフリーズするのか?
MS Projectのデータ構造は、Excelのような単純な「2次元の表」ではありません。親タスク(Summary Task)があり、その子供(Sub Task)がぶら下がり、さらにその下に孫タスクが……という、「ツリー構造(階層構造)」をしています。
このツリー構造をコードで上から下まで舐めるように走査する際、最も直感的に思いつくのが「再帰呼び出し(Recursive Call)」です。自分自身の関数の中で、自分をもう一度呼び出すアレですね。
‘ 【アンチパターン】よくある素朴な再帰処理
Sub TraverseTasks(t As Task)
Debug.Print t.Name
Dim subT As Task
For Each subT In t.Collection ‘ ここが諸悪の根源になり得る
Call TraverseTasks(subT)
Next subT
End Sub
このコード、タスクの階層が浅いうちは何何の問題もなく動きます。しかし、WBSの深さが10階層、20階層と深くなり、総タスク数が数万件に達した途端に悲劇が起きます。
原因の正体:コールスタックの爆発とCOMオブジェクトの肥大化
1. スタックオーバーフロー: 関数が自分を呼び出すたびに、コンピュータのメモリ(コールスタック)に「戻り番地」や「ローカル変数」が積み上げられます。階層が深すぎると、スタック領域がパンクします。
2. COMオブジェクトの参照リーク: Project VBAの裏側では、OfficeアプリケーションとCOM(Component Object Model)が会話しています。`For Each` でオブジェクトを次々に取得する際、適切な解放を行わないと、メモリ上にゴミ(参照)が残り続け、ガベージコレクションが追いつかなくなります。
—
2. 【極意】スタックを汚さない!「非再帰(スタック明示管理)」または「スマート再帰」の設計
では、深いWBSを安全に処理するにはどうすればよいでしょうか?
アプローチは大きく分けて2つあります。
1. 配列やコレクションを「自前スタック」として使う(非再帰アプローチ)
2. 参照のスコープを極限まで絞り、即座にメモリを解放する(スマート再帰アプローチ)
今回は、実務で最もエレガントに記述でき、かつメモリ効率を高めた「スマート再帰アプローチ」のコードを授けます。
実践:巨大WBSを安全かつ高速に走査するテンプレート
以下のコードは、Projectの全タスクを安全に走査し、すべてのサマリータスク(親タスク)のコストや進捗を最適化するシーンを想定したテンプレートです。そのままコピーしてVBE(Visual Basic Editor)に貼り付けて使えます。
‘ ==============================================================================
‘ モジュール名: 巨大WBS安全走査エンジンのサンプル
‘ 概要: スタックオーバーフローとメモリリークを防ぐ、洗練されたタスク走査
‘ ==============================================================================
Sub RunSafeWBSTraversal()
Dim prj As Project
Set prj = ActiveProject
‘ 処理開始のログ
Debug.Print “— WBS走査処理を開始します —”
Dim startTime As Double
startTime = Timer
‘ 処理の安全性を高めるため、画面描画と自動計算を一時停止(爆速化の基本!)
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
‘ ルートレベル(階層1)のタスク群から走査を開始
Dim t As Task
For Each t In prj.Tasks
‘ 外部から挿入された空行やNothingをガード
If Not t Is Nothing Then
‘ トップレベル(親を持たないタスク)からのみ再帰をスタート
If t.OutlineLevel = 1 Then
Call ProcessTaskRecursive(t)
End If
End If
Next t
ErrorHandler:
If Err.Number <> 0 Then
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
End If
‘ 画面描画の復元(絶対に忘れないこと)
Application.ScreenUpdating = True
Debug.Print “— 処理終了 (実行時間: ” & Format(Timer – startTime, “0.00秒”) & “) —”
End Sub
‘ ——————————————————————————
‘ 内部関数: スマート再帰プロシージャ
‘ ——————————————————————————
Private Sub ProcessTaskRecursive(ByVal parentTask As Task)
‘ 【極意1】オブジェクト変数をローカルで完結させ、スコープを抜けるときに消滅させる
Dim childTask As Task
‘ — ここにタスクに対するメイン処理を記述 —
‘ 例:名前を出力する(実務ではカスタムフィールドの書き換えなど)
‘ Debug.Print Spacing(parentTask.OutlineLevel) & parentTask.Name
‘ ——————————————-
‘ 【極意2】子タスクの存在チェック
‘ Projectのタスクモデルでは、子がいる親は .AggregateChildren などではなく
‘ 下位のOutlineLevelを持つタスクを走査するのが確実。
‘ ここではスマートに「Subproject」や「OutlineChildren」相当の走査を行う。
‘ ※実務的な簡易アプローチ:プロジェクト全体のタスクリストから
‘ 親タスクのIDと階層関係を利用して次の子を特定する、または
‘ あらかじめコレクション化する手法をとります。
‘ ここでは簡潔に、子タスク(OutlineChildren)を安全に取得するパターンを示します。
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
‘ 親タスクの直下にあり、かつ階層が1つ深いものを「子」とみなす判定ロジック
‘ (※Projectのデータ構造に応じた厳密なID比較を入れるとさらに堅牢になります)
If t.UniqueID > parentTask.UniqueID And t.OutlineLevel = parentTask.OutlineLevel + 1 Then
‘ 次の兄弟タスクが出てきたら、この親の子孫走査を終了する(パフォーマンス向上!)
‘ ※実際のWBS順序に依存するため、厳密なループ設計がここでのキモになります。
‘ 再帰呼び出し(メモリの無駄食いを防ぐため、変数の使い回しに注意)
Call ProcessTaskRecursive(t)
ElseIf t.UniqueID > parentTask.UniqueID And t.OutlineLevel <= parentTask.OutlineLevel Then
' 親の階層に戻った、または次の兄弟に移った場合はループを抜ける
Exit For
End If
End If
Next t
' 【極意3】明示的なオブジェクトの破棄(VBAのメモリ管理の鉄則)
Set childTask = Nothing
End Sub
---
3. コードのキモ:ここが分かればプロフェッショナル!
上記のコードに散りばめられた、シニアエンジニアならではの「防衛的プログラミング」のポイントを解説します。
① `Application.ScreenUpdating = False` の絶対的効果
Excel VBAと同様に、MS Projectでもコードの実行中に画面(ガントチャートやタスクシート)が再描画されると、莫大なCPUパワーが消費されます。巨大なWBSを触るときは、処理の最初に画面描画を止め、最後に必ず戻す。これだけで処理速度が数倍〜数十倍変わります。エラーハンドラー(`On Error GoTo`)を必ず挟んで、途中で止まっても画面がフリーズしたままにならないようにする配慮もプロの証です。
② タスクIDとアウトラインレベルの逆算
MS Projectのタスクオブジェクトには、実は「直の子タスクのコレクションを直接取得するプロパティ」がありません(※バージョンやオブジェクトモデルの仕様による)。そのため、全タスクをなめる `For Each` と組み合わせる必要がありますが、ここで愚直に全件スキャンすると $O(N^2)$ の計算量になり、巨大プロジェクトで完全に沈黙します。
上記のサンプルコードのように、「IDが親より大きく、かつアウトラインレベルが直属の子であるものだけをピンポイントで拾い、次の兄弟が現れたら即座に抜ける(`Exit For`)」という枝刈り(Pruning)の思想を入れることが、パフォーマンスを保つ最大の秘訣です。
③ COMオブジェクトの「デタッチ」
VBAは自動でメモリ管理(ガベージコレクション)をしてくれますが、それはあくまでVBAの世界の話。背後で動いているC++製のMicrosoft Project本体(COMサーバー)とのパイプは、プログラマが意図的に `Set xxx = Nothing` として切断してあげる必要があります。特に再帰呼び出しの中では、これが数千回蓄積するため、メモリリークの温床になります。
—
4. まとめ:基本を極めた先にある「安定感」
いかがでしたでしょうか?
今回は、Project VBAにおける「再帰呼び出しの最適化」という、ややディープで実務に直結するテーマをお届けしました。
- 階層の深いWBSは、コールスタックとメモリリークの危険地帯。
- 画面描画の停止(ScreenUpdating)で無駄なコストを削る。
- 条件判定による「枝刈り(Exit For)」で計算量を爆発させない。
- ローカル変数のスコープ管理と明示的な `Nothing` 解放を徹底する。
ここをクリアできれば、もう「マクロを実行するとProjectが固まる恐怖」におびえる必要はありません。どんなに巨大で複雑なスケジュール表が来ても、一瞬で裏側からコントロールできる頼もしいエンジニアになれます。
日々の自動化ライフを、もっとエレガントに、もっと高速に。
あなたのVBAライフが、今日からさらに素晴らしいものになることを応援しています!
