【テクニカル・上級編】Applicationオブジェクトのイベントハンドラで実現する「プロジェクト操作のログ記録」自動化 – Project VBA解析バイブル

スポンサーリンク

MS Projectを掌握せよ:Applicationオブジェクトのイベント駆動型・監査ロギングアーキテクチャ

レガシーシステムの最前線において、Microsoft Project(MS Project)は依然として巨大な要塞である。数千行に及ぶWBS、複雑なリソース割り当て、クリティカルパスの変動――。プロジェクトマネジメントのデータは、企業にとって機密そのものであり、同時に「誰が、いつ、どこを改変したのか」というトレーサビリティ(監査証跡)の担保が最も困難な領域でもある。

ExcelやAccessであれば標準的な変更履歴機能やシート保護が機能するものの、MS Projectのネイティブな変更履歴機能は極めて冗長であり、エンタープライズレベルの厳格なコンプライアンス要件には耐えられない。

本稿では、Project VBAの核心である `Application` オブジェクトのイベントハンドラ を完全に手なずけ、システムリソースを極限まで消費させずに、誰が・いつ・どのプロジェクトファイルをどう変更したかを別系統のストレージ(CSV/外部DB)へ自動記録する、堅牢な監査ロギングツールの実装手法を解説する。

1. Project VBAにおけるイベントの罠と「真のライフサイクル」

多くのVBAエンジニアは、`Workbook_Open` や `Worksheet_Change` と同じ感覚でProjectのイベントを実装し、メモリリークやイベントの多重発火の泥沼にハマる。

MS Projectのオブジェクトモデルにおいて、イベントを捉えるには クラスモジュール が不可欠である。特に `Application` オブジェクトのイベントをフックする場合、以下のアーキテクチャ上の制約を理解していなければならない。

  • グローバルインスタンスの維持: イベントを監視するクラス変数は、標準モジュールのスコープアウト(消滅)と共にイベント検知を停止する。
  • イベントの連鎖(Cascade): ロギング処理の中でプロジェクトプロパティを書き換えると、それが新たな `Project_Change` イベントを引き起こし、無限ループに陥る。
  • マルチプロジェクト環境の脆弱性: MS Projectは複数のプロジェクトを同時に開くことができる(MDIアーキテクチャの残滓)。どのプロジェクトでイベントが発生したかを厳密に識別しなければ、ログが破損する。

これらを解決するための設計図が、以下の実装である。

2. 実装:イベント駆動型・監査ロギングシステム

以下のコードは、アドイン(`.mpp` または `.dll` / `.xla` 相当のグローバルテンプレート)として常駐し、開かれたすべてのプロジェクトのライフサイクルと変更を監視するアーキテクチャの核心部である。

① クラスモジュール:`CAppEvents`

まずはアプリケーションレベルのイベントを捉えるクラスを作成する。

‘ ==============================================================================
‘ クラスモジュール名: CAppEvents
‘ 役割: MS ProjectのApplicationイベントをフックし、監査ログを記録する
‘ ==============================================================================
Option Explicit

‘ WithEventsキーワードにより、Applicationのイベントをキャプチャする
Public WithEvents App As MSProject.Application

‘ イベントハンドラの多重発火を防ぐためのガードフラグ
Private isLogging As Boolean

‘ — プロジェクトオープン時 —
Private Sub App_ProjectOpen(ByVal Sv As Project)
If isLogging Then Exit Sub
Call WriteAuditLog(Sv, “PROJECT_OPEN”, “プロジェクトが開かれました。”)
End Sub

‘ — プロジェクトクローズ前 —
Private Sub App_ProjectBeforeClose(ByVal Sv As Project, ByRef Cancel As Boolean)
If isLogging Then Exit Sub
‘ 必要に応じて変更未保存のチェックや強制保存のロジックをここに挟む
Call WriteAuditLog(Sv, “PROJECT_CLOSE”, “プロジェクトが閉じられます。”)
End Sub

‘ — プロジェクト保存時 —
Private Sub App_ProjectBeforeSave(ByVal Sv As Project, ByRef Cancel As Boolean)
If isLogging Then Exit Sub
Call WriteAuditLog(Sv, “PROJECT_SAVE”, “プロジェクトが保存されました。”)
End Sub

‘ — 変更検知(タスク・リソース等の変更) —
Private Sub App_ProjectChange(ByVal Sv As Project)
‘ 自身がログ書き込み中の変更であれば無視(無限ループ防止)
If isLogging Then Exit Sub

isLogging = True
On Error GoTo ErrorHandler

‘ ここで変更差分の検知ロジックを走らせる(今回は簡易的にイベント名のみ)
Call WriteAuditLog(Sv, “PROJECT_CHANGE”, “プロジェクトデータに変更が加えられました。ズレ・遅延の発生に注意。”)

ErrorHandler:
isLogging = False
If Err.Number <> 0 Then
Debug.Print “AuditLog Error: ” & Err.Description
End If
End Sub

‘ — ログ書き込みのコアロジック(Windows APIおよびファイルIO) —
Private Sub WriteAuditLog(ByRef prj As Project, ByVal actionType As String, ByVal description As String)
Dim logPath As String
Dim fileNum As Integer
Dim userName As String

‘ ネットワーク上の共有監査ログパス(環境に合わせて変更)
logPath = “C:\ProjectAuditLogs\AuditTrail_” & Format(Date, “YYYYMM”) & “.csv”

