【実務中級】Task.OutlineChildrenを用いたWBSの再帰的走査:階層構造をCSVへ書き出すツール
Microsoft Project(MS Project)を用いた大規模プロジェクトの管理において、最も頻繁に直面する課題の一つが「深部までネストされたWBS(Work Breakdown Structure)の外部システム連携」だ。
基幹システム、BIツール、あるいは単なる進捗管理用のデータレイク。これらにMS Projectのデータを渡す際、階層構造(Tree構造)を保ったままのバイナリや複雑なオブジェクトを渡すことはできない。求められるのは常に、「親・子・孫の関係性をフラットに紐付けた正規化されたリスト(CSV)」である。
今回は、Project VBAにおけるオブジェクトモデルの深淵に踏り込み、`Task.OutlineChildren` コレクションを用いた再帰的走査アルゴリズムの極限を解説する。単に動くだけのコードではない。COMコンポーネントのメモリ管理、大規模プロジェクトにおけるパフォーマンス最適化、そして実務で必ず直面する罠を回避するための実践的知見を共有する。
—
1. Project VBAにおけるオブジェクトモデルの罠
多くのVBAエンジニアは、ExcelやAccessの作法をそのままMS Projectに持ち込み、見事に撃沈する。MS Projectのオブジェクトモデルは、Excelの `Range` や `Worksheet` とは異なり、遅延バインディングとCOMの参照カウンタの挙動が極めてシビアな世界だ。
再帰処理におけるメモリリークの温床
`Task.OutlineChildren` は、呼び出されるたびに新しいコレクションオブジェクト(COMラッパー)をヒープ上に生成する。これを適切に解放せず、`Set` の連鎖や不要な変数保持を行うと、数千行規模のWBSを走査した瞬間にVBAのメモリ空間が肥大化し、最悪の場合ExcelやProjectプロセスそのものがクラッシュする。
シニアエンジニアたる者、「取得したオブジェクトは速やかに解放する(`Set obj = Nothing`)」という鉄則を、再帰関数の内部においても厳格に適用しなければならない。
—
2. 実装アーキテクチャ:再帰的WBS走査エンジン
以下に、実務の現場で即座に稼働させられる、堅牢かつ高速なWBSフラット化・CSV出力エンジンの完全なコードを示す。
このコードは、単にタスクを舐めるだけでなく、階層レベル(OutlineLevel)、一意のID(UniqueID)、先行タスク(Predecessors)などの実務上必須となるメタデータを同時に抽出し、文字コード(UTF-8 with BOM)を担保したストリームで出力する。
Option Explicit
‘ メモリ最適化とファイルストリーム操作のためのAPI定義(必要に応じて拡張)
Private Const UTF8_BOM As String = “utf8”
Public Sub ExportWBSToCSV()
Dim prj As Project
Set prj = ActiveProject
‘ プロジェクト全体にタスクが存在するか事前検証
If prj.Tasks.Count = 0 Then
MsgBox “エクスポート対象のタスクが存在しません。”, vbExclamation, “WBS Exporter”
Exit Sub
End If
Dim filePath As String
filePath = GetSaveCSVPath()
If filePath = “” Then Exit Sub ‘ キャンセル時
‘ FileSystemObjectを用いた高速ストリーム書き込みの準備
Dim fso As Object
Dim ts As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 文字化け防止のためADODB.Streamを用いてUTF-8 (BOM付き) で書き込むのがプロの作法
Dim stream As Object
Set stream = CreateObject(“ADODB.Stream”)
stream.Type = 2 ‘ adTypeText
stream.Charset = “UTF-8”
stream.Open
‘ CSVヘッダーの書き込み
stream.WriteText “UniqueID,ID,Name,OutlineLevel,Start,Finish,Duration,Cost,ResourceNames,Predecessors” & vbCrLf
‘ 処理時間の計測開始
Dim startTime As Double
startTime = Timer
‘ 画面描画とイベントを停止し、処理速度を極限まで引き上げる
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
‘ ルートレベルのタスク群(OutlineChildrenを持たない最上位の親)を起点とする
‘ ※ Projectのタスクは OutlineLevel = 1 のみがルートとは限らないため、
‘ prj.Tasks 全体を走査するか、OutlineChildrenの親を起点にする。
Dim t As Task
For Each t in prj.Tasks
If Not t Is Nothing Then
‘ 親を持たない(あるいは最上位の)タスクを起点に再帰処理へ突入
‘ MS Projectのデータ構造上、OutlineLevel 1 または親IDを持たないものをトップとする
If t.OutlineLevel = 1 Then
Call TraverseTasks(t, stream)
End If
End If
Next t
‘ ストリームをファイルに保存
stream.SaveToFile filePath, 2 ‘ adSaveCreateOverWrite
stream.Close
Application.ScreenUpdating = True
MsgBox “WBSのエクスポートが完了しました。” & vbCrLf & _
“実行時間: ” & Format(Timer – startTime, “0.00”) & “秒” & vbCrLf & _
“保存先: ” & filePath, vbInformation, “完了”
CleanUp:
‘ 徹底的なオブジェクトの解放
On Error Resume Next
If Not stream Is Nothing Then stream.Close
Set stream = Nothing
Set fso = Nothing
Set prj = Nothing
Exit Sub
ErrorHandler:
Application.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error ” & Err.Number & “: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub
『
‘ —————————————————————–
‘ @Sub: TraverseTasks
‘ @Description: Task.OutlineChildrenを再帰的に走査し、フラットなCSV行を生成する
‘ @Param: currentTask – 現在処理中のタスクオブジェクト
‘ @Param: stream – 出力先のADODB.Streamオブジェクト
‘ —————————————————————–
Private Sub TraverseTasks(ByVal currentTask As Task, ByRef stream As Object)
If currentTask Is Nothing Then Exit Sub
Dim csvLine As String
Dim resNames As String
‘ リソース名が未割当の場合のエラータスク対策
On Error Resume Next
resNames = currentTask.ResourceNames
If Err.Number <> 0 Then resNames = “”
On Error GoTo 0
‘ CSV行の組み立て(ダブルクォーテーションによるエスケープ処理を内包)
csvLine = currentTask.UniqueID & “,” & _
currentTask.ID & “,” & _
EscapeCSV(currentTask.Name) & “,” & _
currentTask.OutlineLevel & “,” & _
currentTask.Start & “,” & _
currentTask.Finish & “,” & _
currentTask.Duration / 480 & “,” & _ ‘ 分単位から日単位(1日=480分)へ正規化
currentTask.Cost & “,” & _
EscapeCSV(resNames) & “,” & _
EscapeCSV(currentTask.Predecessors)
‘ ストリームへ書き込み
stream.WriteText csvLine & vbCrLf
‘ 子タスクが存在するか?
‘ OutlineChildrenコレクションの存在チェック
Dim children As Tasks
Set children = currentTask.OutlineChildren
If Not children Is Nothing Then
If children.Count > 0 Then
Dim childTask As Task
For Each childTask In children
‘ 再帰呼び出し
Call TraverseTasks(childTask, stream)
Next childTask
End If
End If
‘ 【重要】COMオブジェクトの連鎖的リークを防ぐため、明示的に解放
Set children = Nothing
End Sub
‘ —————————————————————–
‘ @Function: EscapeCSV
‘ @Description: CSVのカンマやダブルクォーテーションによる構造破壊を防ぐ
‘ —————————————————————–
Private Function EscapeCSV(ByVal target As String) As String
If InStr(target, “,”) > 0 Or InStr(target, “”””) > 0 Or InStr(target, vbLf) > 0 Then
EscapeCSV = “””” & Replace(target, “”””, “”””””) & “”””
Else
EscapeCSV = target
End If
End Function
‘ —————————————————————–
‘ @Function: GetSaveCSVPath
‘ @Description: 保存ダイアログを表示し、パスを取得する
‘ —————————————————————–
Private Function GetSaveCSVPath() As String
Dim fd As FileDialog
Set fd = Application.FileDialog(msoFileDialogSaveAs)
fd.Title = “WBS CSVエクスポート”
fd.FilterIndex = 1
fd.Filters.Clear
fd.Filters.Add “CSVファイル”, “.csv”
If fd.Show = -1 Then
GetSaveCSVPath = fd.SelectedItems(1)
Else
GetSaveCSVPath = “”
End If
Set fd = Nothing
End Function
—
3. チーフアーキテクトが教える、現場の「地雷」と回避策
このコードを実務の巨大プロジェクト(タスク数10,000超)に投入した際、遭遇し得るトラブルと、その対策をあらかじめ伝授する。
① ユニットタイムの罠(Durationの単位)
MS Projectの内部データにおいて、`Task.Duration` は「分(Minutes)」で保持されている。これをそのままBIやExcelに渡すと、人間が直感的に理解できる「日(Days)」と乖離して大混乱を招く。
コード内にある `currentTask.Duration / 480` は、1日を8時間(480分)として換算している標準的なロジックだが、プロジェクトの設定(カレンダーの稼働時間)によっては変更が必要になる点に留意せよ。
② 再帰呼び出しの深さ制限(スタックオーバーフロー)
VBAの再帰呼び出しにはコールスタックの制限がある。理論上、数千階層のネストがあればスタックオーバーフローを引き起こすが、実務上のWBSで深さが50階層を超えることはまずない。
もし組織の標準WBSが異常に深い構造を持つ場合は、再帰ではなく、`OutlineChildren` を用いたスタック(配列またはCollection)ベースの非再帰的(ループ)走査アルゴリズムへとリファクタリングする勇気を持て。
③ 文字コードの近代化(Shift-JISの呪縛からの脱却)
レガシーなVBAコードの多くは未だに `Open … For Output As #1` を使い、Shift-JIS(CP932)でCSVを出力している。これでは、タスク名に外字や環境依存文字、あるいは絵文字が含まれた瞬間に文字化け、あるいは書き込みエラーでシステムが停止する。
上記のコードであえて `ADODB.Stream` を採用し、`UTF-8 (BOM付き)` で出力しているのは、現代のWeb系APIやクラウド型BIツール(Power BI等)とのインテグレーションを完全に担保するためである。
—
結びにかえて
VBAは「古い言語」ではない。システムの中枢にあるデスクトップアプリケーション(MS Project)のメモリ空間に直接アクセスし、ミリ秒単位でオブジェクトを制御できる唯一無二のアーキテクチャだ。
今回解説した `Task.OutlineChildren` の再帰的走査とメモリ管理の思想は、Project VBAに限らず、あらゆるCOMオートメーションの設計に応用できる。退屈な定型業務を自動化する玩具としてではなく、エンタープライズシステムのピースとしてVBAを極めよ。あなたの書くコードが、プロジェクトの運命を左右する。
