【テクニカル・上級編】制約条件の変更履歴をVBAでログ出力し、スケジュール変動の責任所在を明確化する – Project VBA解析バイブル

スポンサーリンク

聖域の番人:Project VBAによる制約条件変更履歴の「完全捕捉」

スケジュールは、プロジェクトにおける「契約」そのものである。
Microsoft Projectという巨大な計算エンジンにおいて、タスクの制約条件(ASAP: 可能な限り早く、MFO: 指定日に終了、等)を一つ変更することは、その契約の根幹を揺るがす行為に他ならない。

しかし、標準のMS Projectには「誰が、いつ、なぜ制約を変更したか」を時系列で追跡する堅牢な監査ログ機能が欠落している。無造作なドラッグ&ドロップや、無理解な担当者による制約の固定(Must Finish On等)が、クリティカルパスを沈黙させ、プロジェクトを破綻へと導く。

我々チーフアーキテクトに課せられた使命は、この「ブラックボックス」を排し、計算エンジンの挙動に透明性という名の審判を下すことだ。本稿では、VBAを用いた制約条件の変更監視と、Windows APIを介したアイデンティティの捕捉、そしてメモリ効率を極限まで高めた実装を詳解する。

—

1. アーキテクチャの設計思想:イベント駆動と永続化

MS Projectのタスク変更を捕捉するには、`Project_TaskChange`イベントを利用するのが定石だが、単に値を記録するだけでは不十分だ。

ログの所在

1. Task Custom Fields (Text1-30): 現場の人間が即座に確認できるよう、最新の変更履歴をタスク内に保持する。
2. External Log (CSV/Database): 監査証跡として、不変のログを外部ファイルへパージする。

捕捉すべきメタデータ

  • 変更前後の制約タイプ (`ConstraintType`)
  • 変更前後の制約日 (`ConstraintDate`)
  • OSレベルのユーザー名(Project上のリソース名ではなく、実行環境の権限者)
  • タイムスタンプ(ミリ秒単位の精度)

—

2. Windows APIによる実行主体(Who)の特定

Project内部のリソース名は容易に改ざん可能であり、信頼に値しない。OSのシステムプロセストークンから直接ユーザー名を取得するのが、プロフェッショナルの流儀だ。

‘ — 標準モジュール: modSystemAPI —
Option Explicit

If VBA7 Then
Private Declare PtrSafe Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As LongPtr) As Long
Else
Private Declare Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As Long) As Long
End If

”’

”’ OSレベルのログインユーザー名を取得する
”’

Public Function GetOSUserName() As String
Dim strBuffer As String 255
Dim lngSize As Long
lngSize = 255
If GetUserName(strBuffer, lngSize) <> 0 Then
GetOSUserName = Left(strBuffer, InStr(strBuffer, vbNullChar) – 1)
Else
GetOSUserName = “Unknown”
End If
End Function

—

3. 監視エンジンの実装:Class Moduleによるカプセル化

`ThisProject`に直接ロジックを書き込むのは、素人の仕事だ。イベント監視は専用のクラスモジュールに閉じ込め、オブジェクトのライフサイクルを厳密に管理する。

‘ — クラスモジュール: clsConstraintMonitor —
Option Explicit

Private WithEvents prjApp As MSProject.Application
Private Const LOG_FIELD As Long = pjTaskText30 ‘ 変更履歴を格納するフィールド

Private Sub Class_Initialize()
Set prjApp = MSProject.Application
End Sub

Private Sub Class_Terminate()
Set prjApp = Nothing
End Sub

”’

”’ タスク変更イベントをフックし、制約条件の変動を検知する
”’

