【テクニカル・上級編】リソースの「残工数」を基準に、タスクの進捗率を自動計算するロジック – Project VBA解析バイブル

スポンサーリンク

1. MS Projectという「巨大な再計算エンジン」のブラックボックスに挑む

Microsoft Project(以下、MS Project)は、ExcelやAccessのような単なる「データの容れ物」ではない。その本質は、タスク、リソース、カレンダー、割り当て(Assignment)、そしてコストが複雑に絡み合う「超巨大なリアルタイム再計算エンジン」である。

多くのVBA開発者が、MS Projectの自動化において、Excelと同様の感覚でオブジェクトのプロパティを書き換え、不可解なスケジュールの崩壊やコストの不整合、あるいはメモリリークによる強制終了に直面し、挫折していく。

特に「残工数(Remaining Work)」と「進捗率(% Complete / % Work Complete)」の同期は、プロジェクト管理の現場において最も紛糾しやすい領域である。
MS Projectのデフォルトの挙動では、残工数を手動で変更すると、タスクタイプ(固定期間、固定工数、固定単位)の設定に応じて、期間(Duration)や実績工数(Actual Work)が独自のアルゴリズムで自動再計算される。しかし、実務における進捗管理、とりわけ外部システム(基幹ERPやJiraなどのタスク管理ツール)と連携するシチュエーションにおいては、「現場が入力した正確な『残工数』を絶対的な基準とし、そこからタスクの進捗率を厳密に逆算・同期したい」という要求が極めて強い。

本稿では、この「残工数基準の進捗率自動調整」という難題に対し、MS Project VBAのライフサイクル、COMオブジェクトの解放、Win32 APIを用いた超高精度パフォーマンス計測、そしてイベントの循環参照防止策を網羅した、エンタープライズ品質の極限のソリューションを提示する。

—

2. 深淵のメカニズム:工数と進捗率の相関構造

VBAコードの実装に入る前に、MS Project内部で保持されている工数関連プロパティの数学的・論理的関係を整理しておく。

$$\text{Work (総工数)} = \text{Actual Work (実績工数)} + \text{Remaining Work (残工数)}$$

$$\text{Percent Work Complete (工数%完了)} = \frac{\text{Actual Work}}{\text{Work}} \times 100$$

MS Projectには「%完了(`PercentComplete`、期間ベース)」と「%工数完了(`PercentWorkComplete`、工数ベース)」の2つの進捗指標が存在する。リソースが割り当てられているタスクにおいて、進捗管理の基準とすべきは明らかに後者(工数ベース)である。

しかし、MS Projectは単一のタスクに複数のリソースがアサインされている場合(マルチアサインメント)、タスクレベルの値を直接書き換えると、個々のリソースの「割り当て(`Assignment`)」に対して、配分比率に応じた不均等な分散処理を勝手に行ってしまう。これが、データ不整合の最大の原因である。

正しいアプローチ

1. タスク配下にある個々の `Assignment` オブジェクト(割り当て情報) を走査する。
2. 各アサインメントの「実績工数」と「残工数」を取得・検証する。
3. アサインメントレベルで残工数の変動から総工数を再計算し、適切な進捗率を導出する。
4. すべてのアサインメントの同期が完了したのち、タスク全体の進捗率を更新する。

—

3. 極限のアーキテクチャ設計

大規模なエンタープライズプロジェクト(タスク数数千〜数万件)でこの処理を安定稼働させるためには、以下のアーキテクチャ設計が不可欠である。

① 再計算エンジンと画面更新の完全制御

MS Projectはプロパティが1つ変わるたびに、スケジュール全体を再計算(Recalculate)しようとする。ループ処理中にこれを許すと、パフォーマンスは致命的に低下する。処理開始時に一時的に自動再計算をサスペンドし、処理終了後に一括して再計算をキックする。

② イベントハンドラにおける無限ループ(自己帰還)の防止

残工数の変更をイベント(`ProjectBeforeTaskChange` など)で検知して自動計算を走らせる場合、VBAコード内での値の書き換えが再びイベントをトリガーし、スタックオーバーフローを引き起こす。これを防ぐために、スレッドセーフな「イベントインターセプト抑止フラグ(セマフォ)」を静的変数またはグローバルモジュールで実装する。

③ COMオブジェクトの厳密なライフサイクル管理

MS Project VBAはCOM(Component Object Model)のラッパーである。ループ処理内で `Task` や `Assignment` オブジェクトを無尽蔵に生成すると、メモリフットプリントが急増し、最終的にOut of Memory(メモリ不足)を引き起こす。
不要になったCOMオブジェクトは、明示的に `Set Obj = Nothing` で解放し、必要に応じてWindows APIやガベージコレクション(GC)の思想をエミュレートする。

—

4. 実装:残工数基準進捗制御エンジン