‘ 実行ユーザーの取得(Windows APIまたはEnviron環境変数)
userName = Environ$(“USERNAME”)
If userName = “” Then userName = “UNKNOWN_USER”

‘ ファイル番号の取得(排他制御を考慮した安全なI/O)
fileNum = FreeFile

On Error GoTo FileError
Open logPath For Append As #fileNum

‘ 記録フォーマット: タイムスタンプ, ユーザー名, PC名, アクション, プロジェクト名, パス, 詳細
Print #fileNum, Format(Now, “YYYY-MM-DD HH:NN:SS”) & “,” & _
Chr$(34) & userName & Chr$(34) & “,” & _
Chr$(34) & Environ$(“COMPUTERNAME”) & Chr$(34) & “,” & _
Chr$(34) & actionType & Chr$(34) & “,” & _
Chr$(34) & prj.Name & Chr$(34) & “,” & _
Chr$(34) & prj.FullName & Chr$(34) & “,” & _
Chr$(34) & description & Chr$(34)

Close #fileNum
Exit Sub

FileError:
‘ ログ書き込み失敗時に本体の動作を止めてはならないため、エラーはイミディエイト出力に留める
Debug.Print “Critical: Audit log write failed. Error ” & Err.Number & “: ” & Err.Description
If fileNum > 0 Then Close #fileNum
End Sub

② 標準モジュール:`MAppInitializer`

このクラスのインスタンスをメモリ上に常駐させ、ライフサイクルを担保するためのエントリーポイント。

‘ ==============================================================================
‘ 標準モジュール名: MAppInitializer
‘ 役割: アドイン起動時にイベント監視クラスを初期化し、メモリに維持する
‘ ==============================================================================
Option Explicit

Private GlobalAppEvents As CAppEvents

‘ グローバルイベントの初期化(Global.mppのAuto_Openやアドイン起動時に実行)
Public Sub InitializeAuditSystem()
If GlobalAppEvents Is Nothing Then
Set GlobalAppEvents = New CAppEvents
Set GlobalAppEvents.App = VB.Application ‘ または Application オブジェクト

MsgBox “プロジェクト監査ロギングシステムが正常に稼働を開始しました。”, vbInformation, “Audit System”
End If
End Sub

‘ システムの停止とオブジェクトの明示的解放
Public Sub TerminateAuditSystem()
If Not GlobalAppEvents Is Nothing Then
Set GlobalAppEvents.App = Nothing
Set GlobalAppEvents = Nothing
End If
End Sub

3. シニアエンジニアが押さえるべき「極限の知見」とパフォーマンス最適化

このアーキテクチャを実運用に乗せるにあたり、通常のVBA開発の常識を超えた「システム負荷の最小化」と「フォールトトレランス」の考慮が必要となる。

1. `Project_Change` の重さとスロットリング

MS Projectの `Project_Change` は、ユーザーが1つのタスクの日付を変更しただけでも、依存関係(リンク)によって連鎖的に数十・数百のタスクが再計算された結果として発火する。
もしこのイベント内で重い処理(データベースへの直接ODBC接続や、巨大なXMLパース)を行うと、UIスレッドがブロックされ、ユーザーの操作性が致命的に悪化する。

  • 対策: 上記コードの通り、ログ出力は軽量なファイルI/O(CSV追記)に留め、重い処理(集計やBIツール連携)は夜間バッチや外部常駐サービス(C#.NET等で作成したファイルウォッチャー)に非同期で委譲する設計が望ましい。

2. メモリリークとCOMオブジェクトの解放

VBAのガベージコレクションは参照カウント方式である。`CAppEvents` クラス内で `Project` オブジェクトや `Task` オブジェクトをローカル変数として保持し続けたり、不適切に参照を残したりすると、プロジェクトを閉じた後もメモリ上に残骸(ゾンビオブジェクト)が残り、最終的にMS Project全体の強制終了を引き起こす。

  • 対策: イベントハンドラ内で取得したオブジェクトは極力ローカルスコープにとどめ、プロシージャの終了と共に自動解放させる。クラスの破棄時には必ず `Set GlobalAppEvents.App = Nothing` を明示的に実行する。

3. レガシー環境・マルチユーザー競合の回避

全社共通のグローバルテンプレート (`Global.mpp`) から各クライアントへ配布する場合、ネットワーク上の単一のCSVファイルに対して複数ユーザーが一斉に書き込みを行うと、ファイルアクセスの競合エラー(Error 70: 書き込み権限がありません / Permission Denied)が発生する。

  • 対策:

1. ログの保存先はローカルの隠しフォルダ(例: `C:\ProgramData\CompanyProjectAudit\`)に一旦出力し、Windowsタスクスケジューラ等で夜間に集約サーバーへ吸い上げる。
2. または、エラーハンドリング内で `Resume Next` を用いて数ミリ秒のウェイトを挟みながらリトライする堅牢なキューイング機構を実装する。

総括

MS Projectの自動化とガバナンス統制は、単なるマクロの記述ではない。オブジェクトモデルのライフサイクルを正しく理解し、アプリケーションの挙動の裏で何が起きているかを俯瞰できる「アーキテクトの視点」があって初めて成立する。

今回紹介したイベント駆動型ロギングは、アドインとして全クライアントにサイレント導入することが可能であり、企業のプロジェクト管理における「誰がいつ何を変えたか分からない」というブラックボックスを完全に排除する強力な武器となる。

レガシーな技術と侮るなかれ。VBAの深淵を極めた者だけが、強固なエンタープライズ・アーキテクチャを構築できるのだ。

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