Private Sub prjApp_ProjectBeforeTaskChange(ByVal tsk As Task, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
On Error GoTo ErrorHandler

‘ 制約タイプ(pjTaskConstraintType) または 制約日(pjTaskConstraintDate) の変更か
If Field = pjTaskConstraintType Or Field = pjTaskConstraintDate Then
Dim oldVal As String
Dim fieldName As String

‘ 現在値の取得(変更前の状態)
If Field = pjTaskConstraintType Then
fieldName = “ConstraintType”
oldVal = GetConstraintTypeName(tsk.ConstraintType)
Else
fieldName = “ConstraintDate”
oldVal = IIf(tsk.ConstraintDate = “NA”, “NA”, Format(tsk.ConstraintDate, “yyyy/mm/dd HH:mm”))
End If

‘ ログの生成と記録
Call WriteAuditLog(tsk, fieldName, oldVal, CStr(NewVal))
End If

Exit Sub
ErrorHandler:
‘ スケジュール計算エンジンを止めないよう、エラーはサイレントに処理
Debug.Print “Error in Monitor: ” & Err.Description
End Sub

”’

”’ 制約タイプ定数を可読テキストに変換
”’

Private Function GetConstraintTypeName(ByVal ct As Long) As String
Select Case ct
Case pjAsSoonAsPossible: GetConstraintTypeName = “ASAP”
Case pjAsLateAsPossible: GetConstraintTypeName = “ALAP”
Case pjMustFinishOn: GetConstraintTypeName = “MFO”
Case pjMustStartOn: GetConstraintTypeName = “MSO”
Case pjStartNoEarlierThan: GetConstraintTypeName = “SNET”
Case pjStartNoLaterThan: GetConstraintTypeName = “SNLT”
Case pjFinishNoEarlierThan: GetConstraintTypeName = “FNET”
Case pjFinishNoLaterThan: GetConstraintTypeName = “FNLT”
Case Else: GetConstraintTypeName = “Unknown”
End Select
End Function

”’

”’ タスク内カスタムフィールドおよび外部ログへの書き出し
”’

Private Sub WriteAuditLog(ByRef tsk As Task, ByVal fieldName As String, ByVal oldVal As String, ByVal newVal As String)
Dim logEntry As String
Dim userName As String
userName = GetOSUserName()

‘ ログエントリの整形
logEntry = “[” & Format(Now, “yyyy-mm-dd HH:mm”) & “] ” & _
“User: ” & userName & ” | ” & _
fieldName & “: ” & oldVal & ” -> ” & newVal

‘ 1. タスクのText30に履歴を蓄積(最新3件程度を保持するロジック)
tsk.SetField LOG_FIELD, Left(logEntry & vbCrLf & tsk.GetField(LOG_FIELD), 255)

‘ 2. 外部デバッグウィンドウ(またはファイル)へのパージ
Debug.Print “AUDIT: Task ID ” & tsk.ID & ” – ” & logEntry
End Sub

—

4. ライフサイクル管理:メモリの最適化と保守

Project VBAにおいて最も警戒すべきは、「ゾンビ・インスタンス」だ。
アドインやマクロが予期せずメモリに残り続けることで、Projectの終了を妨げたり、計算エンジンを不安定にさせたりする。

`ThisProject`モジュールで、この監視クラスを適切にブートストラップし、解放する必要がある。

‘ — ThisProject —
Option Explicit

Private monitor As clsConstraintMonitor

Private Sub Project_Open(ByVal pj As Project)
‘ 監視エンジンの起動
Set monitor = New clsConstraintMonitor
End Sub

Private Sub Project_BeforeClose(ByVal pj As Project)
‘ オブジェクトの明示的解放。これを怠る者は、リソースリークの責を負う。
Set monitor = Nothing
End Sub

—

5. 極限の知見:なぜ「BeforeTaskChange」なのか

多くの開発者は、変更後の値を記録するために `TaskChange` イベントを使用するが、これでは「変更前の値」を失ってしまう。`ProjectBeforeTaskChange` を使用することで、エンジンが値を書き換える直前のスナップショットを取得できる。

また、大規模なスケジュール(10,000タスク超)において、すべての変更をログ出力すると、I/Oオーバーヘッドにより操作性が著しく低下する。その場合は、`WriteAuditLog` 内で Scripting.FileSystemObject のインスタンスを静的に保持するか、あるいはメモリ上のバッファに蓄積し、`Project_BeforeSave` イベントで一括して物理ディスクへフラッシュする設計へと昇華させるべきだ。

結言

制約条件の変更履歴を追跡することは、単なるログ取りではない。それは、プロジェクトの「論理的整合性」を守るための防壁である。

VBAというレガシーな道具を用いながらも、Windows APIによる厳格な認証情報の取得、クラスによるカプセル化、そしてメモリマネジメントを徹底することで、現代のエンタープライズ環境にも耐えうる監査システムを構築できる。

このコードを組み込んだ瞬間から、あなたのスケジュールは「誰が壊したかわからないパズル」から、「すべての意思決定が記録された信頼の航海図」へと変貌を遂げるだろう。

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