【実務・中級編】【中級者向け】タスクの「インデントレベル」を判定して、WBS番号を自動採番する汎用パーサー – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見:タスクの「インデントレベル」を判定し、WBS番号を自動採番する汎用パーサー

はじめに:なぜ、今WBSの自動化に命をかけるのか?

諸君、私はプロジェクト管理の現場で幾度となく、手作業によるWBS(Work Breakdown Structure)番号の採番と、それに伴う地獄を見てきた。タスクの追加、削除、移動があるたびに、Excelとの二重管理、コピペミス、そして何よりも「時間の無駄」が膨大な損失を生み出す。Projectの標準機能でWBS番号は生成される。だが、それでは飽き足らない。柔軟性がない。外部システムとの連携が難しい。だからこそ、我々はProject VBAという強力な武器を手に、この非効率の根源を断ち切らねばならない。

私がここで語るのは、単なるリファレンスの引き写しではない。オブジェクトのライフサイクル、メモリフットプリント、そしてパフォーマンスの重みを骨の髄まで知り尽くした者だけが到達できる、極限の知見だ。プロジェクトの構造を正確に、そして瞬時に可視化するWBS番号の自動採番。これを実現する汎用パーサーの設計と実装について、その魂を込めて伝授しよう。

本質を理解する:ProjectのWBSと`OutlineLevel`の絶対的真実

まず、WBS番号自動採番の根幹をなすProjectの構造理解から始めよう。
Projectのタスク階層は、インデントレベルによって厳密に定義される。VBAにおいては、このレベルは`Task`オブジェクトの`OutlineLevel`プロパティとして表現される。

  • `OutlineLevel = 1`:最上位のタスク(多くの場合、プロジェクトそのものか、主要フェーズ)
  • `OutlineLevel = 2`:`OutlineLevel = 1`のタスクのサブタスク
  • 以下、同様に続く。

この`OutlineLevel`こそが、WBS番号「1.1」「1.2.1」といった階層的なナンバリングを生成する際の、唯一にして絶対的な真実である。一般的なプロジェクトでは最大9階層程度だが、その制約すらもコードでハンドリングできる設計を目指す。

既存のWBSフィールドとカスタムフィールドの使い分け:愚かさを避ける賢明な選択

Projectには既に「WBS」という標準フィールドが存在する。しかし、これを直接利用しようと考えるのは愚策だ。

標準WBSフィールドの限界

1. 変更不可: 標準WBSフィールドはProjectが自動的に管理するため、VBAから直接その値を変更することはできない。読み取り専用だ。
2. 柔軟性の欠如: ナンバリング形式が固定されており、プロジェクト固有のルール(例:「A-1.1」「PJT-1.1.1」のようなプレフィックス付与)に対応できない。
3. 他の用途への転用不可: 純粋にWBS番号しか格納できず、将来的な拡張性がない。

カスタムフィールドの絶対的優位性

我々が目指すべきは、カスタムフィールド(具体的には「テキスト型カスタムフィールド」)の活用だ。

1. 完全な制御: VBAから自由に値を読み書きできる。
2. 無限の柔軟性: 任意のフォーマットでWBS番号を生成し、格納できる。プレフィックス、サフィックス、区切り文字など、思いのままだ。
3. 拡張性: 将来的に、このWBS番号をキーとして外部システム(データベース、ERPなど)と連携させる際のIDとして利用できる。これは、単なるWBS番号を超えた、戦略的な意味を持つ。

したがって、我々は「テキスト型カスタムフィールド」にWBS番号を自動採番する。これが、堅牢で拡張性のあるソリューションを構築するための第一歩だ。

汎用パーサー設計の要諦:堅牢性、パフォーマンス、保守性のトライアングル

優れたコードとは、単に動くものではない。未来の変更に耐え、あらゆる異常事態を想定し、高速に動作し、そして何よりも読みやすいものである。

