MS Projectの闇を断つ:`Project_BeforeTaskChange`で実現する鉄壁の変更監査ログ設計
プロジェクトマネジメントの現場において、最も恐ろしい瞬間は何だろうか。
それは、「誰が、いつ、どのタスクの期限や工数を勝手に書き換えたのか分からない」というカオスな状態に陥ったときだ。進捗遅延の理由を追おうにも、スケジュールがサイレントに変更されていては、プロジェクトの歴史は闇の中である。
Excel VBAであればワークシートの変更検知は容易だが、Microsoft Project(以下、MSP)の世界では話が別だ。MSP特有のオブジェクトモデル、そしてイベントの挙動の癖を理解していないと、パフォーマンスが極端に低下するか、あるいは無限ループや予期せぬクラッシュの悪夢を見る。
今回は、Project VBAのイベント駆動(`Project_BeforeTaskChange`)を極限までチューニングし、実務の現場でそのまま稼働できる「堅牢な監査ログ自動記録システム」の設計思想と実装コードを伝授する。
—
1. 現場のエンジニアが陥る「よくあるアンチパターン」
まず、初心者がやりがしな非効率なアプローチを指摘しておこう。
- ポーリング(定期実行)による監視:
タイマーで定期的にタスクプロパティをスキャンする方法。CPUリソースを無駄に消費するだけでなく、変更の瞬間を正確に捉えられない。
- イベント内での重いI/O処理:
イベントハンドラ内で直接外部の巨大なExcelファイルやデータベースへ同期書き込みを行う。これではユーザーがタスク名を1文字変更するたびにフリーズし、実用に耐えない。
- 例外処理の欠落:
MSPのイベントは、エラーハンドリングを怠るとアプリケーション全体の強制終了(サイレントクラッシュ)を引き起こす。
我々が目指すべきは、「MSPのネイティブなイベントを正確に捕らえ、メモリ上で安全にキューイングし、非同期または軽量に外部出力する」というプロダクション品質のアーキテクチャだ。
—
2. アーキテクチャ設計:イベント駆動の核心
MSPのオブジェクトモデルにおいて、タスクの変更を検知するイベントには主に以下のものがある。
- `Project_BeforeTaskChange`: タスクの値が変更される直前に発火する。
- `Project_TaskChange`: タスクの値が変更された後に発火する。
【チーフアーキテクトの知見】
変更前後の値を比較して監査ログ(「誰が」「何を」「何から何へ」変えたか)を残す場合、`BeforeTaskChange`で「変更前の値」を退避させ、実際の記録は`TaskChange`または変数保持で行うのが最も安全かつ確実である。
今回は、最も実用的なアプローチとして、`Project_BeforeTaskChange`イベントを用いて変更をキャッチし、同一セッション内のメモリに保持、または即座に外部のCSV/ログファイルへ安全にアペンド(追記)する実装を行う。
—
3. プロダクションコード:実用監査ログモジュール
このコードは、MSPのThisProjectモジュールに配置する。
実務を想定し、エラーハンドリング、変更前後の値の差分検知、およびマルチユーザー環境を見据えたユーザー情報の取得を網羅している。
実装手順
1. MSPファイルを開く。
2. `Alt + F11` でVBE(Visual Basic Editor)を起動する。
3. 左側のプロジェクトエクスプローラから `ThisProject` をダブルクリックする。
4. 以下のコードを貼り付ける。
‘ Option Explicitを必ず記述し、暗黙の変数の宣言を防ぐ(基本中の基本)
Option Explicit
‘ 変更前の値を一時保持する変数
Private m_OldTaskName As String
Private m_OldTaskStart As Date
Private m_OldTaskFinish As Date
Private m_OldTaskWork As Double
Private Sub Project_BeforeTaskChange(ByVal t As Task, ByVal Field As PjField, ByVal NewValue As Variant, Cancel As Boolean)
‘ エラーハンドリングの盾を張る
On Error GoTo ErrorHandler
‘ タスクが存在しない、またはサマリータスク等の特殊ケースを除外
If t Is Nothing Then Exit Sub
If t.Summary Then Exit Sub
‘ 変更対象のフィールドに応じ、変更前の状態をグローバル変数(モジュールレベル)に退避
‘ ※Project_TaskChangeイベントはFieldを引数に持たないため、ここで捕まえるのが定石
Select Case Field
Case pjTaskName
m_OldTaskName = t.Name
Call WriteAuditLog(t, “タスク名”, m_OldTaskName, CStr(NewValue))
Case pjTaskStart
m_OldTaskStart = t.Start
Call WriteAuditLog(t, “開始日”, Format(m_OldTaskStart, “yyyy/mm/dd”), Format(CDate(NewValue), “yyyy/mm/dd”))
Case pjTaskFinish
m_OldTaskFinish = t.Finish
Call WriteAuditLog(t, “終了日”, Format(m_OldTaskFinish, “yyyy/mm/dd”), Format(CDate(NewValue), “yyyy/mm/dd”))
Case pjTaskWork
m_OldTaskWork = t.Work / 60 ‘ 分単位から時間に変換
Call WriteAuditLog(t, “工数(H)”, CStr(m_OldTaskWork), CStr(CDbl(NewValue) / 60))
End Select
Exit Sub
ErrorHandler:
‘ ログ記録の失敗でMSP自体の操作を止めてはならないため、イミディエイトに吐き出して継続
Debug.Print “【監査ログエラー】” & Err.Description
End Sub
‘ ==============================================================================
‘ ログ出力コアプロシージャ
‘ ==============================================================================
Private Sub WriteAuditLog(ByVal t As Task, ByVal fieldName As String, ByVal oldValue As String, ByVal newValue As String)
Dim logPath As String
Dim fileNum As Integer
Dim userName As String
Dim projectFileName As String
‘ ログファイルの出力先(例: プロジェクトファイルと同じ階層の “AuditLog.csv”)
On Error Resume Next
projectFileName = ActiveProject.FullName
If projectFileName = “” Then
logPath = Environ(“TEMP”) & “\MSP_AuditLog.csv”
Else
logPath = Left(projectFileName, InStrRev(projectFileName, “\”)) & “MSP_AuditLog.csv”
End If
On Error GoTo 0
‘ Windowsのログインユーザー名を取得(誰が変更したか)
userName = Environ(“USERNAME”)
‘ FreeFile関数で安全にファイル番号を取得
fileNum = FreeFile
‘ CSV形式で追記モード(Append)でオープン
Open logPath For Append As #fileNum
‘ フォーマット: タイムスタンプ, プロジェクト名, ユーザー名, タスクID, タスク名, 変更項目, 変更前, 変更後
Print #fileNum, Format(Now, “yyyy/mm/dd hh:nn:ss”) & “,” & _
“[” & ActiveProject.Name & “],” & _
userName & “,” & _
t.ID & “,” & _
“””” & Replace(t.Name, “”””, “”””””) & “””,” & _
fieldName & “,” & _
“””” & oldValue & “””,” & _
“””” & newValue & “”””
Close #fileNum
End Sub
—
4. コードの解説と「プロのこだわり」
このコードがなぜ「実務中級以上」の現場で耐えうるのか、その設計思想を解説する。
1. `Environ(“USERNAME”)` による正確な誰がの特定
MSPの標準機能には厳密な変更者トラッキング機能がない場合が多い(SharePoint/Project Server連携を除く)。Windowsのログインセッションからユーザー名を取得することで、ローカル運用であっても「誰が変更したか」の証跡を担保できる。
2. CSVアペンディングによるI/Oの軽量化
Excelオブジェクトを毎回立ち上げて書き込む方式は、数千行のプロジェクトでは耐え難い遅延を生む。`Open … For Append` によるテキストストリーム書込は圧倒的に高速であり、MSPの操作性を損なわない。
3. ダブルクォーテーションのエスケープ処理
タスク名の中にカンマ(`,`)やダブルクォーテーション(`”`)が含まれていた場合、CSVが破損する。`Replace(t.Name, “”””, “”””””)` により、CSVの構造破壊を完全に防御している。
4. 堅牢なエラーハンドリング
ファイルサーバーの権限問題などでログ書き込みに失敗した場合でも、`On Error GoTo ErrorHandler` がトラップし、`Resume Next` 的に処理を流すことで、「ログが残らないことはあっても、ユーザーのスケジュール編集作業が妨げられることはない」という、システムとしての正しいフォールバックを実現している。
—
5. 運用上の注意点とさらなる高みへ
- マクロの有効化とセキュリティ:
組織で運用する場合、このMSPファイルは「信頼できる場所」に置くか、デジタル署名を付与する必要がある。マクロが無効化されている環境ではイベントは発火しない。
- データベース連携への拡張:
今回はローカルのCSV出力に留めているが、`WriteAuditLog` プロシージャ内部のロジックを ADODB (ActiveX Data Objects) に書き換えれば、SQL ServerやPostgreSQLなどの社内DBへリアルタイムに監査ログを飛ばすことも容易に可能だ。
最後に
ツールや自動化の本質は、「人のミスを防ぎ、不正や無駄の温床をなくすこと」にある。
今回紹介した `Project_BeforeTaskChange` を活用した監査ログの仕組みは、あなたのプロジェクトを「言った言わない」の泥沼から救い出し、完全なトレーサビリティをもたらす強力な武器となるはずだ。
プロフェッショナルなエンジニアとして、ぜひ現場のガバナンス向上に役立ててほしい。
