Project VBAを掌握する極限の知見:CSVからのタスク依存関係一括構築とメモリ・パフォーマンス極限チューニング
大規模なプロジェクト管理において、Microsoft Projectの真価は、その強固なスケジュール計算エンジンとWBSの依存関係(Predecessors)の制御にある。しかし、数百・数千行に及ぶタスクのリンク構造を手動で構築・維持することは、ヒューマンエラーの温床であり、エンジニアリングの観点から完全にナンセンスだ。
外部システム(ERPやExcel、独自DBなど)から出力されたCSVを基に、MS Projectへタスクの依存関係(FS/SS/FF/SF)を流し込む処理を実装する。
単に `Task.Predecessors.Add` をループさせるだけのコードであれば、ネットの海に溢れている。だが、シニアエンジニアが構築すべきシステムは、「数万行のデータ処理におけるパフォーマンスの担保」「COMオブジェクトの厳密なライフサイクル管理」「不正な依存関係による循環参照や例外のハンドリング」を網羅した、実戦に耐えうるアーキテクチャでなければならない。
今回は、MS Project VBAの深層を知り尽くしたアーキテクトの視点から、極限まで最適化されたタスク依存関係一括設定エンジンの全貌を解説する。
—
1. MS Project VBAにおけるパフォーマンスの罠と設計思想
MS Projectのオブジェクトモデルを操作する際、最大のボトルネックとなるのは「GUIの再描画(画面更新)」と「自動計算エンジンの都度走査」である。
数千件のタスクに対して、ループのたびにリンクを追加していくと、Projectは毎回スケジュールを再計算し、ガントチャートのビューを更新しようとする。これがスクリプトを数分レベルでフリーズさせる主原因だ。
必須の最適化アプローチ
1. 画面描画の完全抑制 (`ScreenUpdating = False`)
2. 計算モードの手動化 (`Calculation = pjCalculationManual`)
3. オブジェクト変数の即時解放とメモリリークの防止
4. 一意なキー(独自TextフィールドやUniqueID)による高速ルックアップ
これらを徹底することで、処理速度を劇的に(場合によっては数十倍から数百倍)向上させることが可能となる。
—
2. CSVフォーマットの設計
前提として、外部から読み込むCSVの構造を以下のように定義する。
TaskUniqueID,PredecessorUniqueID,DependencyType,Lag
105,101,FS,2d
106,102,SS,0
107,103,FF,1h
- TaskUniqueID: 後続タスクの固有ID(UniqueID)
- PredecessorUniqueID: 先行タスクの固有ID(UniqueID)
- DependencyType: 依存関係の種類 (`FS`: Finish-to-Start, `SS`: Start-to-Start, `FF`: Finish-to-Finish, `SF`: Start-to-Finish)
- Lag: ラグ(遅延時間。例: `2d` = 2日, `4h` = 4時間)
※ ※タスクの特定には、変更される可能性のある「ID(行番号)」ではなく、永続的な識別子である `UniqueID` を使用するのが、エンタープライズ開発の鉄則である。
—
3. 実装コード:極限最適化された依存関係一括設定エンジン
以下のコードは、エラーハンドリング、描画・計算の抑制、メモリの明示的解放を実装したプロダクション品質のVBAモジュールである。
Option Explicit
‘ —————————————————————–
‘ @Title: Project Task Dependency Batch Importer
‘ @Description: CSVから依存関係を読み込み、MS Projectへ高パフォーマンスで反映する
‘ @Author: Chief Architect
‘ —————————————————————–
Public Sub ImportTaskDependenciesFromCSV()
Dim fso As Object
Dim ts As Object
Dim csvPath As String
Dim lineBuf As String
Dim fields() As String
Dim tProj As Project
Dim tTask As Task
Dim pTask As Task
Dim targetUniqueID As Long
Dim predUniqueID As Long
Dim depTypeStr As String
Dim lagStr As String
Dim linkString As String
Dim rowCount As Long
Dim startTime As Double
startTime = Timer
Set tProj = ActiveProject
‘ — 1. 環境の爆速化チューニング(描画・計算の停止) —
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual
‘ ファイルダイアログでCSVを選択
csvPath = Application.GetOpenFilename(“CSVファイル (.csv), .csv”, , “依存関係CSVの選択”)
If csvPath = “False” Then GoTo CleanUp
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FileExists(csvPath) Then
MsgBox “指定されたファイルが存在しません。”, vbCritical
GoTo CleanUp
End If
Set ts = fso.OpenTextFile(csvPath, 1) ‘ ForReading
‘ ヘッダー行のスキップ
If Not ts.AtEndOfStream Then ts.ReadLine
rowCount = 0
‘ — 2. メインループ:ストリーム処理によるメモリ効率化 —
Do While Not ts.AtEndOfStream
lineBuf = ts.ReadLine
If Trim(lineBuf) <> “” Then
fields = Split(lineBuf, “,”)
If UBound(fields) >= 2 Then
targetUniqueID = CLng(Trim(fields(0)))
predUniqueID = CLng(Trim(fields(1)))
depTypeStr = UCase(Trim(fields(2)))
lagStr = “”
If UBound(fields) >= 3 Then
lagStr = Trim(fields(3))
End If
‘ タスクの取得(UniqueIDによる高速検索)
On Error Resume Next
Set tTask = tProj.Tasks.UniqueID(targetUniqueID)
Set pTask = tProj.Tasks.UniqueID(predUniqueID)
On Error GoTo ErrorHandler
If Not tTask Is Nothing And Not pTask Is Nothing Then
‘ Predecessors構文の構築 (例: “101FS+2d”)
linkString = CStr(predUniqueID) & depTypeStr
If lagStr <> “” Then
linkString = linkString & “+” & lagStr
End If
‘ リンクの追加
‘ ※同一リンクの重複追加を防ぐため、必要に応じて既存チェックを入れること
tTask.Predecessors.Add Predecessor:=pTask, Type:=GetDependencyTypeEnum(depTypeStr), Lag:=lagStr
End If
‘ オブジェクト変数のループ内解放
Set tTask = Nothing
Set pTask = Nothing
rowCount = rowCount + 1
End If
End If
Loop
ts.Close
‘ — 3. 計算の再実行と描画の復元 —
Application.Calculation = pjCalculationAutomatic
Application.ScreenUpdating = True
MsgBox “依存関係のインポートが完了しました。” & vbCrLf & _
“処理件数: ” & rowCount & ” 件” & vbCrLf & _
“処理時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, vbInformation, “完了”
CleanUp:
‘ 異常終了時の安全策
Application.Calculation = pjCalculationAutomatic
Application.ScreenUpdating = True
‘ オブジェクトの明示的解放(メモリリーク防止)
Set ts = Nothing
Set fso = Nothing
Set tProj = Nothing
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error ” & Err.Number & “: ” & Err.Description, vbCritical, “致命的なエラー”
Resume CleanUp
End Sub
‘ 依存関係文字列からMS ProjectのPjTaskLinkType列挙体へ変換
Private Function GetDependencyTypeEnum(ByVal typeStr As String) As PjTaskLinkType
Select Case typeStr
Case “SS”: GetDependencyTypeEnum = pjLinkSS
Case “FF”: GetDependencyTypeEnum = pjLinkFF
Case “SF”: GetDependencyTypeEnum = pjLinkSF
Case Else: GetDependencyTypeEnum = pjLinkFS
End Select
End Function
—
4. チーフアーキテクトが解説するコードの急所
A. オブジェクトモデルのトラバーサル最適化
`tProj.Tasks.Item(i)` のようなインデックスアクセスは、コレクションの再評価が発生するため、大規模データでは劇的に遅くなる。今回採用している `tProj.Tasks.UniqueID(id)` は内部インデックスを直接引くため、O(1)に近い高速ルックアップを実現している。
B. COMコンポーネントのメモリ管理とライフサイクル
VBAのガベージコレクションは極めて脆弱だ。特に `Scripting.FileSystemObject` や Projectの各オブジェクト (`Task`, `Project`) をループ内で参照・破棄を繰り返すと、COMの参照カウンタが溢れ、メモリリークやVBAの突然の強制終了(クラッシュ)を引き起こす。
ループの内部で `Set tTask = Nothing` を明示的に実行し、スコープ単位でのメモリ解放を担保することが、堅牢なシステムの生死を分ける。
C. 実行時例外のハンドリングと状態の保証
処理途中でエラーが発生した場合、`Application.ScreenUpdating = False` や `Calculation = pjCalculationManual` の状態がそのまま残ると、ユーザーのMS Projectが操作不能(ハングアップしたような状態)に陥る。
`On Error GoTo ErrorHandler` を経由して、必ずクリーンアップ処理で環境変数を元の状態(Automatic / True)に戻す防御的プログラミングが絶対条件となる。
—
5. まとめ
業務自動化の本質は、コードの行数や見た目の美しさではなく、「大規模データに対しても破綻しないスケーラビリティ」と「予期せぬ異常系に対する徹底した堅牢性」にある。
今回紹介した手法を適用することで、手作業では数時間かかる複雑なWBSの依存関係構築を、数秒のバックグラウンド処理へと昇華させることが可能だ。
レガシーと侮られがちなVBAであっても、オブジェクトのライフサイクルとProjectの内部挙動を完全に制御下に入れれば、エンタープライズの基幹システムと渡り合える強力なソリューションとなる。プロフェッショナルとして、妥協なきコードベースを構築し続けてほしい。