堅牢性:あらゆる「もしも」に備える

  • 空行・無効なタスクの無視: プロジェクトデータには、しばしば空の行や、無効なタスクが混入していることがある。これらを適切にスキップし、エラーを発生させない設計が必須だ。`Task.Name`が空かどうか、`Task.ID`が有効かなどを確認する。
  • サマリータスクとサブタスクの関係: `OutlineLevel`を基にWBSを生成する際、サマリータスク(Summary Task)とそのサブタスク(Subtask)の関係性を正確に理解し、WBS番号が論理的に連鎖するよう設計する。サマリータスク自体にもWBS番号を付与するのが一般的だ。
  • エラーハンドリング: 予期せぬエラー(例:Projectアプリケーションが開かれていない、カスタムフィールドが見つからない)が発生した場合に、処理が中断することなく、ユーザーに適切なフィードバックを提供し、安全に終了する。`On Error GoTo`は不可欠だ。
  • 再実行可能性(冪等性): 何度実行しても同じ結果が得られること。既存のWBS番号を上書きする設計にするため、これは自然と満たされるが、意識しておくべき重要な原則だ。

パフォーマンス:巨大プロジェクトでも息切れしないために

  • DOM操作の最小化: Project VBAは、UIを介したオブジェクト操作をシミュレートするため、描画処理などが挟まると極端に遅くなる。
  • `Application.ScreenUpdating = False`: 画面描画を停止し、処理速度を劇的に向上させる。
  • `Application.EnableEvents = False`: Projectのイベント発火を一時的に停止し、予期せぬ挙動や処理のオーバーヘッドを防ぐ。
  • `Application.Calculation = pjManual`: (必要であれば)計算モードを手動に設定し、タスクプロパティ変更時の自動再計算を抑制する。
  • オブジェクトへのアクセス回数の削減: ループ内で`Task`オブジェクトのプロパティ(例:`Task.OutlineLevel`)を何度も参照するのではなく、一度変数に格納してから利用する。
  • 配列によるWBSレベル管理: WBS番号の各レベル(例:1.2.3の「1」「2」「3」)を管理するために、固定長または動的配列を利用する。これにより、文字列操作を最小限に抑え、数値演算で効率的にレベルを更新できる。

保守性:未来の自分、未来のチームのために

  • 定数とマジックナンバーの排除: カスタムフィールド名、区切り文字などの固定値は、必ず定数として定義する。これにより、変更があった際に一箇所修正するだけで済む。
  • コメントの適切な配置: 「何をしているか」だけでなく、「なぜそうしているか」を明確にするコメントを記述する。特に、パフォーマンス最適化やエラーハンドリングの意図は詳しく記述すべきだ。
  • 関数・プロシージャの分割: コードの塊を論理的な単位で分割し、責任を明確にする。これにより、コードの再利用性が高まり、デバッグが容易になる。

実装フェーズ:極限のコード設計

それでは、これらの設計思想を具体的なコードへと落とし込もう。
まずは準備として、Projectファイルに「WBS Custom」という名前のテキスト型カスタムフィールドを作成しておく必要がある。これは手動で行っても良いが、VBAで自動的に作成することも可能だ。ここでは手動での作成を前提とする。

前提:カスタムフィールドの作成

1. Projectリボンから「プロジェクト」タブを選択。
2. 「プロパティ」グループの「カスタムフィールド」をクリック。
3. 「タスク」を選択し、「種類」を「テキスト」にする。
4. 「新しいフィールド」ボタンをクリックし、フィールド名に「WBS Custom」と入力してOK。
5. 必要であれば、表示する列に「WBS Custom」を追加しておく。

コアロジック:WBS番号生成アルゴリズムの真髄

`OutlineLevel`を基にしたWBS番号の生成は、配列を使って各階層の番号を管理するのが最も効率的だ。

Option Explicit

‘

‘# Project VBA チーフアーキテクトによるWBS自動採番汎用パーサー

‘# 目的: Projectのタスク階層(OutlineLevel)に基づき、堅牢かつ高性能なWBS番号を
‘# 指定されたカスタムテキストフィールドに自動採番する。
‘# 設計思想:
‘# 1. 堅牢性: エラーハンドリング、空タスクのスキップ、再実行可能性を確保。
‘# 2. パフォーマンス: 画面更新停止、イベント停止、オブジェクトアクセス最小化。
‘# 3. 保守性: 定数利用、コメント充実、論理的分割。
‘# 4. 柔軟性: カスタムフィールド利用により、WBSフォーマットの自由度を最大化。
‘

‘ — 定数定義 —