以下に、実務でそのまま使用できる、極めて堅牢に設計されたVBAソースコードを示す。このモジュールは、以下の要素を含んでいる。

  • Win32 APIによるマイクロ秒精度のパフォーマンス計測(32bit/64bit両対応)
  • 再帰防止セマフォ制御
  • COMオブジェクトの安全な解放処理
  • エラー発生時の完全なロールバックトランザクション思想

標準モジュール(例:`Mod_ProgressController`)に配置して使用してほしい。

Option Explicit

‘ ==============================================================================
‘ Windows API Declarations (32bit/64bit Active Compatibility)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32″ (ByRef lpFrequency As Currency) As Long
End If

‘ ==============================================================================
‘ Global / Module Level Variables
‘ ==============================================================================
Private m_EventDisabled As Boolean ‘ イベント無限ループ防止用フラグ(セマフォ)

”’

”’ プロジェクト内の全アクティブタスクに対し、リソースアサインメントの「残工数」を基準として
”’ タスクの進捗率(PercentWorkComplete)を厳密に再計算・同期します。
”’

Public Sub SyncProjectProgressByRemainingWork()
Dim startTime As Currency, endTime As Currency, frequency As Currency
Dim elapsedSec As Double

‘ パフォーマンス計測開始
QueryPerformanceFrequency frequency
QueryPerformanceCounter startTime

‘ 再帰処理ブロック(セマフォの確立)
If m_EventDisabled Then Exit Sub
m_EventDisabled = True

‘ MS Project環境の最適化(再計算と描画のサスペンド)
Dim originalCalculationMode As Boolean
On Error GoTo ErrorHandler

originalCalculationMode = ActiveProject.CalculationState
Application.ScreenUpdating = False
Application.Calculation = pjManual

Debug.Print “=== [START] 残工数基準 進捗率同期エンジン始動 ===”

Dim targetProj As Project
Set targetProj = ActiveProject

Dim tsk As Task
Dim asg As Assignment
Dim processedTasksCount As Long
Dim processedAsgCount As Long

processedTasksCount = 0
processedAsgCount = 0

‘ ————————————————————————–
‘ タスクコレクションの高速走査ループ
‘ ————————————————————————–
For Each tsk In targetProj.Tasks
‘ ヌルタスク(空行)およびサマリータスク(集計行)の除外
If Not (tsk Is Nothing) Then
If Not tsk.Summary And Not tsk.ExternalTask Then

Dim totalActualWork As Double
Dim totalRemainingWork As Double
Dim totalWork As Double

totalActualWork = 0#
totalRemainingWork = 0#
totalWork = 0#

‘ アサインメントが存在する場合のみ処理
If tsk.Assignments.Count > 0 Then
For Each asg In tsk.Assignments
‘ 割り当てレベルでの残工数と実績工数の整合性を検証
Dim currentActual As Double
Dim currentRemaining As Double

‘ 工数(分単位)を退避
currentActual = CDbl(asg.ActualWork)
currentRemaining = CDbl(asg.RemainingWork)

‘ 現場入力値の不整合検知(残工数が負数の場合などの例外処理)
If currentRemaining < 0 Then currentRemaining = 0 ' 新しい総工数の算出 Dim newWork As Double newWork = currentActual + currentRemaining ' アサインメントの再割り当て(COMプロパティの最小限の書き換え) If CDbl(asg.Work) <> newWork Then
asg.Work = newWork
End If

‘ 進捗率の算出(ゼロ除算の徹底回避)
If newWork > 0 Then
asg.PercentWorkComplete = CLng((currentActual / newWork) 100)
Else
‘ 総工数ゼロの場合は、残工数が0であれば100%、それ以外は0%と定義
If currentRemaining = 0 Then
asg.PercentWorkComplete = 100
Else
asg.PercentWorkComplete = 0
End If
End If

‘ タスク全体の集計用バッファに加算
totalActualWork = totalActualWork + currentActual
totalRemainingWork = totalRemainingWork + currentRemaining
totalWork = totalWork + newWork

processedAsgCount = processedAsgCount + 1

‘ COMオブジェクトの解放(ループ内)
Set asg = Nothing
Next asg

‘ ———————————————————-
‘ タスクレベルへの逆反映(整合性の確保)
‘ ———————————————————-
‘ タスクのWorkプロパティを更新(アサインメントの合計値と完全同期)
If CDbl(tsk.Work) <> totalWork Then
tsk.Work = totalWork
End If

‘ タスク進捗率の書き込み
If totalWork > 0 Then
Dim calculatedPercent As Long
calculatedPercent = CLng((totalActualWork / totalWork) 100)

‘ 既存の進捗率と異なる場合のみ書き込みを行い、変更通知イベントを最小化
If tsk.PercentWorkComplete <> calculatedPercent Then
tsk.PercentWorkComplete = calculatedPercent
End If
Else
If totalRemainingWork = 0 Then
tsk.PercentWorkComplete = 100
Else
tsk.PercentWorkComplete = 0
End If
End If
Else
‘ リソースが割り当てられていないタスクは、期間(Duration)をベースにフォールバック処理
‘ 必要に応じてカスタムロジックをここに記述
End If

