【テクニカル・上級編】タスクの「名前」から自動的に階層を判定し、インデントを自動適用するパーサーの実装 – Project VBA解析バイブル

スポンサーリンク

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によるカプセル化されたモジュールこそが、最もコストパフォーマンスの高いインフラストラクチャとなる。

コードの意図を汲み取り、適切な例外処理とロギングを付加すれば、あなたの管理するプロジェクト計画の初動は、他の追随を許さないほどの圧倒的なスピードと正確性を手に入れるはずだ。

タイトルとURLをコピーしました