Private Const CUSTOM_WBS_FIELD_NAME As String = “WBS Custom” ‘ WBS番号を書き込むカスタムフィールド名
Private Const WBS_SEPARATOR As String = “.” ‘ WBS番号の区切り文字
Private Const MAX_WBS_LEVEL As Long = 9 ‘ 想定されるWBSの最大階層レベル (Projectの標準は9階層まで)

‘ — グローバル変数 (パフォーマンス最適化のため、フィールドIDを一度取得したら保持) —
Private customWBSFieldID As Long

‘

‘# メインプロシージャ: カスタムWBS番号の自動採番を実行する

‘# 処理概要:
‘# 1. 環境設定(画面更新、イベント、計算モードの一時停止)
‘# 2. カスタムフィールドIDの取得
‘# 3. 全タスクを走査し、WBS番号を生成・設定
‘# 4. 環境設定の復元
‘# 5. ユーザーへの完了通知
‘

Public Sub GenerateCustomWBS()

Dim prj As Project
Dim tsk As Task
Dim wbsLevels(1 To MAX_WBS_LEVEL) As Long ‘ 各階層のWBS番号を管理する配列
Dim currentWbsString As String
Dim i As Long
Dim initialScreenUpdating As Boolean
Dim initialEnableEvents As Boolean
Dim initialCalculationMode As PjCalculation

‘ — 1. 環境設定の一時停止(パフォーマンス最適化) —
‘ 現在の状態を保存し、処理後に復元する
initialScreenUpdating = Application.ScreenUpdating
initialEnableEvents = Application.EnableEvents
initialCalculationMode = Application.Calculation

Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = pjManual ‘ 自動計算を停止し、処理速度を向上させる

On Error GoTo ErrorHandler ‘ エラー発生時の処理を指定

‘ 現在開いているプロジェクトを取得
Set prj = ActiveProject

‘ — 2. カスタムフィールドIDの取得 —
‘ フィールド名をFieldConstantに変換する。これにより、ハードコーディングを避け、
‘ かつTask.SetFieldメソッドでの参照を高速化する。
‘ ※一度取得したらグローバル変数に保持することで、複数回呼び出し時のパフォーマンスを向上させる
If customWBSFieldID = 0 Then
customWBSFieldID = GetCustomFieldID(CUSTOM_WBS_FIELD_NAME)
End If

‘ フィールドIDが取得できなかった場合、カスタムフィールドが存在しない
If customWBSFieldID = 0 Then
MsgBox “指定されたカスタムフィールド ‘” & CUSTOM_WBS_FIELD_NAME & “‘ が見つかりません。” & vbCrLf & _
“プロジェクトファイルにこのカスタムフィールドが作成されているか確認してください。”, vbCritical
GoTo CleanExit
End If

‘ — WBSレベル配列の初期化 —
For i = 1 To MAX_WBS_LEVEL
wbsLevels(i) = 0
Next i

‘ — 3. 全タスクを走査し、WBS番号を生成・設定 —
For Each tsk In prj.Tasks
‘ 無効なタスクや空の行はスキップ(堅牢性向上)
If Not tsk Is Nothing And Not tsk.Name = “” Then
Dim currentOutlineLevel As Long
currentOutlineLevel = tsk.OutlineLevel

‘ WBSレベルが想定範囲外の場合、エラーを避けるためスキップまたは警告
If currentOutlineLevel < 1 Or currentOutlineLevel > MAX_WBS_LEVEL Then
Debug.Print “警告: タスク ‘” & tsk.Name & “‘ (ID: ” & tsk.ID & “) のOutlineLevelが想定範囲外です (” & currentOutlineLevel & “)”
‘ このタスクのWBS番号は生成せず、次のタスクへ進む
GoTo NextTask
End If

‘ 現在の階層レベルの番号をインクリメント
wbsLevels(currentOutlineLevel) = wbsLevels(currentOutlineLevel) + 1

‘ 現在の階層より深い階層の番号をリセット(WBSのルール)
For i = currentOutlineLevel + 1 To MAX_WBS_LEVEL
wbsLevels(i) = 0
Next i

‘ WBS番号文字列の構築
currentWbsString = “”
For i = 1 To currentOutlineLevel
‘ WBS番号の最初の部分は区切り文字をつけない
If i = 1 Then
currentWbsString = wbsLevels(i)
Else
currentWbsString = currentWbsString & WBS_SEPARATOR & wbsLevels(i)
End If
Next i

