MSProject VBAを極限まで加速する:命名規則からの階層(WBS)自動構築パーサーの実装
レガシーな現場において、Excelやテキストで乱雑に管理されたタスクリストをMicrosoft Projectへ流し込む作業ほど、エンジニアの精神を摩耗させるものはない。数百行、数千行に及ぶ工程表を一つひとつ手動でインデントし、前提条件(先行タスク)を手で結んでいく作業は、自動化の恩恵を知る者にとって悪夢でしかない。
「1. 」「1.1 」「1.1.1 」といったアウトライン番号の規則性。これさえ担保されていれば、VBAのメモリ管理とオブジェクトのライフサイクルを熟知したアーキテクトの手により、一瞬で完璧なWBS(Work Breakdown Structure)を構築することが可能だ。
今回は、MSProject VBAの暗黙のオーバーヘッドを回避し、数千件のタスク群を極限の速度でパースして階層構造を自動構築するチーフアーキテクト級のエンジンを公開する。
—
1. 現場の現実:なぜ標準機能や安直なVBAでは破綻するのか
GUIを通じたインデント操作(`OutlineIndent`メソッドの乱用)は、MSProjectの内部描画エンジンとUndoスタックを暴走させる。さらに、COMオブジェクトの解放漏れ、`ActiveProject.Tasks` コレクションの無駄な走査は、タスク数が2,000を超えたあたりから激しいメモリリークとパフォーマンス低下を引き起こす。
真にスケーラブルなパーサーを実装するためには、以下の要件を満たさなければならない。
1. 文字列解析の高速化: 正規表現(RegEx)のループ内生成を避け、メモリ上で一気に判定する。
2. 階層スタックの維持: 親タスクのIDを追跡し、現在のレベルと前回レベルの差分から適切な `Outdent`/`Indent` のデルタを算出する。
3. 描画の完全抑制: 処理中の画面更新・イベント・再計算を停止し、CPUリソースをデータ構築に全振りする。
—
2. アーキテクチャ設計:正規表現エンジンとステート管理
今回実装するパーサーは、タスク名の先頭にあるドット区切りの数値(例: `2.3.1`)を解析し、その深さ(Level)を算出してMSProject上のインデントレベルへ直接マッピングする。
文字列表現からレベルを抽出するコアロジックには、VBE標準の `VBScript.RegExp` を用いるが、インスタンスの生成と破棄をループの外へ追い出すのがプロの作法である。
—
3. 実装コード:WBS自動構築パーサー
以下のコードは、MSProjectのVBAモジュールにそのまま貼り付けて実行できる完成版のエンジンである。外部テキストやExcelから配列として渡されたタスクデータを想定し、極限まで最適化された処理を行う。
‘ ==============================================================================
‘ Module: ModWBSParser
‘ Description: 命名規則ベースのWBS階層自動構築エンジン
‘ Author: Chief Architect
‘ ==============================================================================
Option Explicit
‘ Windows API: 処理速度向上のための画面描画停止(必要に応じて拡張)
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Public Sub ExecuteWBSParser()
Dim sw As Double
sw = Timer
‘ 1. パフォーマンス・安定性のための環境最適化
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
‘ 2. サンプルデータの準備(実際にはExcelやCSV、テキストファイルから読み込む)
Dim rawData() As String
rawData = GetSampleTaskData()
Dim i As Long
Dim taskName As String
Dim currentLevel As Long
Dim prevLevel As Long
Dim t As Task
prevLevel = 1
‘ 既存のタスクをクリアする場合(必要に応じてコメントアウト解除)
‘ Dim existingTask As Task
‘ For Each existingTask in ActiveProject.Tasks
‘ If Not existingTask Is Nothing Then existingTask.Delete
‘ Next existingTask
‘ 正規表現エンジンの初期化(ループ内インスタンス化の排除によるメモリ最適化)
Dim regEx As Object
Set regEx = CreateObject(“VBScript.RegExp”)
regEx.Pattern = “^\s(\d+(\.\d+))\s+(.)”
regEx.IgnoreCase = True
regEx.Global = False
‘ 3. メインループ:タスクの逐次追加とインデント調整
For i = LBound(rawData) To UBound(rawData)
taskName = rawData(i)
If Trim(taskName) <> “” Then
‘ タスクを末尾に追加
Set t = ActiveProject.Tasks.Add(Name:=taskName)
‘ レベルの算出
currentLevel = CalculateWBSLevel(taskName, regEx)
‘ インデントレベルの調整ロジック
‘ 前回のレベルと比較し、適切な相対インデントを実行する
If currentLevel > prevLevel Then
Dim d As Long
For d = 1 To (currentLevel – prevLevel)
t.OutlineIndent
Next d
ElseIf currentLevel < prevLevel Then
Dim u As Long
For u = 1 to (prevLevel - currentLevel)
t.OutlineOutdent
Next u
End If
prevLevel = currentLevel
' オブジェクト変数の明示的解放(メモリリーク防止)
Set t = Nothing
End If
NotepadNext:
Next i
' 4. 終了処理
MsgBox "WBSの構築が完了しました。処理時間: " & Format(Timer - sw, "0.00") & "秒", vbInformation, "チーフアーキテクト通知"
CleanUp:
' リソースの解放
Set regEx = Nothing
Application.ScreenUpdating = True
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "System Error"
Resume CleanUp
End Sub
' ==============================================================================
' 補助関数: タスク名からWBSの階層レベルを算出
' ==============================================================================
Private Function CalculateWBSLevel(ByVal taskName As String, ByRef regEx As Object) As Long
Dim matches As Object
If regEx.Test(taskName) Then
Set matches = regEx.Execute(taskName)
Dim wbsNumber As String
wbsNumber = matches(0).SubMatches(0)
' ドットの数から階層レベルを決定 ("1" = 1, "1.1" = 2, "1.1.1" = 3)
CalculateWBSLevel = UBound(Split(wbsNumber, ".")) + 1
Set matches = Nothing
Else
' マッチしない場合は最上位(Level 1)とする
CalculateWBSLevel = 1
End If
End Function
' ==============================================================================
' サンプルデータプロバイダ(実運用では外部I/Oに置き換えること)
' ==============================================================================
Private Function GetSampleTaskData() As String()
Dim data(9) As String
data(0) = "1. 要件定義フェーズ"
data(1) = "1.1 ヒアリング実施"
data(2) = "1.1.1 現行業務フロー分析"
data(3) = "1.1.2 課題の整理"
data(4) = "1.2 要件定義書作成"
data(5) = "2. 設計フェーズ"
data(6) = "2.1 基本設計"
data(7) = "2.1.1 画面レイアウト設計"
data(8) = "2.1.2 DBスキーマ設計"
data(9) = "2.2 詳細設計"
GetSampleTaskData = data
End Function
---
4. チーフアーキテクトの視点:コードに宿る「守り」と「攻め」
上記のコードには、レガシーシステムや大規模エンタープライズ環境で生き残るための知見が凝縮されている。
1. ループ内インスタンス化の完全排除
初心者によくある過ちは、ループの中で `CreateObject(“VBScript.RegExp”)` を実行することだ。COMコンポーネントの生成と破棄は、VBAにおいて莫大なオーバヘッドを生む。数千回のループで行えば、それだけで数秒の無駄な遅延が発生する。オブジェクトはループ外で1度だけ生成し、参照を使い回すのが鉄則だ。
2. COMオブジェクトのスコープと明示的解放 (`Set t = Nothing`)
VBAのガベージコレクションは非力である。特にMSProjectの `Task` オブジェクトのような重厚なCOMラッパーは、参照がスタックに残ったままループが回ると、メモリ空間を圧迫し続ける。毎ループの最後に `Set t = Nothing` を明示することで、メモリフットプリントを極限まで小さく抑え込んでいる。
3. `ScreenUpdating = False` による描画バイパス
MSProjectは、タスクが1行追加されるたびにガントチャートの再描画とWBSコードの再計算をバックグラウンドで走らせる。これを抑制せずに大量追加を行うと、O(N^2)に近いパフォーマンス劣化を引き起こす。画面描画を停止することで、純粋なメモリ上のデータ構造構築に集中させ、爆発的な高速化を実現している。
—
5. さらなる高みへ:システム間連携と保守性
このパーサーを実務の基幹システムへ組み込む場合、入力元はハードコーディングされた配列ではなく、DBからのSQLフェッチ結果 や REST APIから返却されるJSONストリーム になる。
VB.NETやC#で同様のロジックを組むのも一つの手だが、既存のMicrosoft Projectアドイン環境や、社内に散在する「VBA職人」がメンテするレガシーサイロにおいては、このVBAによるカプセル化されたモジュールこそが、最もコストパフォーマンスの高いインフラストラクチャとなる。
コードの意図を汲み取り、適切な例外処理とロギングを付加すれば、あなたの管理するプロジェクト計画の初動は、他の追随を許さないほどの圧倒的なスピードと正確性を手に入れるはずだ。
