Project VBAを掌握する極限の知見:インフレ率追従型リソース単価一括補正エンジンの構築
Microsoft Project(以下、MS Project)におけるVBA開発は、一般的なExcel VBAの延長線上で捉えていると、必ず致命的なパフォーマンスの壁に突き当たる。特に「リソース」というエンティティは、`Resource`オブジェクトの背後で複雑なコストレートテーブル、カレンダー、そして何千もの割り当て(Assignment)が緻密なリレーションシップを結んで存在している。
マクロの記録をそのまま貼り付けたようなコードでは、数千件規模のリソースを持つエンタープライズプロジェクトにおいて、画面の描画更新と不要なオブジェクト生成・破棄の嵐によって、完了までに数十分を要するか、最悪の場合はCOM例外によるメモリリークで死に至る。
本稿では、シニアエンジニアおよび社内システム管理者を対象に、インフレ率や労務単価改定に伴うリソース単価の一括補正を、極限まで最適化されたメモリ管理と高速バッチ処理で実現するVBAシミュレーションエンジンの全貌を解説する。
—
1. 致命的なボトルネック:なぜ通常のVBAループでは破綻するのか
MS ProjectのVBA(Project VBA)は、COM(Component Object Model)を介してMS Projectのネイティブコア(C++製C/Sエンジン)と通信している。
VBA側から `ActiveProject.Resources` を巡回し、個別に `CostRateTable` や `StandardRate` を書き換えるコードを書いた場合、以下の致命的なオーバーヘッドが発生する。
1. 暗黙のCOMマーシャリング: ループのたびにVBAとネイティブ層の間でコンテキストスイッチが発生する。
2. 再計算エンジンの暴走: 単価が1つ変更されるたびに、MS Projectはスケジュール全体、コスト、キャッシュフローの再計算をバックグラウンドでトリガーしようとする。
3. 画面描画とUIスレッドの占有: 更新のたびにGUIが再描画され、プロセッササイクルが無駄に消費される。
これを打破するためには、「イベントの完全な抑制」「再計算の遅延バッチ処理」「オブジェクト変数の明示的かつ厳格なスコープ管理と即時解放」が不可欠となる。
—
2. 極限最適化アーキテクチャの設計思想
今回構築するシミュレーションツールは、以下の3つの鉄則に基づいて設計されている。
- ScreenUpdating と Calculation の制御: 一連の処理中はMS Projectの自動計算と画面更新を完全に殺す。
- Object Lifetime Management (OLM): ループ内で生成されるオブジェクト参照は、イテレーションごとに即座に `Nothing` を代入し、COM参照カウンタを即時デクリメントする。
- トランザクション的アプローチ: シミュレーション結果を一時的にメモリ上で保持し、ユーザーが確定(Commit)ボタンを押した場合のみプロジェクトへ書き込むか、あるいは一過性のバックアップ(Baseline/CostRateTableの切り替え)を利用する。
—
3. 実装コード:インフレ率一括補正エンジン
以下のコードは、選択されたコストレートテーブル(A〜E)に対し、指定されたインフレ率(例:+3.5%ならば `1.035`)を乗算し、小数点以下の端数処理(切り捨て・四捨五入など)を適用しながら一括高速補正を行う実用プロシージャである。
‘ ==============================================================================
‘ Module: modResourceCostSimulator
‘ Description: インフレ率・労務単価改定に基づくリソース単価一括補正エンジン
‘ Author: Chief Architect (Project VBA Expert)
‘ ==============================================================================
Option Explicit
‘ Windows API: 処理速度計測用(高精度タイマー)
If VBA7 Then
Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Declare Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
End If
Public Sub SimulateAndApplyInflationRate()
Dim tStart As Currency, tFreq As Currency
QueryPerformanceFrequency tFreq
QueryPerformanceCounter tStart
‘ 1. ユーザー入力の取得(実務ではUserForm経由を推奨)
Dim inflationRate As Double
Dim targetTableIndex As Integer
Dim roundMode As Integer ‘ 1:四捨五入, 2:切り捨て, 3:切り上げ
inflationRate = 1.035 ‘ 例: 3.5%のインフレ率
targetTableIndex = 0 ‘ 0: 標準レート(A), 1-4: レートB-E
roundMode = 1 ‘ 四捨五入
If ActiveProject.Resources.Count = 0 Then
MsgBox “対象となるリソースが存在しません。”, vbExclamation, “シミュレーション中断”
Exit Sub
End If
‘ 2. パフォーマンス最適化の極限:環境設定の退避とロック
Dim origCalc As Long
origCalc = Application.Calculation
‘ 画面描画とバックグラウンド再計算を強制停止
Application.Calculation = pjManual
On Error GoTo ErrorHandler
Dim resCount As Long: resCount = 0
Dim r As Resource
Dim cRate As Double
Dim i As Long
‘ 3. メモリ最適化ループ
‘ コレクションを直接回すのではなく、変数キャッシュと即座のオブジェクト解放を徹底
Dim resourcesColl As Resources
Set resourcesColl = ActiveProject.Resources
For i = 1 To resourcesColl.Count
Set r = resourcesColl(i)
‘ 該当リソースがコストリソース、またはマテリアル・作業リソースの判定
‘ ここでは作業リソース(pjResourceTypeWork)を対象とする
If Not r Is Nothing Then
If r.Type = pjResourceTypeWork Then
‘ コストレートテーブルに応じた単価取得・設定
‘ Projectの標準単価(StandardRate)はバリアント型で返されるため厳密にキャスト
Select Case targetTableIndex
Case 0 ‘ 標準単価 (Standard Rate)
If IsNumeric(r.StandardRate) Then
cRate = CDbl(r.StandardRate)
r.StandardRate = ApplyRound(cRate inflationRate, roundMode)
resCount = resCount + 1
End If
‘ 残業単価(Overtime Rate)も連動させる場合
If IsNumeric(r.OvertimeRate) Then
cRate = CDbl(r.OvertimeRate)
r.OvertimeRate = ApplyRound(cRate inflationRate, roundMode)
End If
‘ ※ CostRateTable(1)〜(4)を操作する場合はここに拡張ロジックを記述
‘ 例: r.CostRateTables(targetTableIndex).PayRates(1).StandardRate …
End Select
End If
End If
‘ 【重要】COMオブジェクトの参照を即時破棄し、メモリリークを根絶する
Set r = Nothing
Next i
Set resourcesColl = Nothing
‘ 4. 処理完了後の環境復元と再計算
Application.Calculation = origCalc
‘ 強制的にプロジェクト全体の再計算を実行
ActiveProject.Calculate
Dim tEnd As Currency, elapsedSec As Double
QueryPerformanceCounter tEnd
elapsedSec = CDbl(tEnd – tStart) / CDbl(tFreq)
MsgBox “シミュレーション完了” & vbCrLf & _
“処理対象リソース数: ” & resCount & ” 件” & vbCrLf & _
“処理時間: ” & Format(elapsedSec, “0.000”) & ” 秒”, _
vbInformation, “Project VBA エンジン”
Exit Sub
ErrorHandler:
‘ 異常終了時も必ず環境を復元する(さもないとProjectが不安定になる)
Application.Calculation = origCalc
Set r = Nothing
Set resourcesColl = Nothing
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “System Error”
End Sub
‘ 端数処理ヘルパー関数
Private Function ApplyRound(ByVal val As Double, ByVal mode As Integer) As Double
Select Case mode
Case 1: ApplyRound = Round(val, 0) ‘ 四捨五入
Case 2: ApplyRound = Int(val) ‘ 切り捨て
Case 3: ApplyRound = -Int(-val) ‘ 切り上げ
Case Else: ApplyRound = val
End Select
End Function
—
4. コードの深層解説:なぜこの書き方が「プロフェッショナル」なのか
① `Application.Calculation = pjManual` の絶対性
MS Projectは、リソース単価が1つ変わるだけで、WBS全体の「Earned Value(アーンドバリュー)」、「ACWP(実コスト)」、「ETC(完了時コスト見積もり)」のすべてを再計算し直す。
これを有効にしたまま数千件のリソースをループさせると、OSのプロセス間で膨大なIPC(プロセス間通信)が発生し、実用に耐えない遅延を引き起こす。計算を手動に固定し、ループ脱出後に `ActiveProject.Calculate` を1回だけ叩くのが、データ整合性を保ったまま速度を最大化する唯一の解である。
② イテレーションごとの `Set r = Nothing`
VBAのガベージコレクションは参照カウント方式(Reference Counting)を採用している。しかし、COMオブジェクト(MS Projectの内部オブジェクト)の参照は、VBAのランタイムが自動解放するタイミングが曖昧になりがちである。
ループのインデクサ内で `Set r = resourcesColl(i)` と代入し続けると、古いオブジェクトへの参照がメモリ上に残存し、数千回のイテレーションの過程でメモリ消費量が急増する。ループの末尾で明示的に `Set r = Nothing` を呼ぶことで、COMラッパーのライフサイクルを完全に制御下におく。
③ 型安全性の確保 (`IsNumeric` と `CDbl`)
Projectのプロパティはバリアント(`Variant`)で返されることが多く、未設定のリソース(Null値や空文字)が含まれている場合に直接演算を行うと、型不一致エラー(Error 13)でマクロが強制終了する。
必ず `IsNumeric` でガードをかけ、明示的に `CDbl` で倍精度浮動小数点数へキャストした上で演算を行うべきである。
—
5. レガシー環境・システム間連携における実務的知見
社内システム管理者やシニアエンジニアがこのツールを運用する際、単にVBAを動かすだけでなく、外部基幹システム(ERPやSAP、人事マスターなど)との連携を見据えたアーキテクチャにしておく必要がある。
- CSV/JSONからの動的インフレ率読み込み:
ハードコーディングされたインフレ率ではなく、総務部門や財務部門が吐き出したCSVファイル、あるいはREST API経由のJSONから当期のインフレ係数を動的に読み込ませることで、予算シミュレーションの信頼性が飛躍的に向上する。
- シミュレーション結果の「What-If」分離:
本番のプロジェクトファイル(`.mpp`)を直接書き換えるのではなく、処理の最初に一時的なバックアップ(または別名保存、あるいは一時的なコストレートテーブルの退避)を行い、ユーザーが「適用」または「破棄」を選べるトランザクション構造に拡張することが、ミッションクリティカルな環境では求められる。
総括
Project VBAはレガシーな技術と侮られがちだが、その背後にあるCOMオブジェクトのライフサイクルとMS Projectの計算エンジンの挙動を完全に掌握していれば、現代のエンタープライズ環境においても極めて強力な自動化・シミュレーション武器となる。
安易なコードに頼らず、メモリとパフォーマンスの限界までチューニングされたコードベースこそが、シニアエンジニアの武器である。現場のシステムに今すぐこの知見を組み込み、圧倒的な処理速度の違いを体感してほしい。