‘ 生成したWBS番号をカスタムフィールドに設定
‘ Task.SetFieldメソッドは、Task.TextXプロパティへの直接アクセスよりも柔軟だが、
‘ パフォーマンスを重視する場合は Task.TextX = currentWbsString も検討できる。
‘ しかし、カスタムフィールドのID変換を含めSetFieldは比較的効率的であり、
‘ 可読性と堅牢性(フィールドタイプチェックなど)の面で優位であるため、ここではSetFieldを採用する。
tsk.SetField FieldID:=customWBSFieldID, Value:=currentWbsString
End If
NextTask:
Next tsk

‘ — 4. 環境設定の復元 —
CleanExit:
Application.ScreenUpdating = initialScreenUpdating
Application.EnableEvents = initialEnableEvents
Application.Calculation = initialCalculationMode

‘ — 5. ユーザーへの完了通知 —
MsgBox “WBSカスタムフィールドの自動採番が完了しました。”, vbInformation

Exit Sub

ErrorHandler:
MsgBox “WBS自動採番中にエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical
GoTo CleanExit ‘ エラー発生時もクリーンアップ処理を実行

End Sub

‘

‘# ヘルパー関数: カスタムフィールド名からFieldConstant IDを取得する

‘# 引数:
‘# fieldName (String): 取得したいカスタムフィールドの名前
‘# 戻り値:
‘# Long: FieldConstant ID。見つからない場合は0を返す。
‘# 目的: フィールド名をハードコードせず、動的にフィールドIDを取得することで保守性を高める。
‘

Private Function GetCustomFieldID(fieldName As String) As Long

On Error Resume Next ‘ エラー発生時も処理を続行し、戻り値で判定
GetCustomFieldID = Application.CustomFieldNameToFieldConstant(fieldName)
If Err.Number <> 0 Then
GetCustomFieldID = 0 ‘ フィールドが見つからない場合は0を返す
Err.Clear
End If
On Error GoTo 0 ‘ エラーハンドリングをデフォルトに戻す
End Function

コード解説:極限の知見を紐解く

1. 定数とグローバル変数:

  • `CUSTOM_WBS_FIELD_NAME`, `WBS_SEPARATOR`, `MAX_WBS_LEVEL` を定数化。これにより、将来的な変更(例:区切り文字をハイフンに変更)が容易になる。
  • `customWBSFieldID` をグローバル変数として定義。`GetCustomFieldID` 関数は`Application.CustomFieldNameToFieldConstant`を内部で呼び出すが、このメソッドは若干のオーバーヘッドがある。一度取得したIDを保持することで、もし`GenerateCustomWBS`が複数回呼び出されるようなシナリオでも、フィールドIDの再取得を避けてパフォーマンスを向上させる。

2. パフォーマンス最適化の徹底:

  • `Application.ScreenUpdating = False`:画面描画を停止。これが最も効果的なパフォーマンス改善策の一つだ。
  • `Application.EnableEvents = False`:Projectのイベント(タスク変更時の再計算、リンク更新など)を停止。大規模プロジェクトでこれが有効な場合がある。
  • `Application.Calculation = pjManual`:計算モードを手動に。タスクプロパティの変更による自動再計算を防ぐ。
  • これらの設定は、プロシージャの最後に`GoTo CleanExit`で必ず元の状態に復元されるように設計している。これにより、VBA実行後のProjectの挙動が正常に保たれる。

3. WBS番号生成ロジック:

  • `wbsLevels(1 To MAX_WBS_LEVEL) As Long`:各階層の番号を保持する配列。`OutlineLevel`をインデックスとして直接利用できるため、非常に効率的だ。
  • `currentOutlineLevel = tsk.OutlineLevel`:タスクの階層レベルを取得。オブジェクトプロパティへのアクセスは一度に留める。
  • `wbsLevels(currentOutlineLevel) = wbsLevels(currentOutlineLevel) + 1`:現在の階層の番号をインクリメント。
  • `For i = currentOutlineLevel + 1 To MAX_WBS_LEVEL: wbsLevels(i) = 0: Next i`:ここが重要だ。より深い階層の番号をリセットする。これにより、「1.1」の後に「1.2」が来て、その下に「1.2.1」が続く、というWBSの正しい論理を担保する。
  • WBS文字列の構築は、`For`ループで`currentWbsString`に連結していく。`Join`関数を使うよりも、レベルが少ない場合は直接連結する方がオーバーヘッドが小さい。