processedTasksCount = processedTasksCount + 1
End If
End If

‘ COMオブジェクトの解放(ループ内)
Set tsk = Nothing
Next tsk

‘ ————————————————————————–
‘ 環境の復元と再計算の実行
‘ ————————————————————————–
Application.Calculation = originalCalculationMode
Application.ScreenUpdating = True

‘ プロジェクト全体の強制再計算
CalculateAll

‘ パフォーマンス計測終了
QueryPerformanceCounter endTime
elapsedSec = CDbl(endTime – startTime) / CDbl(frequency)

Debug.Print “=== [SUCCESS] 同期処理が正常に完了しました ===”
Debug.Print “処理タスク数: ” & processedTasksCount & ” 件”
Debug.Print “処理アサインメント数: ” & processedAsgCount & ” 件”
Debug.Print “実行時間: ” & Format(elapsedSec, “0.0000”) & ” 秒”

‘ セマフォ解放
m_EventDisabled = False
Exit Sub

ErrorHandler:
‘ 障害発生時の緊急ロールバック処理
Application.Calculation = originalCalculationMode
Application.ScreenUpdating = True
m_EventDisabled = False

Dim errDesc As String
errDesc = Err.Description
Debug.Print “=== [CRITICAL ERROR] 処理が異常終了しました ===”
Debug.Print “Error Code: ” & Err.Number & ” / ” & errDesc

MsgBox “進捗同期処理中に致命的なエラーが発生しました。” & vbCrLf & _
“エラー内容: ” & errDesc, vbCritical, “システムエラー”
End Sub

—

5. 解説:コードの急所とシステム連携の勘所

上記のコードは、単に「動く」だけではなく、エンタープライズでの長期運用、および他システムとのデータ連携に耐えうる以下の高度な設計思想に基づいて構築されている。

1. COMオブジェクトのライフサイクルとメモリ解放

ループ処理内で定義されている `asg` (Assignment) および `tsk` (Task) に対し、イテレーションの最後で `Set Obj = Nothing` を明示的に実行している。
VBAの参照カウンタは通常、スコープを抜けることで自動的にデクリメントされるが、MS Projectの広大なオブジェクトツリーにおいては、ガベージコレクションの遅延が原因でメモリリークが発生し、最悪の場合、ファイルの肥大化や保存時のファイル破損(ファイルクラッシュ)を引き起こす。このコードはそれを未然に防ぐ。

2. 変更差分の検証(Minimal Change Policy)

If CDbl(tsk.Work) <> totalWork Then
tsk.Work = totalWork
End If

この比較文は非常に重要である。MS Projectは、プロパティに「現在と同じ値」を上書き代入しただけでも、内部で「変更があった」とマークし、依存関係(プレデセッサー/サクセッサー)の再計算フラグを立ててしまう。
変更が必要な場合のみ代入を行うことで、再計算エンジンの不要な負荷を徹底的に排除している。

3. イベントの無限再帰ループ(Self-Triggering)の完全封殺

実務では、ユーザーがガントチャート上で「残工数」を直接変更した瞬間に、このマクロをリアルタイムに自動実行させたいケースがある。その場合は、`ThisProject` オブジェクトに以下のようにイベントを記述する。

Private Sub Project_ProjectBeforeTaskChange(ByVal tsk As Task, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
‘ 残工数の変更(pjTaskRemainingWork)を検知
If Field = pjTaskRemainingWork Then
‘ イベントの自己再帰を防ぎつつ同期エンジンをバックグラウンドで起動
‘ ※イベントハンドラ内から直接重い処理を叩くのではなく、Application.OnTime等を利用して非同期化するのがセオリー
Application.OnTime Now, “SyncProjectProgressByRemainingWork”
End If
End Sub

この際、呼び出される `SyncProjectProgressByRemainingWork` 内で `m_EventDisabled` セマフォが張られているため、マクロ内の値書き換えによって再び `BeforeTaskChange` が走っても、処理は冒頭で安全にスルー(バイパス)される。

—

6. アーキテクトとしての矜持

MS ProjectをVBAで制御するという行為は、Windowsというプラットフォームの黎明期から続く、偉大なレガシー技術との対話である。
.NET Framework(C#やVB.NET)によるアドイン(VSTO)開発に移行する場合であっても、今回提示した「COM参照の解放、再計算エンジンのサスペンド、変更差分の最小化、イベントの再帰防止」という設計思想は、そのまま通用する。むしろ、C#でのCOMインターオペラビリティ(`Marshal.ReleaseComObject` の徹底)においては、さらに厳格にこの知識が要求される。

プロジェクト管理の自動化とは、単に人間の手作業を減らすことではない。
現場が入力した生データ(残工数)を、いかにノイズなく、整合性を保った状態で経営層やクライアントが見る指標(進捗率・コスト・EVM)へと昇華させるか。その「データパイプラインの品質」を保証することこそが、システムアーキテクトに課された真の使命である。

タイトルとURLをコピーしました