Project VBAにおけるWBS階層構造維持・タスク群切り出しモジュールの極意
大規模プロジェクトの管理において、WBS(Work Breakdown Structure)の階層構造を維持したまま、特定のタスク群を別ファイルへ切り出す、あるいは統合するニーズは、プロジェクトの分割管理や、フェーズごとのタスク集約といった場面で頻繁に発生します。
しかし、この一見単純に見える操作が、実際には多くの落とし穴を孕んでいます。単にデータをコピー&ペーストするだけでは、タスク間の依存関係が崩壊し、WBSの構造が破綻してしまう。結果として、プロジェクト全体の進捗管理に深刻な影響を及ぼしかねません。
本稿では、Project VBAを熟知したチーフアーキテクトの視点から、この「WBS階層構造維持・タスク群切り出しモジュール」を堅牢かつ保守性の高い形で開発するための、実践的な設計思想と具体的なコード例を伝授します。読者の皆様が、現場で即戦力となる業務効率化ツールを開発できるよう、魂を込めて解説していきましょう。
—
1. なぜ、安易なコピー&ペーストが破滅を招くのか
まず、なぜWBSの階層構造を保ったままタスク群を切り出すことが、それほどまでに難しいのか、その根本原因を理解することが重要です。
1.1. WBSの「階層」は単なるインデントではない
多くの開発者が陥りがちな誤解は、WBSの階層が単に「インデント」レベルで表現されているだけだと考えてしまうことです。しかし、Project VBAにおけるWBSの階層は、より深い意味合いを持ちます。
- 親子関係の明示: 各タスクは、その上位タスク(サマリータスク)との明確な親子関係によって定義されます。この関係性は、タスクの表示順序だけでなく、プロジェクト全体の構造、そして依存関係の解釈にも深く関わってきます。
- サマリータスクの役割: サマリータスクは、単に子タスクをまとめるだけでなく、その配下にあるタスク群の期間、コスト、リソースなどを集計・代表する役割を担います。階層構造が崩れると、この集計情報も意味をなさなくなります。
- 依存関係の紐付け: タスク間の依存関係(先行タスク、後続タスク)は、プロジェクトの論理的な流れを定義する生命線です。この依存関係は、タスクの「ID」や「UID」(Unique Identifier)といった内部的な識別子に基づいて確立されています。階層構造が変化すると、これらのIDの関連性が失われ、依存関係が意図せず解除されたり、誤ったタスクに紐づいたりする危険性が極めて高くなります。
1.2. 「切り出し」が引き起こすIDとUIDの混乱
タスク群を別ファイルに切り出す、あるいは別のファイルから統合する際に、最も深刻な問題となるのが「タスクID」と「タスクUID」の扱いです。
- タスクID: Project VBAで表示されるタスクIDは、その時点での表示順序に基づいた連番です。これは動的に変動するため、ファイル間で固定的な識別子として利用するには不向きです。
- タスクUID: Project VBAは、各タスクに内部的に一意の「UID」を割り当てています。このUIDは、タスクが作成・削除・移動されても、原則として永続的にそのタスクを識別します。依存関係の定義は、このUIDを基に行われます。
ファイル間でタスクをコピー&ペーストすると、新しいファイルでは当然、既存のタスクとは異なる新しいUIDが割り当てられます。このとき、元のファイルにあった依存関係情報(「タスクAのUIDが10であるため、タスクBはタスクAの後に開始する」というような情報)は、新しいUIDの文脈では一切意味をなさなくなります。結果として、依存関係はすべて失われてしまうのです。
2. 堅牢な設計思想:オブジェクトのライフサイクルとパフォーマンスを理解する
この問題を解決し、堅牢なモジュールを開発するためには、Project VBAのオブジェクトモデルを深く理解し、タスクのライフサイクルとパフォーマンスの重みを考慮した設計が不可欠です。
2.1. 根本的なアプローチ:UIDを「キー」とするデータ操作
WBSの階層構造と依存関係を維持したままタスク群を操作する最も確実な方法は、UIDを「キー」として、タスクのプロパティを抽出し、新しいファイルで再現することです。
1. ソースファイルからのタスク抽出:
- 対象となるタスク群のUIDを特定します。
- 各タスクのUID、名前、期間、開始日、終了日、先行タスクのUID、後続タスクのUID、リソース割り当て情報など、必要なプロパティをすべて取得します。
- 重要なのは、上位サマリータスクのUIDと、そのタスクがどのレベルの階層に属していたか(インデントレベル)も同時に記録することです。
2. ターゲットファイルへのタスク構築:
- ターゲットファイルで、抽出したタスクを適切な順序と階層構造で新規作成します。
- 新規作成されたタスクには、新しいUIDが自動的に割り当てられます。
- 元のUIDと新しいUIDの対応関係を一時的に保持しておきます。
3. 依存関係の再構築:
- 保持しておいた対応関係を用いて、元のファイルでの依存関係情報を、ターゲットファイルでの新規UIDに基づいて再構築します。例えば、「元のUID A」が「新しいUID X」に、「元のUID B」が「新しいUID Y」になった場合、元のファイルで「UID A」→「UID B」の依存関係があれば、新しいファイルでは「UID X」→「UID Y」の依存関係を設定します。
- 階層構造も、保持しておいたインデントレベル情報に基づいて、サマリータスクとの親子関係を再設定します。
2.2. パフォーマンスとオブジェクトのライフサイクルへの配慮
大規模プロジェクトファイルでは、タスク数が数千、数万に及ぶことも珍しくありません。このようなファイルに対して、安易なループ処理やオブジェクトの頻繁な生成・破棄は、パフォーマンスの低下やメモリリークの原因となります。
- プロパティのまとめて取得: `Tasks`コレクションから個々のタスクオブジェクトをループで取得し、その都度プロパティにアクセスするのではなく、可能であれば一次元配列などにまとめて取得する方が効率的です。
- オブジェクトの解放: VBAでは、COMオブジェクト(Project VBAのオブジェクトも含む)は、使用後に適切に解放しないとメモリを占有し続けます。`Set obj = Nothing`を適切に使用し、不要になったオブジェクトは速やかに解放する習慣をつけましょう。特に、大きなコレクションを扱う場合や、外部ファイルとの連携では、この点を強く意識する必要があります。
- イベント処理の無効化: タスクの追加や変更を行う際に、Project VBAは様々なイベントを発火させます。これらは通常、UIの更新などに使われますが、自動処理の際には不要であり、むしろ処理速度の低下を招きます。`Application.EventsEnabled = False` を処理の開始時に設定し、終了時に `True` に戻すことで、イベント処理を一時的に無効化できます。これは、パフォーマンス向上に極めて効果的です。
2.3. ファイル・データベース連携における注意点
- Project VBAファイル (.mpp): Project VBAファイルは、単なるデータファイルではなく、COMオブジェクトとして操作されるため、ファイルを開いたり閉じたりする操作も、オブジェクトの生成・破棄を伴います。大規模ファイルに対して頻繁な開閉は避けるべきです。可能であれば、一度開いたファイルをメモリ上で操作し、最後に保存する、という流れが理想的です。
- 外部データベース (Access, SQL Serverなど): データベース連携を行う場合、SQLインジェクションの脆弱性、トランザクション管理、データ型の一貫性などに十分注意が必要です。特に、タスクUIDのような識別子は、データベースの主キーや外部キーと密接に関連させることが多いですが、Project VBAのUIDは自動採番であり、データベース側のシーケンスやIdentityとは異なる管理が必要です。
- エラーハンドリング: ファイルが見つからない、アクセス権がない、ファイルが破損している、といった外部要因によるエラーは常に考慮する必要があります。`On Error GoTo` ステートメントなどを適切に配置し、予期せぬエラーが発生した場合でも、プログラムが異常終了せず、適切なメッセージを表示して処理を中断できるように設計します。
—
3. 実践!WBS階層維持・タスク群切り出しモジュールのプロダクションコード例
それでは、上記の設計思想に基づいた、具体的なVBAコード例を見ていきましょう。このコードは、「現在アクティブなProjectファイルから、指定されたタスク群(UIDリストで指定)を抽出し、新規Projectファイルとして保存する」というシナリオを想定しています。
前提:
- 抽出したいタスクのUIDリストが、配列 `vTargetUids` として与えられるものとします。
- 依存関係は、先行タスク・後続タスクのUIDで保持されているものとします。
- 階層構造は、タスクのID(表示順)を元に再構築します。これは、UIDがファイル間で変更されるため、UIDを直接参照して階層を再現することが困難なためです。代わりに、元ファイルでのタスクの「親子関係」をUIDで紐づけ、新しいファイルでその親子関係をUIDではなく「表示順」を元に再現する、というアプローチを取ります。
Option Explicit
‘ ==========================================================================
‘ Sub ExportTasksWithHierarchy
‘
‘ 現在アクティブなProjectファイルから、指定されたタスク群を抽出し、
‘ WBSの階層構造と依存関係を維持したまま、新しいProjectファイルとして保存する。
‘
‘ 引数:
‘ vTargetUids: 抽出対象タスクのUIDを格納したVariant型配列 (例: Array(5, 12, 23))
‘ sExportFilePath: 新規Projectファイルとして保存するパス (例: “C:\Exports\MyTasks.mpp”)
‘
‘ 戻り値:
‘ Boolean: 処理が成功した場合はTrue、失敗した場合はFalse
‘
‘ 備考:
‘ – 依存関係は、UIDをキーとして抽出・再構築する。
‘ – 階層構造は、元ファイルでの「親タスクUID」と「タスクID」を記録し、
‘ 新しいファイルで「親タスクUID」に対応する新しいUIDを特定し、
‘ その新しいUIDの子としてタスクを追加することで再現する。
‘ – パフォーマンス向上のため、Application.EventsEnabled を一時的に無効化する。
‘ ==========================================================================
Public Function ExportTasksWithHierarchy(ByRef vTargetUids As Variant, ByVal sExportFilePath As String) As Boolean
Dim oApp As MSProject.Application
Dim oCurrentProject As MSProject.Project
Dim oExportProject As MSProject.Project
Dim oTask As MSProject.Task
Dim oExportTask As MSProject.Task
‘ 抽出対象タスクのUIDを格納するDictionaryオブジェクト
‘ キー: 元のUID, 値: 新しいUID
Dim dictUidMap As Object ‘ Scripting.Dictionary
‘ 抽出対象タスクの情報を格納するDictionaryオブジェクト
‘ キー: 元のUID, 値: TaskPropertiesオブジェクト (カスタムクラス)
Dim dictTaskData As Object ‘ Scripting.Dictionary
‘ 依存関係情報を格納するCollection
‘ 要素: PredecessorUID, SuccessorUID
Dim colDependencies As Collection
Dim lTaskCount As Long
Dim lExportTaskCount As Long
Dim lUid As Long
Dim lNewUid As Long
Dim lNewId As Long
Dim lParentUid As Long
Dim lNewParentUid As Long
Dim vTaskData As Variant
Dim vKey As Variant
Dim vPredecessor As Variant
Dim vSuccessor As Variant
Dim bEventsEnabled As Boolean
‘ Dictionaryオブジェクトの生成
Set dictUidMap = CreateObject(“Scripting.Dictionary”)
Set dictTaskData = CreateObject(“Scripting.Dictionary”)
Set colDependencies = New Collection
‘ 現在アクティブなProjectオブジェクトを取得
On Error Resume Next
Set oApp = Application
Set oCurrentProject = oApp.ActiveProject
On Error GoTo 0
If oCurrentProject Is Nothing Then
MsgBox “アクティブなProjectファイルがありません。”, vbCritical
ExportTasksWithHierarchy = False
Exit Function
End If
‘ 処理開始時のイベント設定
bEventsEnabled = oApp.EventsEnabled
oApp.EventsEnabled = False
‘ — ステップ 1: 抽出対象タスクの情報を収集 —
Debug.Print “— ステップ 1: 抽出対象タスク情報の収集 —”
lTaskCount = oCurrentProject.Tasks.Count
‘ 抽出対象タスクのUIDをDictionaryに登録 (検索用)
Dim dictTargetUids As Object ‘ Scripting.Dictionary
Set dictTargetUids = CreateObject(“Scripting.Dictionary”)
For lUid = LBound(vTargetUids) To UBound(vTargetUids)
If Not dictTargetUids.Exists(vTargetUids(lUid)) Then
dictTargetUids.Add vTargetUids(lUid), True
End If
Next lUid
‘ 全タスクをループし、対象タスクの情報を抽出
For Each oTask In oCurrentProject.Tasks
If dictTargetUids.Exists(oTask.UID) Then
‘ 対象タスクの情報を保存
Dim taskProps As New TaskProperties ‘ 後述するカスタムクラス
taskProps.Uid = oTask.UID
taskProps.Name = oTask.Name
taskProps.Duration = oTask.Duration
taskProps.Start = oTask.Start
taskProps.Finish = oTask.Finish
taskProps.OutlineLevel = oTask.OutlineLevel ‘ 階層レベル (インデント)
taskProps.ParentUid = oTask.ParentUID ‘ 親タスクのUID
‘ 依存関係情報を抽出
Dim oPred As MSProject.PredecessorLink
For Each oPred In oTask.PredecessorLinks
‘ 依存関係のキーは、先行タスクのUIDと後続タスクのUIDの組み合わせ
‘ ここでは、後続タスクのUID (oTask.UID) と先行タスクのUID (oPred.UID) を保存
colDependencies.Add oPred.UID & “:” & oTask.UID ‘ Key: PredecessorUID:SuccessorUID
Next oPred
‘ 抽出対象タスクの情報をDictionaryに格納
dictTaskData.Add oTask.UID, taskProps
Debug.Print ” Collected Task: UID=” & oTask.UID & “, Name='” & oTask.Name & “‘, ParentUID=” & oTask.ParentUID & “, OutlineLevel=” & oTask.OutlineLevel
End If
Next oTask
‘ 抽出対象タスクが存在しない場合
If dictTaskData.Count = 0 Then
MsgBox “指定されたUIDに該当するタスクが見つかりませんでした。”, vbExclamation
oApp.EventsEnabled = bEventsEnabled ‘ イベント設定を元に戻す
Set dictTargetUids = Nothing
Set dictTaskData = Nothing
Set colDependencies = Nothing
ExportTasksWithHierarchy = False
Exit Function
End If
‘ — ステップ 2: 新しいProjectファイルを作成し、タスクを構築 —
Debug.Print vbCrLf & “— ステップ 2: 新規Projectファイル作成とタスク構築 —”
‘ 新しいProjectオブジェクトを作成
Set oExportProject = oApp.Projects.Add
oExportProject.Name = “Exported Tasks” ‘ 仮の名前
‘ 抽出対象タスクを、親タスクを考慮して順番に追加
‘ 順序付けのために、OutlineLevelでソートしたListを作成する
Dim arrSortedUids() As Long
ReDim arrSortedUids(1 To dictTaskData.Count)
Dim lIndex As Long
lIndex = 1
‘ まず、最上位レベルのタスクを特定し、それらを起点にツリー構造を構築する
Dim dictTasksByOutline As Object ‘ Scripting.Dictionary
Set dictTasksByOutline = CreateObject(“Scripting.Dictionary”)
‘ OutlineLevelごとにタスクをグループ化
For Each vTaskData In dictTaskData.Values
If Not dictTasksByOutline.Exists(vTaskData.OutlineLevel) Then
Set dictTasksByOutline.Add vTaskData.OutlineLevel, CreateObject(“Collection”)
End If
dictTasksByOutline(vTaskData.OutlineLevel).Add vTaskData ‘ TaskPropertiesオブジェクトを追加
Next vTaskData
‘ 階層構造を維持しながらタスクを追加するための再帰関数を呼び出す
‘ 最上位レベル (OutlineLevel 1) から開始
If dictTasksByOutline.Exists(1) Then
Dim vTaskProp As Variant
For Each vTaskProp In dictTasksByOutline(1)
‘ 再帰的にタスクを追加する関数を呼び出す
Call AddTaskRecursive(oExportProject, vTaskProp, dictTaskData, dictUidMap, dictTasksByOutline, 1)
Next vTaskProp
Else
‘ OutlineLevel 1 のタスクがない場合 (通常はありえないが念のため)
MsgBox “WBSの最上位レベルのタスクが見つかりませんでした。”, vbCritical
oApp.EventsEnabled = bEventsEnabled
Set dictUidMap = Nothing
Set dictTaskData = Nothing
Set colDependencies = Nothing
Set dictTargetUids = Nothing
Set dictTasksByOutline = Nothing
ExportTasksWithHierarchy = False
Exit Function
End If
‘ — ステップ 3: 依存関係を再構築 —
Debug.Print vbCrLf & “— ステップ 3: 依存関係の再構築 —”
Dim sDepKey As Variant
Dim sPredUid As String
Dim sSuccUid As String
Dim lPredUid As Long
Dim lSuccUid As Long
For Each sDepKey In colDependencies
sPredUid = Split(sDepKey, “:”)(0)
sSuccUid = Split(sDepKey, “:”)(1)
‘ 抽出対象タスクに含まれている依存関係のみを処理
If dictUidMap.Exists(sPredUid) And dictUidMap.Exists(sSuccUid) Then
lPredUid = CLng(dictUidMap(sPredUid)) ‘ 新しいUIDを取得
lSuccUid = CLng(dictUidMap(sSuccUid)) ‘ 新しいUIDを取得
‘ 依存関係を追加 (Typeはデフォルトで良い)
‘ 依存関係のType (FS, SS, FF, SF) は、ここでは考慮せず、デフォルトのFSとする
‘ 必要であれば、元のタスクからTypeを抽出して設定する処理を追加する
On Error Resume Next
oExportProject.Tasks(lSuccUid).PredecessorLinks.Add oExportProject.Tasks(lPredUid)
If Err.Number <> 0 Then
Debug.Print ” Warning: Failed to add predecessor link between UID ” & lPredUid & ” and UID ” & lSuccUid & “. Error: ” & Err.Description
Err.Clear
Else
Debug.Print ” Added dependency: New UID ” & lPredUid & ” -> New UID ” & lSuccUid
End If
On Error GoTo 0
End If
Next sDepKey
‘ — ステップ 4: 新規Projectファイルを保存 —
Debug.Print vbCrLf & “— ステップ 4: 新規Projectファイル保存 —”
On Error Resume Next
‘ ファイル名を指定して保存
oExportProject.SaveAs sExportFilePath
If Err.Number <> 0 Then
MsgBox “ファイルの保存に失敗しました: ” & sExportFilePath & vbCrLf & “エラー: ” & Err.Description, vbCritical
oApp.EventsEnabled = bEventsEnabled ‘ イベント設定を元に戻す
Set dictUidMap = Nothing
Set dictTaskData = Nothing
Set colDependencies = Nothing
Set dictTargetUids = Nothing
Set dictTasksByOutline = Nothing
ExportTasksWithHierarchy = False
Exit Function
End If
On Error GoTo 0
‘ 新規作成したプロジェクトを閉じる(必要に応じて)
‘ oExportProject.Close
Debug.Print “— 処理完了 —”
MsgBox “タスク群を ‘” & sExportFilePath & “‘ に正常にエクスポートしました。”, vbInformation
‘ イベント設定を元に戻す
oApp.EventsEnabled = bEventsEnabled
‘ オブジェクトの解放
Set oCurrentProject = Nothing
Set oExportProject = Nothing
Set oTask = Nothing
Set oExportTask = Nothing
Set dictUidMap = Nothing
Set dictTaskData = Nothing
Set colDependencies = Nothing
Set dictTargetUids = Nothing
Set dictTasksByOutline = Nothing
ExportTasksWithHierarchy = True
End Function
‘ ==========================================================================
‘ Private Sub AddTaskRecursive
‘
‘ 指定されたタスクプロパティに基づき、新しいProjectファイルにタスクを再帰的に追加する。
‘ 階層構造を維持するために、親タスクのUIDを元に子タスクを追加していく。
‘
‘ 引数:
‘ oTargetProject: タスクを追加するProjectオブジェクト
‘ oParentTaskProps: 追加する親タスクのTaskPropertiesオブジェクト
‘ dictTaskData: 全抽出タスクのTaskPropertiesをUIDをキーとして格納したDictionary
‘ dictUidMap: 元UIDと新UIDのマッピングを格納するDictionary (参照渡し)
‘ dictTasksByOutline: OutlineLevelでグループ化されたタスクDictionary
‘ lCurrentOutlineLevel: 現在処理中のOutlineLevel
‘
‘ 戻り値:
‘ なし
‘ ==========================================================================
Private Sub AddTaskRecursive(ByRef oTargetProject As MSProject.Project, _
ByRef oParentTaskProps As TaskProperties, _
ByRef dictTaskData As Object, _
ByRef dictUidMap As Object, _
ByRef dictTasksByOutline As Object, _
ByVal lCurrentOutlineLevel As Long)
Dim oExportTask As MSProject.Task
Dim lNewUid As Long
Dim lParentUid As Long
Dim vTaskProp As Variant
Dim lNextOutlineLevel As Long
‘ 親タスクの新しいUIDを取得 (または、親タスクがまだ存在しない場合は0とする)
If dictUidMap.Exists(oParentTaskProps.Uid) Then
lParentUid = CLng(dictUidMap(oParentTaskProps.Uid))
Else
‘ 親タスクがまだ追加されていない場合(通常、最上位レベルのタスクはこの条件に該当しない)
‘ もしくは、親タスクが抽出対象外であった場合。
‘ この場合、新しいファイルでの最上位レベルに追加されるべきだが、
‘ 実際には、親タスクのUIDがoParentTaskProps.ParentUidで参照される。
‘ ここでは、追加するタスクの親となるタスクのUID (新しいUID) を取得する。
‘ もし、oParentTaskProps.ParentUid が 0 や Nothing であれば、それは最上位タスク。
‘ 抽出対象外の親タスクは、新しいプロジェクトのルート直下に追加される。
If oParentTaskProps.ParentUid <> 0 And Not dictUidMap.Exists(oParentTaskProps.ParentUid) Then
‘ 抽出対象外の親タスクが存在する場合、それは新しいプロジェクトのルート直下に追加される
‘ この処理は、新しいプロジェクトのルート直下に追加されるタスクの親IDを0とするため、
‘ 実際には、このSubを呼び出す側で親タスクが解決されている必要がある。
‘ ここでは、oParentTaskProps.ParentUid が 0 であるか、
‘ dictUidMap に存在しない場合 (つまり、抽出対象外の親タスク) を考慮する。
lParentUid = 0 ‘ ルート直下に追加
ElseIf dictUidMap.Exists(oParentTaskProps.ParentUid) Then
‘ 親タスクが抽出対象であり、既にUIDマッピングされている場合
lParentUid = CLng(dictUidMap(oParentTaskProps.ParentUid))
Else
‘ 親タスクが 0 (ルート直下) である場合
lParentUid = 0
End If
End If
‘ 新しいタスクを追加
On Error Resume Next
If lParentUid = 0 Then
‘ 親タスクがルート直下の場合
Set oExportTask = oTargetProject.Tasks.Add(Name:=oParentTaskProps.Name, Before:=oTargetProject.Tasks.UniqueIDFromId(oParentTaskProps.Id)) ‘ Idは本来使えないので、ここでは無視
‘ Addメソッドの引数でBefore/Afterを指定して位置を制御するのが一般的だが、
‘ WBS構造を保つためには、OutlineLevelを元に親子関係を設定するのがより直接的。
‘ ここでは、Addメソッドでタスクを作成し、後からParentUIDを設定する。
Set oExportTask = oTargetProject.Tasks.Add(Name:=oParentTaskProps.Name)
Else
‘ 親タスクの下に追加する場合
‘ 親タスクのUIDを指定して、その子として追加する
‘ 実際には、Project VBA では Tasks.Add メソッドで直接子タスクを追加する機能はない。
‘ Tasks.Add でタスクを作成した後、ParentUID プロパティを設定する。
Set oExportTask = oTargetProject.Tasks.Add(Name:=oParentTaskProps.Name)
oExportTask.ParentUID = lParentUid ‘ 親タスクのUIDを設定
End If
If Err.Number <> 0 Then
Debug.Print ” Error adding task ‘” & oParentTaskProps.Name & “‘: ” & Err.Description
Err.Clear
Exit Sub ‘ エラーが発生したら、このパスの追加は中断
End If
On Error GoTo 0
‘ 新しく割り当てられたUIDを取得
lNewUid = oExportTask.UID
‘ UIDマッピングを更新
dictUidMap.Add oParentTaskProps.Uid, lNewUid
‘ TaskProperties オブジェクトのUIDを新しいUIDで更新 (必要であれば)
‘ oParentTaskProps.Uid = lNewUid ‘ 参照渡しなので、元のオブジェクトも更新される
‘ タスクのプロパティを設定 (期間、開始日、終了日など)
oExportTask.Duration = oParentTaskProps.Duration
‘ Start/Finish は、依存関係や制約によって自動計算される場合があるため、
‘ ここで設定すると意図しない結果になる可能性がある。
‘ 基本的には、依存関係と制約のみを設定し、Project VBA に計算させるのが安全。
‘ もし、絶対的な開始/終了日を設定したい場合は、タスクのConstraintTypeなどを考慮する必要がある。
‘ oExportTask.Start = oParentTaskProps.Start
‘ oExportTask.Finish = oParentTaskProps.Finish
‘ OutlineLevel を設定 (この設定は、タスクの追加時にParentUIDを設定していれば不要な場合が多いが、念のため)
‘ oExportTask.OutlineLevel = lCurrentOutlineLevel ‘ これは設定できないプロパティ
Debug.Print ” Added Task: NewUID=” & lNewUid & “, Name='” & oExportTask.Name & “‘, ParentUID=” & lParentUid
‘ — 子タスクを再帰的に追加 —
lNextOutlineLevel = lCurrentOutlineLevel + 1
‘ 次のOutlineLevelに属するタスクが存在するか確認
If dictTasksByOutline.Exists(lNextOutlineLevel) Then
Dim vChildTaskProp As Variant
For Each vChildTaskProp In dictTasksByOutline(lNextOutlineLevel)
‘ 現在処理中のタスク (oParentTaskProps) が、子タスク (vChildTaskProp) の親であるかを確認
If vChildTaskProp.ParentUid = oParentTaskProps.Uid Then
‘ 再帰的に子タスクを追加
AddTaskRecursive oTargetProject, vChildTaskProp, dictTaskData, dictUidMap, dictTasksByOutline, lNextOutlineLevel
End If
Next vChildTaskProp
End If
End Sub
‘ ==========================================================================
‘ カスタムクラス: TaskProperties
‘
‘ 抽出対象タスクの主要なプロパティを保持するためのクラスモジュール。
‘ 複数タスクの情報をまとめて管理するのに便利。
‘
‘ このクラスモジュールをVBAエディタに別途追加する必要があります。
‘ (挿入 -> クラスモジュール)
‘ クラスモジュール名を「TaskProperties」に変更してください。
‘ ==========================================================================
‘
‘ — TaskProperties クラスモジュール —
‘ Public Uid As Long
‘ Public Id As Long ‘ 便宜上保存するが、ファイル間では無効
‘ Public Name As String
‘ Public Duration As String
‘ Public Start As Date
‘ Public Finish As Date
‘ Public OutlineLevel As Integer
‘ Public ParentUid As Long ‘ 親タスクのUID
‘ Public StartDateConstraint As PjConstraint ‘ 例: 依存関係の種類
‘ Public FinishDateConstraint As PjConstraint ‘ 例: 依存関係の種類
‘ ‘ 必要に応じて、リソース割り当て情報なども追加可能
‘ ‘ Public Resources As Collection
‘ ————————————————————————–
‘ — VBAエディタでのクラスモジュールの追加方法 —
‘ 1. VBAエディタを開く (Alt + F11)
‘ 2. プロジェクトエクスプローラーで、VBAProject (YourProjectName) を右クリック
‘ 3. [挿入] -> [クラスモジュール] を選択
‘ 4. 新しく追加された「Class1」などを選択し、プロパティウィンドウ (F4) で [名前] を「TaskProperties」に変更
‘ — TaskProperties クラスモジュールのコード例 —
‘ (注: 実際には、このコードをクラスモジュールに貼り付けてください)
‘ Public Uid As Long
‘ Public Id As Long
‘ Public Name As String
‘ Public Duration As String
‘ Public Start As Date
‘ Public Finish As Date
‘ Public OutlineLevel As Integer
‘ Public ParentUid As Long
‘ Public StartDateConstraint As PjConstraint
‘ Public FinishDateConstraint As PjConstraint
‘ — 以下は、上記コード例で使用される TaskProperties クラスの定義 —
‘ クラスモジュール TaskProperties に以下のコードを貼り付けてください。
‘ ========================================================================
‘ Public Uid As Long
‘ Public Id As Long
‘ Public Name As String
‘ Public Duration As String
‘ Public Start As Date
‘ Public Finish As Date
‘ Public OutlineLevel As Integer
‘ Public ParentUid As Long
‘ Public StartDateConstraint As PjConstraint ‘ 例: PjConstraint.pjFinishToStart
‘ Public FinishDateConstraint As PjConstraint ‘ 例: PjConstraint.pjStartToStart
‘========================================================================
‘ —– 実行例 —–
‘ Sub TestExport()
‘ ‘ 抽出したいタスクのUIDを配列で指定
‘ Dim targetUids() As Long
‘ targetUids = Array(5, 12, 23) ‘ 例: UIDが 5, 12, 23 のタスクを抽出
‘
‘ ‘ 保存先のファイルパスを指定
‘ Dim exportPath As String
‘ exportPath = Application.Path & “\ExportedTasks_” & Format(Now, “yyyymmdd_hhmmss”) & “.mpp”
‘
‘ ‘ 関数を実行
‘ If ExportTasksWithHierarchy(targetUids, exportPath) Then
‘ MsgBox “エクスポートが成功しました。”, vbInformation
‘ Else
‘ MsgBox “エクスポート中にエラーが発生しました。”, vbExclamation
‘ End If
‘ End Sub
コード解説のポイント
- `TaskProperties` クラス: タスクの UID、名前、期間、開始日、終了日、そして最も重要な「親タスクのUID」と「OutlineLevel」を保持するためのカスタムクラスです。これにより、タスクの属性を構造化して管理できます。
- `dictUidMap` (Dictionary): 元のProjectファイルでのタスクUIDと、新しく作成されたProjectファイルでのタスクUIDの対応関係を記録します。依存関係を再構築する際に、このマッピングを使用して新しいUIDに変換します。
- `dictTaskData` (Dictionary): 抽出対象となったタスクの `TaskProperties` オブジェクトを、元のUIDをキーとして格納します。
- `colDependencies` (Collection): 元のファイルで定義されていた先行・後続タスクのUIDのペアを保存します。
- `dictTasksByOutline` (Dictionary): タスクを `OutlineLevel` ごとにグループ化します。これにより、階層構造を再現する際に、親タスクの配下にある子タスクを効率的に見つけ出すことができます。
- `AddTaskRecursive` サブルーチン: このモジュールの核となる部分です。
- まず、親タスクの新しいUIDを `dictUidMap` から取得します。
- `oTargetProject.Tasks.Add` メソッドで新しいタスクを作成します。
- 作成したタスクの `ParentUID` プロパティに、取得した親タスクの新しいUIDを設定することで、階層構造を再現します。
- 新しいタスクのUIDを `dictUidMap` に登録します。
- 次に、現在のタスクの `OutlineLevel` + 1 に属するタスクを `dictTasksByOutline` から探し、そのタスクが現在のタスクの子である場合に `AddTaskRecursive` を再帰的に呼び出します。
- `Application.EventsEnabled`: 大量のタスクを追加・変更する際には、UIの更新やイベント処理がボトルネックになりがちです。このプロパティを `False` に設定することで、これらの処理を一時的に無効化し、大幅なパフォーマンス向上が期待できます。処理完了後に必ず `True` に戻すことを忘れないでください。
- エラーハンドリング: `On Error Resume Next` や `On Error GoTo 0` を適切に使用し、ファイル操作やオブジェクト操作で発生しうるエラーを捕捉・処理しています。
—
4. 運用と保守性を高めるための追加検討事項
開発したモジュールを現場で長く、そして安全に利用するためには、以下の点を考慮することをお勧めします。
4.1. ユーザーインターフェース (UI) の整備
- タスク選択UI: UIDを直接入力させるのは非実用的です。Project VBAのタスクリストを画面に表示し、チェックボックスなどで複数選択できるようにするUIを実装することで、ユーザビリティが劇的に向上します。
- 保存先選択UI: ファイルダイアログ (`Application.GetSaveAsFileName`) を使用して、ユーザーに保存場所とファイル名を指定させるようにします。
- 進捗表示: 大規模ファイルの場合、処理に時間がかかることがあります。プログレスバーなどを表示することで、ユーザーに処理状況を伝えることができます。
4.2. エラーハンドリングとログ出力の強化
- 詳細なエラーメッセージ: 発生したエラーについて、タスク名やUIDなどのコンテキスト情報を含めた、より詳細なエラーメッセージを表示するようにします。
- ログファイル: 処理の成否、発生したエラー、処理時間などをログファイルに出力することで、問題発生時の原因究明が容易になります。これは、複数のユーザーがツールを利用する場合に特に重要です。
4.3. 依存関係のタイプと遅延/先行時間の考慮
今回のコード例では、依存関係のタイプ(FS, SS, FF, SF)や、先行タスクとの遅延/先行時間(Lag)については、デフォルトの「終了-開始 (FS)」のみを考慮しています。より高度な要件では、これらの情報も抽出し、新しいファイルで正確に再現する必要があります。これには、`PredecessorLink` オブジェクトの `Type` プロパティや `Lag` プロパティなどを参照・設定する処理を追加する必要があります。
4.4. リソース割り当ての扱い
タスク群を切り出す際、そのタスクに割り当てられているリソース情報も一緒に移動させる必要がある場合があります。
- リソースのコピー: 新しいProjectファイルに、元ファイルのリソースも一緒にコピーするかどうかを決定します。
- リソースの重複: 既存のリソースと名前が重複する場合の処理(上書き、スキップ、リネームなど)を定義します。
- リソース割り当ての再設定: 元のファイルでのリソース割り当て情報を、新しいファイルでのリソース(またはコピーされたリソース)に正しく紐づける処理が必要です。
4.5. テストの重要性
本番環境に投入する前に、様々なシナリオでの徹底的なテストが不可欠です。
- 単純なタスク群: 階層構造が浅く、依存関係が少ない場合。
- 複雑な階層構造: 深いネストや、複数のサマリータスクを持つ場合。
- 多数の依存関係: 複雑に絡み合った先行・後続関係。
- 大規模ファイル: 数千、数万タスクを持つファイルでのパフォーマンステスト。
- エッジケース: 空のプロジェクト、依存関係のないタスク、サマリータスクのみの抽出など。
—
まとめ
Project VBAにおけるWBS階層構造の維持とタスク群の切り出しは、一見単純な操作に見えて、その実、オブジェクトのライフサイクル、UIDの管理、そして階層構造の論理的な再現といった、高度な知識と細心の注意を要する作業です。
本稿で示した設計思想とコード例は、これらの課題を克服し、堅牢で保守性の高いモジュールを開発するための一助となるはずです。重要なのは、単にコードをコピペするだけでなく、その背後にある「なぜ」を理解し、自身のプロジェクトの要件に合わせてカスタマイズしていくことです。
この技術が、皆様のプロジェクト管理業務の効率化に大きく貢献できることを願っています。Project VBAの奥深い世界を、ぜひ探求し続けてください。