4. 堅牢性への配慮:

  • `If Not tsk Is Nothing And Not tsk.Name = “” Then`:空のタスクや無効なタスクオブジェクトをスキップ。ユーザーが誤って挿入した空行などによるエラーを防ぐ。
  • `If currentOutlineLevel < 1 Or currentOutlineLevel > MAX_WBS_LEVEL Then`:`OutlineLevel`が想定外の値だった場合の防御。デバッグ情報として出力し、処理を続行する。
  • `On Error GoTo ErrorHandler`:汎用的なエラーハンドリング。どのようなエラーが発生しても、VBAがクラッシュするのではなく、ユーザーに通知し、`CleanExit`ルーチンで環境設定を元に戻して終了する。

5. カスタムフィールドIDの動的取得:

  • `GetCustomFieldID`関数は、カスタムフィールド名を引数に取り、その`FieldConstant` IDを返す。これにより、コード内に`pjTaskText1`のようなマジックナンバーを直接記述することを避け、カスタムフィールドの割り当てが変更されてもコードの変更が不要になる。`Application.CustomFieldNameToFieldConstant`の利用は、Project VBAを深く理解している証だ。

拡張性と発展:戦略的優位性への道

このWBS自動採番パーサーは、単なる数値の付与に終わらない。

  • 他のカスタムフィールドとの連携: このWBS番号を「外部ID」として利用し、他のカスタムフィールド(例:担当者コード、部門コード、予算コード)と組み合わせて、よりリッチなプロジェクト管理を実現できる。
  • 進捗管理・予算管理への応用: WBS番号をキーとした外部データベースやスプレッドシートとのデータ連携により、Projectのデータを集約・分析する基盤を構築できる。これにより、プロジェクトの進捗や予算実績をリアルタイムで可視化し、経営層への報告も容易になる。
  • バージョン管理とデプロイメント: このVBAコードは、Projectファイル(.mptまたは.mpp)に直接埋め込むこともできるが、エンタープライズ環境では、テンプレートファイル(.mpt)として管理し、全ユーザーに配布するのが望ましい。Gitなどのバージョン管理システムでコードを管理し、CI/CDプロセスに組み込むことで、高品質なコードベースを維持できる。

よくある罠と回避策:愚か者の道は歩まない

1. 標準WBSフィールドへの書き込み試行: 初心者が陥りがちな罠だ。`Task.WBS = “1.1.1”`のようなコードはコンパイルエラーになるか、実行時エラーになる。標準フィールドは読み取り専用である。常にカスタムフィールドを使え。
2. `OutlineLevel`の理解不足: サマリータスクの子タスクが常に`OutlineLevel + 1`であるという原則を忘れるな。再帰処理を使わず、配列ベースの反復処理でWBS番号を管理することが、大規模プロジェクトでのスタックオーバーフローを防ぎ、パフォーマンスを向上させる。
3. 大規模プロジェクトでのパフォーマンス劣化: `Application.ScreenUpdating = False`などを怠ると、数百、数千のタスクを持つプロジェクトでは処理に数分かかることもある。徹底的なパフォーマンスチューニングは必須だ。UIスレッドをブロックするような処理は極力避ける。

終わりに:自動化がもたらす戦略的優位性

諸君、Project VBAを駆使してWBS番号の自動採番を実現することは、単なる作業効率化ではない。それは、プロジェクトの構造を常に最新かつ正確に保ち、プロジェクトメンバー間の共通理解を促進し、さらには外部システム連携の基盤を築く、戦略的な一手なのだ。

この極限の知見を手に、君たちのプロジェクトが非効率の鎖から解き放たれ、本来あるべき創造的な活動に集中できることを心から願う。このコードは単なるテンプレートではない。君たちの手でさらに磨き上げ、進化させ、真の業務自動化の伝説を築き上げてもらいたい。私は常に、君たちの挑戦を支持する。

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