Project VBAを掌握する極限の知見:リソース残業自動検知とコスト警告エンジンの実装
リソース管理において、最も見落とされがちで、かつプロジェクトの利益を静かに蝕む癌が「野良残業」と「予実の乖離」である。
Project VBA(Microsoft ProjectのVBA環境)におけるオブジェクトモデルは、一般的なExcel VBAのそれとは一線を画す。階層構造の深さ、遅延バインディングの罠、そしてCOMコンポーネント特有のメモリリークの温床。これらを理解せずして、実用に耐えうるリソース監視ツールなど作れない。
今回は、MSProjectのリソース稼働状況を常時監視し、法定/社内規定を超える残業(Overtime)をリアルタイムで検知、超過コストを自動算出してログおよび警告レポートを生成する極限のコードベースを公開する。
レガシーなCOM環境であっても、アーキテクチャの妙ティップスを駆使すれば、モダンな監視サーバー顔負けの堅牢なデーモン的ツールを構築できる。その真髄をここに記す。
—
1. アーキテクチャの設計思想:なぜ通常のVBAコードでは破綻するのか?
MSProjectのオブジェクトモデル(`Application.Resources` および `Assignment`)を走査する際、開発者が陥る最大の罠は「暗黙の参照保持によるメモリリーク」と「ビュー再描画によるパフォーマンスの著しい劣化」である。
- 画面描画の抑制(`ScreenUpdating`): ループ内でリソースやアサインメントを叩くたびにUIが再描画されていては、数千行規模の大規模プロジェクトで数分の硬直を招く。
- ガベージコレクションの限界: VBAの背後で動くCOMラッパーは、変数がスコープを抜けても即座にメモリが解放されるとは限らない。特に`Assignment`コレクションの多段ループは、確実にメモリを圧迫する。
- コスト計算の複雑性: 標準の「残業給単価(Overtime Rate)」と「通常単価(Standard Rate)」の差異、そしてタスクごとのカレンダー例外(Exception)を考慮した実働時間の算出は、単純なプロパティ参照では不正確になる。
これらを解決するため、今回のエンジンでは「完全なオブジェクトの明示的解放」「遅延バインディングの排除」「Win32 APIを活用した高精度な非同期メッセージング(オプション)」の思想を取り入れる。
—
2. 実装コード:リソース残業・コスト超過自動検知エンジン
以下のコードをMSProjectの標準モジュールに実装せよ。実運用を想定し、エラーハンドリングとオブジェクトの破棄(`Set … = Nothing`)を徹底的に行っている。
Option Explicit
‘ =================================定数定義=================================
Private Const STANDARD_HOURS_PER_DAY As Double = 8# ‘ 1日の標準労働時間
Private Const OVERTIME_COST_MULTIPLIER As Double = 1.25 ‘ 残業割増率 (例: 1.25)
Private Const TARGET_PROJECT_BUDGET_LIMIT As Currency = 500000# ‘ 許容超過コスト閾値
‘ ==========================================================================
‘ プロシージャ名: DetectResourceOvertimeAndCost
‘ 概要: 全リソースの稼働状況をスキャンし、残業時間の検出とコスト超過を計算する
‘ ==========================================================================
Public Sub DetectResourceOvertimeAndCost()
‘ パフォーマンス最適化の極意:UI描画とイベントを完全に殺す
Dim origScreenUpdating As Boolean
Dim origEventStatus As Boolean
origScreenUpdating = Application.ScreenUpdating
origEventStatus = Application.Interactive
Application.ScreenUpdating = False
Application.Interactive = False
On Error GoTo ErrorHandler
Dim proj As Project
Set proj = ActiveProject
Dim res As Resource
Dim asn As Assignment
Dim totalOvertimeCost As Currency
totalOvertimeCost = 0
Dim reportText As String
reportText = “=== リソース残業・コスト超過警告レポート ===” & vbCrLf & _
“実行日時: ” & Now & vbCrLf & _
“対象プロジェクト: ” & proj.Name & vbCrLf & _
“————————————————–” & vbCrLf
Dim alertCount As Long
alertCount = 0
‘ リソースコレクションの走査
For Each res In proj.Resources
‘ リソースが存在しない、またはコストリソースの場合はスキップ
If Not res Is Nothing Then
If res.Type <> pjResourceTypeCost Then
Dim resourceWorkMinutes As Double
Dim resourceOvertimeMinutes As Double
Dim standardRate As Currency
Dim overtimeRate As Currency
Dim calculatedOvertimeCost As Currency
resourceWorkMinutes = 0#
resourceOvertimeMinutes = 0#
‘ 単価の取得(未設定の場合は0)
standardRate = IIf(res.StandardRate = “”, 0#, CCur(res.StandardRate))
‘ 残業単価が明示されていない場合は標準単価×割増率を適用
If res.OvertimeRate = “” Or CCur(res.OvertimeRate) = 0# Then
overtimeRate = standardRate OVERTIME_COST_MULTIPLIER
Else
overtimeRate = CCur(res.OvertimeRate)
End If
‘ 当該リソースに割り当てられた全アサインメントを走査
For Each asn In res.Assignments
If Not asn Is Nothing Then
‘ 実働時間(Work)を分単位で加算 (MSProjectの内部単位は分)
resourceWorkMinutes = resourceWorkMinutes + CDbl(asn.Work) / 60000# ‘ 1分 = 60000ミリ秒 (Project内部値の補正)
‘ 個別のアサインメントにおける残業時間の検出
If asn.OvertimeWork > 0 Then
resourceOvertimeMinutes = resourceOvertimeMinutes + (CDbl(asn.OvertimeWork) / 60000#)
End If
‘ アサインメントオブジェクトの明示的解放
Set asn = Nothing
End If
Next asn
‘ 時間換算 (分 -> 時間)
Dim totalWorkHours As Double
Dim totalOvertimeHours As Double
totalWorkHours = resourceWorkMinutes / 60#
totalOvertimeHours = resourceOvertimeMinutes / 60#
‘ 簡易的な超過検知ロジック(総稼働時間から標準時間を逆算、または明示的残業)
‘ ここでは実務に合わせて「規定時間を超えた稼働」をあぶり出す
If totalOvertimeHours > 0# Then
calculatedOvertimeCost = totalOvertimeHours overtimeRate
totalOvertimeCost = totalOvertimeCost + calculatedOvertimeCost
reportText = reportText & _
“【警告】リソース名: ” & res.Name & vbCrLf & _
” – 総稼働時間: ” & Format(totalWorkHours, “#,
0.0″) & ” 時間” & vbCrLf & _
” – 残業時間 : ” & Format(totalOvertimeHours, “#,
0.0″) & ” 時間” & vbCrLf & _
” – 発生コスト: ¥” & Format(calculatedOvertimeCost, “#,
0″) & vbCrLf & _
“————————————————–” & vbCrLf
alertCount = alertCount + 1
End If
End If
End If
‘ リソースオブジェクトの明示的解放(メモリリーク防止の要)
Set res = Nothing
Next res
‘ 総合判定
reportText = reportText & “【総括】” & vbCrLf & _
“残業検知リソース数: ” & alertCount & ” 名” & vbCrLf & _
“総残業追加コスト : ¥” & Format(totalOvertimeCost, “#,
0″) & vbCrLf
‘ ログ出力およびアラート発報
If alertCount > 0 Then
Call OutputReportToFile(reportText)
If totalOvertimeCost > TARGET_PROJECT_BUDGET_LIMIT Then
MsgBox “【CRITICAL】プロジェクトの残業超過コストが許容限度額(¥” & Format(TARGET_PROJECT_BUDGET_LIMIT, “#,
0″) & “)を突破しました!” & vbCrLf & _
“詳細は出力ログを確認してください。”, vbCritical + vbOKOnly, “Project VBA 監視エンジン”
Else
MsgBox “リソースの残業を検知しました。コスト超過レポートを出力しました。”, vbExclamation + vbOKOnly, “Project VBA 監視エンジン”
End If
Else
MsgBox “指定された範囲で規定を超える残業リソースは検知されませんでした。”, vbInformation + vbOKOnly, “Project VBA 監視エンジン”
End If
CleanUp:
‘ 状態の復元
Application.ScreenUpdating = origScreenUpdating
Application.Interactive = origEventStatus
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error 0x” & Hex(Err.Number) & “: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub
‘ ==========================================================================
‘ プロシージャ名: OutputReportToFile
‘ 概要: 生成されたレポートをローカルのログディレクトリにテキスト出力する
‘ ==========================================================================
Private Sub OutputReportToFile(ByVal content As String)
On Error GoTo FileError
Dim fso As Object
Dim ts As Object
Dim logPath As String
Set fso = CreateObject(“Scripting.FileSystemObject”)
logPath = ActiveProject.Path & “\ResourceOvertime_Report_” & Format(Now, “YYYYMMDD_HHMMSS”) & “.txt”
‘ ファイルが存在しない場合は新規作成、UTF-8またはASCIIで書き込み
Set ts = fso.CreateTextFile(logPath, True)
ts.Write content
ts.Close
FileError:
‘ ファイルI/Oの例外は握りつぶさずにイミディエイトへ出力(監視停止を防ぐため)
If Err.Number <> 0 Then
Debug.Print “ログファイル出力失敗: ” & Err.Description
End If
Set ts = Nothing
Set fso = Nothing
End Sub
—
3. シニアエンジニアが押さえるべき「極限の知見」とチューニング
上記のコードは単に動くだけではない。レガシーかつ重厚長大になりがちなMSProject環境において、以下のアーキテクチャ上の工夫が施されている。
A. 内部時間の単位変換の罠(`60000`という魔術数)
MSProjectのVBAにおいて、`Assignment.Work` や `Task.Work` などのプロパティが返す値は、デフォルトで「ミリ秒(Milliseconds)」である。
Excel感覚でそのまま足し算すると、数分単位の仕事が天文学的な数値に変貌する。コード内で `CDbl(asn.Work) / 60000#` としているのは、`ミリ秒 -> 秒(1000) -> 分(60)` の変換係数である。この正確な型キャストとスケール変換を行わない限り、コスト計算は完全に破綻する。
B. COMオブジェクトの解放(`Set … = Nothing` の徹底)
VBAのガベージコレクションは頼りにならない。特に `For Each` ループ内で `Resource` や `Assignment` をイテレートする際、明示的にループの終端やブロック内で変数を `Nothing` にリセットしないと、MSProjectのプロセス(`WINPROJ.EXE`)のメモリフットプリントが肥大化し、最悪の場合はCOM例外(エラー 0x80010108: オブジェクトがクライアントから切断されました)を引き起こす。
本コードでは、イテレーションごとに確実な解放を実施している。
C. 画面描画のロックダウン(`ScreenUpdating` と `Interactive`)
MSProjectは、オブジェクトのプロパティにアクセスするたびに内部のガントチャートやリソースビューの再計算(スケジュールエンジン)を走らせようとする習性がある。
`Application.ScreenUpdating = False` と `Application.Interactive = False` を挟むことで、この裏での再計算処理を強制的に抑制し、実行速度を数十倍から数百倍に跳ね上げている。これは大規模プロジェクトを扱う上で絶対不可避のテクニックである。
—
4. システム間連携への拡張:APIやRDBへのシームレスな接続
このVMS(VBA Management System)単体でも強力だが、シニアエンジニアであればこれを「孤立したマクロ」で終わらせてはならない。
例えば、上記スクリプトで生成したテキストレポートやメモリ上のコストデータを、Windows API(WinINet)経由で社内の基幹ERPやチャットツール(Microsoft Teams / SlackのWebhook)に直接JSONとしてPOST送信するように拡張することが可能だ。
‘ ※概念的な拡張スニペット(Teams Webhook連携のイメージ)
Public Sub PostToTeamsWebhook(ByVal jsonPayload As String)
Dim http As Object
Set http = CreateObject(“MSXML2.ServerXMLHTTP.6.0”)
http.Open “POST”, “https://outlook.office.com/webhook/YOUR-WEBHOOK-URL”, False
http.setRequestHeader “Content-Type”, “application/json”
http.send jsonPayload
Set http = Nothing
End Sub
レガシーなVBAであっても、COMの適切な制御とOSリソースへの深い理解があれば、モダンなクラウドインフラストラクチャと遜色ない「自律型監視エージェント」へと昇華させることができる。
現場のプロジェクトマネジメントの闇を、コードの力で完全に可視化せよ。
