Project VBAの真髄に迫る:WBS番号自動採番でタスク階層を「見える化」する汎用パーサー
こんにちは!Project VBAの世界へようこそ。
「マクロの記録」から一歩踏み出し、もっとパワフルにProject VBAを使いこなしたい!そう思っているあなたへ。今回は、実務で必ず役立つ「タスクのインデントレベルを判定して、WBS番号を自動採番する汎用パーサー」という、ちょっと高度だけれど、マスターすればProject VBAの基本がグッと掴めるテーマに挑戦しましょう。
このパーサーを使いこなせば、例えば「1.1.1」のようなWBS番号がカスタムフィールドに自動で書き出され、プロジェクトの階層構造が驚くほど分かりやすくなります。まるで、プロジェクトの地図が自動で描かれるような感覚ですよ!
なぜWBS番号の自動採番が重要なのか?
プロジェクト管理において、タスクの階層構造を明確にすることは、進捗管理、リソース配分、そしてコミュニケーションの円滑化のために不可欠です。しかし、手作業でWBS番号を管理するのは、タスクが増えるにつれて非常に手間がかかり、ミスも発生しやすくなります。
この自動採番パーサーがあれば、
- 階層構造の視覚化: インデントレベルから自動的にWBS番号が生成されるため、タスクの親子関係が一目で理解できます。
- 作業効率の向上: 手作業による番号付けの時間を削減し、より本質的なプロジェクト管理業務に集中できます。
- ミスの削減: 手動での番号付けに伴う誤りや重複を防ぎ、データの一貫性を保ちます。
さあ、この魔法のようなツールを一緒に作り上げていきましょう!
準備:Project VBAの基本をおさらい
コードに入る前に、Project VBAでよく使うオブジェクトについて、その「ライフサイクル」と「パフォーマンスの重み」を意識しながら、少しだけ深く掘り下げてみましょう。
1. `Application` オブジェクト
Project VBAのすべての操作の「起点」となるのが `Application` オブジェクトです。これは、Excelの`Application`オブジェクトと同じように、Project VBAアプリケーションそのものを表します。
- ライフサイクル: プロジェクトを開いている間、常に存在します。
- パフォーマンス: 多くの操作の起点となるため、直接的なパフォーマンスへの影響は小さいですが、`Application.ActiveProject` や `Application.ActiveTask` など、現在のコンテキストを取得する際に多用します。
2. `Project` オブジェクト
現在開いているプロジェクトを表します。WBS番号の採番は、この `Project` オブジェクト内のタスクを操作することになります。
- ライフサイクル: プロジェクトが開かれている間、存在します。
- パフォーマンス: プロジェクト内のタスク数が増えると、`Project` オブジェクトを介した処理(例: 全タスクのループ)は、パフォーマンスに影響を与える可能性があります。効率的なコードを心がけることが重要です。
3. `Task` オブジェクト
プロジェクト内の個々のタスクを表します。WBS番号を生成するためには、各タスクの `OutlineLevel`(インデントレベル)と `OutlineNumber`(アウトライン番号)を操作することになります。
- ライフサイクル: プロジェクト内に存在し、タスクが追加・削除されるたびにその数は変動します。
- パフォーマンス:
- `OutlineLevel`: タスクの階層構造を示すプロパティで、非常に軽量です。
- `OutlineNumber`: WBS番号そのものを表すプロパティです。このプロパティを直接変更すると、Project VBAが自動的に親タスクや子タスクの `OutlineNumber` を調整してくれるため、非常に強力です。ただし、大量のタスクの `OutlineNumber` を一度に変更しようとすると、Project VBA内部での再計算に時間がかかり、パフォーマンスが低下する可能性があります。
4. `CustomField` オブジェクト
WBS番号を格納するためのカスタムフィールドです。今回は、このカスタムフィールドにWBS番号を書き込む処理を行います。
- ライフサイクル: プロジェクトの定義(テンプレートなど)に紐づく場合と、プロジェクト自体に固有の場合があります。
- パフォーマンス: カスタムフィールドへの値の書き込み自体は、それほど重い処理ではありません。ただし、多数のカスタムフィールドを同時に操作したり、複雑な計算結果を書き込んだりする場合は、パフォーマンスへの影響を考慮する必要があります。
コードの解説:汎用WBS番号自動採番パーサー
それでは、いよいよコードを見ていきましょう。今回は、VBAで記述します。Project VBAのVBAエディタを開き、標準モジュールに以下のコードを貼り付けてください。
Option Explicit
‘==============================================================================
‘ モジュール名: WBSNumberGenerator
‘ 概要: Project VBAのタスクのインデントレベルに基づいてWBS番号を自動採番し、
‘ 指定されたカスタムフィールドに書き出す汎用的なパーサー。
‘ 著者: [あなたの名前]
‘ 作成日: [今日の日付]
‘==============================================================================
Sub GenerateWBSNumbers()
‘==========================================================================
‘ 定数定義
‘==========================================================================
Const CUSTOM_FIELD_NAME As String = “WBS番号” ‘ WBS番号を格納するカスタムフィールド名
‘==========================================================================
‘ 変数宣言
‘==========================================================================
Dim pj As Project ‘ 現在アクティブなプロジェクト
Dim t As Task ‘ 各タスクオブジェクトを格納する変数
Dim cfWBS As CustomField ‘ WBS番号用のカスタムフィールドオブジェクト
Dim i As Long ‘ ループカウンタ
Dim currentOutlineNumber As String ‘ 現在処理中のタスクのWBS番号を一時保持
Dim parentOutlineNumber As String ‘ 親タスクのWBS番号を一時保持
‘==========================================================================
‘ 初期化処理
‘==========================================================================
On Error GoTo ErrorHandler ‘ エラーハンドラを設定
‘ アクティブなプロジェクトを取得
Set pj = Application.ActiveProject
If pj Is Nothing Then
MsgBox “アクティブなプロジェクトがありません。プロジェクトを開いてから実行してください。”, vbExclamation
Exit Sub
End If
‘ WBS番号を格納するためのカスタムフィールドを取得または作成
On Error Resume Next ‘ カスタムフィールドが存在しない場合のエラーを無視
Set cfWBS = pj.CustomFields.Item(CUSTOM_FIELD_NAME)
On Error GoTo ErrorHandler ‘ エラーハンドラを元に戻す
If cfWBS Is Nothing Then
‘ カスタムフィールドが存在しない場合、新規作成を試みる
‘ ここでは便宜上、Text1フィールドをWBS番号として利用することを想定
‘ 実際には、プロジェクトのカスタムフィールド設定で適切なフィールドを事前に定義しておくことを推奨します。
‘ 例: pj.CustomFields.Add(“WBS番号”, FieldNameToFieldConstant(“Text1”))
‘ 今回は、既に”WBS番号”という名前のカスタムフィールド(Text型またはNumber型)が存在することを前提とします。
MsgBox “カスタムフィールド ‘” & CUSTOM_FIELD_NAME & “‘ が見つかりません。” & vbCrLf & _
“プロジェクトのカスタムフィールド設定で、この名前のフィールドを定義してください。”, vbExclamation
Exit Sub
End If
‘==========================================================================
‘ メイン処理: タスクをループしてWBS番号を生成・設定
‘==========================================================================
‘ 処理の開始をユーザーに通知
Application.StatusBar = “WBS番号を生成中…”
‘ 最初のタスクからループを開始
‘ Project VBAでは、タスクのコレクションはインデックス0から始まります。
‘ ただし、タスク0は通常、プロジェクトのダミータスクなので、タスク1から処理することが多いです。
‘ ここでは、OutlineChildrenコレクションを使用して、階層構造を維持したまま処理します。
‘ 親タスクのWBS番号を管理するためのスタック(配列)
Dim outlineNumberStack() As String
ReDim outlineNumberStack(1 To 100) ‘ 初期サイズ(必要に応じて拡張)
Dim stackLevel As Long ‘ スタックの現在の深さ
stackLevel = 0
‘ プロジェクトのルートレベル(インデントレベル1)のタスクから開始
For Each t In pj.Tasks
‘ プロジェクトのルートレベル(インデントレベル1)のタスクに注目
If t.OutlineLevel = 1 Then
stackLevel = 1 ‘ スタックレベルをリセット
outlineNumberStack(stackLevel) = “1” ‘ ルートレベルは常に “1”
currentOutlineNumber = outlineNumberStack(stackLevel)
‘ WBS番号をカスタムフィールドに設定
SetTaskWBSNumber t, cfWBS, currentOutlineNumber
‘ 子タスクの処理
ProcessChildTasks t, cfWBS, outlineNumberStack, stackLevel, currentOutlineNumber
End If
Next t
‘==========================================================================
‘ 終了処理
‘==========================================================================
Application.StatusBar = False ‘ ステータスバーをクリア
MsgBox “WBS番号の生成が完了しました。”, vbInformation
Exit Sub ‘ 正常終了
‘==============================================================================
‘ エラーハンドラ
‘==============================================================================
ErrorHandler:
Application.StatusBar = False ‘ エラー発生時もステータスバーをクリア
MsgBox “エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical
‘ 必要に応じて、エラー発生時のクリーンアップ処理を追加
Set cfWBS = Nothing
Set t = Nothing
Set pj = Nothing
End Sub
‘==============================================================================
‘ サブルーチン: 子タスクを再帰的に処理する
‘==============================================================================
Private Sub ProcessChildTasks(ByRef parentTask As Task, ByRef wbsCustomField As CustomField, _
ByRef outlineNumberStack() As String, ByRef stackLevel As Long, _
ByRef parentOutlineNumber As String)
Dim t As Task ‘ 子タスクオブジェクトを格納する変数
Dim nextOutlineNumber As Long ‘ 次の連番
Dim currentTaskOutlineLevel As Long ‘ 現在処理中のタスクのインデントレベル
Dim currentTaskOutlineNumber As String ‘ 現在処理中のタスクのWBS番号
‘ 親タスクの直下の子タスクをループ
‘ Task.OutlineChildren コレクションは、指定されたタスクの直下の子タスクのみを返します。
For Each t In parentTask.OutlineChildren
currentTaskOutlineLevel = t.OutlineLevel
‘ 階層レベルに応じたWBS番号の計算
If currentTaskOutlineLevel > stackLevel Then
‘ 新しい階層レベルに入った場合、スタックを更新
stackLevel = currentTaskOutlineLevel
‘ 親タスクのWBS番号をスタックに保存
outlineNumberStack(stackLevel) = parentOutlineNumber
‘ 新しいレベルでの連番は1から開始
nextOutlineNumber = 1
ElseIf currentTaskOutlineLevel = stackLevel Then
‘ 同じ階層レベルの場合、連番をインクリメント
nextOutlineNumber = CLng(Split(parentOutlineNumber, “.”)(UBound(Split(parentOutlineNumber, “.”)))) + 1
Else ‘ currentTaskOutlineLevel < stackLevel
' 上位の階層レベルに戻る場合、スタックを遡る
' 例: 1.1.2 から 1.2 に戻る場合
stackLevel = currentTaskOutlineLevel
' 親タスクのWBS番号をスタックから取得
' parentOutlineNumber = outlineNumberStack(stackLevel) ' この行は不要。親タスクの番号は既にparentOutlineNumberに入っている。
nextOutlineNumber = CLng(Split(parentOutlineNumber, ".")(UBound(Split(parentOutlineNumber, ".")))) + 1
End If
' 現在のタスクのWBS番号を生成
If stackLevel = 1 Then
' ルートレベルの場合、単純な連番
currentTaskOutlineNumber = CStr(nextOutlineNumber)
Else
' 親のWBS番号に、現在の連番をドットで連結
currentTaskOutlineNumber = outlineNumberStack(stackLevel - 1) & "." & CStr(nextOutlineNumber)
End If
' WBS番号をカスタムフィールドに設定
SetTaskWBSNumber t, wbsCustomField, currentTaskOutlineNumber
' このタスクの子タスクを再帰的に処理
' ここでparentOutlineNumberを更新して、次の再帰呼び出しに渡す
ProcessChildTasks t, wbsCustomField, outlineNumberStack, stackLevel, currentTaskOutlineNumber
Next t
' 子タスクの処理が終わったら、スタックレベルを戻す
' ただし、同じ親タスクの次の兄弟タスクを処理するために、stackLevelはそのままにする
' 次の兄弟タスクが、現在のタスクと同じ階層レベルであれば、nextOutlineNumberが正しくインクリメントされる
End Sub
'==============================================================================
' サブルーチン: タスクにWBS番号を設定する(エラーハンドリング付き)
'==============================================================================
Private Sub SetTaskWBSNumber(ByRef task As Task, ByRef wbsCF As CustomField, ByVal wbsNumber As String)
On Error Resume Next ' 個別のタスク設定でエラーが発生しても、処理を続行
' カスタムフィールドにWBS番号を設定
' FieldNameToFieldConstant("Text1")のような直接的な指定は、
' CustomFieldオブジェクトではAllowableValuesやFormulaプロパティの操作が主となるため、
' ここではValueプロパティで直接値を設定します。
' WBS番号は通常、テキストまたは数値として扱われます。
' CustomFieldオブジェクトのValueプロパティはVariant型なので、文字列として設定します。
wbsCF.Value = wbsNumber
' TaskオブジェクトのCustomFieldValueプロパティを使用して、タスクのカスタムフィールドに値を設定します。
' Task.CustomFieldValue(FieldID) の形式でアクセスするのが一般的ですが、
' CustomFieldオブジェクト自体を直接渡すことはできません。
' FieldIDは、CustomFieldオブジェクトのFieldIDプロパティから取得します。
task.CustomFieldValue(wbsCF.FieldID) = wbsNumber
If Err.Number <> 0 Then
Debug.Print “タスクID: ” & task.ID & “, タスク名: ” & task.Name & ” のWBS番号設定中にエラー発生。” & Err.Description
Err.Clear
End If
On Error GoTo 0 ‘ エラーハンドラをリセット
End Sub
コードのポイント解説
1. `CUSTOM_FIELD_NAME` 定数:
- WBS番号を格納するカスタムフィールドの名前を定義します。プロジェクトのカスタムフィールド設定で、この名前のフィールド(例:「WBS番号」)を事前に作成しておいてください。Text型でもNumber型でも構いません。
2. `Application.ActiveProject`:
- 現在開いているプロジェクトオブジェクトを取得します。
- パフォーマンスの重み: プロジェクトが開かれていない場合にエラーとなるため、必ず存在チェックを行います。
3. カスタムフィールドの取得:
- `pj.CustomFields.Item(CUSTOM_FIELD_NAME)` で、指定した名前のカスタムフィールドオブジェクトを取得します。
- エラーハンドリング: `On Error Resume Next` を使って、カスタムフィールドが存在しない場合のエラーを一時的に無視し、`cfWBS Is Nothing` で存在チェックを行っています。
- 重要: このパーサーを実用するには、事前にProjectの「リソース」または「タスク」のカスタムフィールド設定で、「WBS番号」のような名前でフィールドを作成しておく必要があります。 コード内で動的に作成することも可能ですが、管理が煩雑になるため、手動で定義しておくことを強く推奨します。
4. `pj.Tasks` コレクション:
- プロジェクト内のすべてのタスクオブジェクトをループ処理します。
- パフォーマンスの重み: タスク数が非常に多い場合、このループ処理自体がパフォーマンスに影響を与える可能性があります。後述する再帰処理で、より効率的に階層をたどります。
5. `ProcessChildTasks` サブルーチン (再帰処理):
- このパーサーの「肝」となる部分です。タスクの階層構造を理解し、WBS番号を正しく生成するために「再帰」というテクニックを使っています。
- 再帰とは?: 関数(ここではサブルーチン)が自分自身を呼び出すことです。これにより、入れ子になった構造(ツリー構造)を簡単に扱うことができます。
- `parentTask.OutlineChildren`: 親タスクの「直下」の子タスクのみを取得します。これにより、階層を一段ずつ降りていくことができます。
- `outlineNumberStack()` と `stackLevel`:
- `outlineNumberStack()` は、各階層レベルのWBS番号を一時的に保存する「スタック」として機能します。例えば、`1.1.1` の場合、`stackLevel` が 3 で、`outlineNumberStack(1) = “1”`, `outlineNumberStack(2) = “1”`, `outlineNumberStack(3) = “1”` のようになります。
- `stackLevel` は、現在の階層の深さを示します。
- WBS番号の生成ロジック:
- `currentTaskOutlineLevel > stackLevel` の場合: 新しい階層に入ったので、`stackLevel` を更新し、新しいレベルの連番を 1 から開始します。
- `currentTaskOutlineLevel = stackLevel` の場合: 同じ階層にいるので、親タスクのWBS番号の最後の数値をインクリメントします。
- `currentTaskOutlineLevel < stackLevel` の場合: 上位の階層に戻ったので、`stackLevel` を更新し、親のWBS番号に基づいて連番を計算します。
- パフォーマンス: 再帰処理は、コードが簡潔になる反面、非常に深い階層構造や大量のタスクがあると、スタックオーバーフローやパフォーマンス低下の原因となることがあります。このコードでは、スタックのサイズをある程度確保していますが、極端に深いプロジェクトでは調整が必要になるかもしれません。
6. `SetTaskWBSNumber` サブルーチン:
- 個々のタスクにWBS番号を設定する処理をカプセル化しています。
- `task.CustomFieldValue(wbsCF.FieldID) = wbsNumber`: ここが、タスクのカスタムフィールドに値を書き込む核心部分です。`CustomField` オブジェクトから `FieldID` を取得し、そのIDを使ってタスクのカスタムフィールドに直接値を代入します。
- パフォーマンス: 個々のタスクへの書き込みは通常軽量ですが、大量のタスクに対してこの処理を繰り返すと、Project VBA内部でのデータ更新処理が積み重なり、パフォーマンスに影響が出ることがあります。
7. `Application.StatusBar`:
- 処理の進行状況を画面下部のステータスバーに表示します。長時間の処理中にユーザーに安心感を与えるための、ちょっとした気遣いです。
8. エラーハンドリング (`On Error GoTo ErrorHandler`):
- 予期せぬエラーが発生した場合に、プログラムが強制終了するのではなく、エラーメッセージを表示して安全に終了するための仕組みです。
- パフォーマンス: エラーハンドラ自体はパフォーマンスに影響を与えませんが、`On Error Resume Next` を多用しすぎると、本来検出されるべきエラーを見逃してしまう可能性があるため、注意が必要です。
実行方法
1. Project VBAを開き、WBS番号を採番したいプロジェクトを開きます。
2. `Alt + F11` キーを押してVBAエディタを開きます。
3. 「挿入」メニューから「標準モジュール」を選択し、開いたコードウィンドウに上記のVBAコードを貼り付けます。
4. コード内の `Const CUSTOM_FIELD_NAME As String = “WBS番号”` の部分を、ご自身のプロジェクトで定義しているカスタムフィールドの名前に合わせて修正します。
5. VBAエディタのメニューから「実行」→「Sub/ユーザーフォームの実行」を選択するか、`F5` キーを押して `GenerateWBSNumbers` マクロを実行します。
陥りやすいエラーとその対処法
- エラー1: 「カスタムフィールド ‘WBS番号’ が見つかりません。」
- 原因: コードで指定したカスタムフィールド名(`CUSTOM_FIELD_NAME`)が、プロジェクトのカスタムフィールド設定に存在しない。
- 対処法: Project VBAの「ツール」→「タイムスケール」→「カスタムフィールド」で、指定した名前のカスタムフィールド(Text型またはNumber型)を新規作成または既存のフィールド名を修正してください。
- エラー2: 実行時エラー ‘1004’: メソッド ‘CustomFieldValue’ またはプロパティ ‘Value’ を実行できません。
- 原因: 権限の問題、またはカスタムフィールドが正しく設定されていない可能性があります。また、タスクがロックされている場合なども考えられます。
- 対処法: プロジェクトのカスタムフィールド設定を確認し、フィールドが有効になっているか確認してください。必要であれば、プロジェクトを管理者権限で開いてみてください。
- エラー3: WBS番号が期待通りに生成されない(例: 1.2.1 の次が 1.2.10 になるなど)。
- 原因: `ProcessChildTasks` サブルーチンのWBS番号生成ロジックが、特定の階層構造で意図通りに動作していない可能性があります。特に、兄弟タスクの連番計算部分で問題が発生しやすいです。
- 対処法: コードの `ProcessChildTasks` サブルーチン内のWBS番号生成ロジック(`nextOutlineNumber` の計算部分)を注意深く見直し、`Split` 関数や `CStr`, `CLng` の使い方を確認してください。`Debug.Print` を使って、各ステップでの `parentOutlineNumber` や `nextOutlineNumber` の値を確認するデバッグが有効です。
- エラー4: プログラムが非常に遅い、またはハングアップする。
- 原因: プロジェクト内のタスク数が非常に多い(数千〜数万件)場合、タスクコレクションのループ処理や、再帰処理の深さが原因でパフォーマンスが低下する可能性があります。
- 対処法:
- `Application.ScreenUpdating = False`: コードの先頭にこれを追加し、末尾で `Application.ScreenUpdating = True` に戻すことで、画面描画のオーバーヘッドを削減できます。
- `Application.Calculation = pjManual`: コードの先頭で計算モードを手動にし、末尾で `Application.Calculation = pjAuto` に戻すことで、Project VBAの自動計算によるパフォーマンス低下を防げます。
- タスクのフィルタリング: WBS番号を採番したいタスク群だけに絞り込む(フィルタリングする)ことで、処理対象を減らすことができます。
- 再帰の制限: 非常に深い階層構造の場合は、再帰処理ではなく、フラットなリストを処理して階層を別途管理するような、より高度なアルゴリズムを検討する必要があるかもしれません。
まとめ:Project VBAマスターへの道
どうでしたか? WBS番号の自動採番パーサーは、Project VBAの「タスク」オブジェクトの階層構造を理解し、それをカスタムフィールドに反映させるための、まさに実践的なツールです。
今回ご紹介したコードは、汎用性を考慮していますが、実際のプロジェクトでは、さらに細かな要件(特定のタスクタイプのみ対象にする、特定のカスタムフィールドをトリガーにするなど)に合わせてカスタマイズしていくことになるでしょう。
このパーサーを理解し、自分のプロジェクトで動かすことができれば、あなたはもうProject VBAの「マクロの記録」レベルから卒業し、より本質的な部分でProject VBAを使いこなせる「中級者」の仲間入りです!
ぜひ、あなたのプロジェクトでこのパーサーを試してみてください。そして、もし「こんな機能があったらもっと便利なのに!」というアイデアがあれば、ぜひコメントで教えてくださいね。Project VBAの世界は、まだまだ奥が深いのですから!
