MS Projectバイナリ肥大化を撃滅する:保存イベントに介入するガベージコレクションとスキーマ最適化アーキテクチャ
大規模なエンジニアリングプロジェクトや長期運用されるプラント建設、基幹システム開発現場において、Microsoft Project(以下、MS Project)のポータビリティ低下とパフォーマンス劣化は深刻な技術的負債となります。
特に、数千タスクを超える`.mpp`ファイルにおいて「保存するたびにファイルサイズが非線形で増大する」「プロジェクトの起動やファイル保存に数十秒を要する」といった事態の裏には、単なるタスクデータの蓄積にとどまらない原因が存在します。それは「UI操作の裏で残存する孤立タスク(Hidden/Transient Tasks)」、そして「テンプレートや別ファイルからのコピー&ペースト時に自動増殖するカスタムビュー・テーブル・フィルタの浮遊オブジェクト」です。
本稿では、MS Project VBAのCOMオブジェクトライフサイクルを深掘りし、ファイル保存(`ProjectBeforeSave`)イベントを契機として、これらの不要なデータを自動的かつ安全にパージする「軽量化・クリーンアップ自動化スクリプト」の構築手法を解説します。
—
1. MS Projectにおけるファイルサイズ肥大化のメカニズム
MS Projectの内部データ構造(`.mpp`バイナリ)は、独自のオブジェクトリレーショナル構造を持っています。ExcelやWordと異なり、ビュー(View)、テーブル(Table)、フィルタ(Filter)、グループ(Group)といったUI表現の定義が、プロジェクトファイル内部にデータベースのスキーマのように永続化されます。
[ .mpp ファイル内部の主要参照構造 ]
│
├── Tasks Collection ─── [ Active Tasks ]
│ └── [ Orphan/Transient Tasks ] ←★肥大化の原因1
│
├── Views Collection ─── [ Built-in Views ]
│ └── [ Custom Views (複製物) ]
│
└── Tables Collection ── [ Built-in Tables ]
└── [ Temp/Unused Tables ] ←★肥大化の原因2
肥大化を引き起こす2大要因
1. 一事象(Transient)タスクおよびフラグ削除漏れ
外部システム(基幹ERPやJira等)との同期スクリプトや中間計算用に使用された一次タスク(`Flag20`などのフラグが付与された隠しタスク)が、処理異常や不完全なトランザクションによってパージされずに残存するケース。
2. カスタムスキーム(View/Table/Filter)の交差汚染
別の`.mpp`ファイルからタスクをコピー&ペーストした際、MS Projectは貼付元に紐づくカスタムテーブルやフィルタを自動的にローカルオブジェクトとして取り込みます。これが長年の運用で「`コピー (1) の ガント チャート`」のように増殖し、ファイルサイズの肥大化とメモリ割り当ての劣化を引き起こします。
これらを解決するには、ファイルがディスクに書き込まれる直前のタイミングで、メモリおよびストレージ上の不要オブジェクトを決定論的(Deterministic)に破棄する設計が必要です。
—
2. アーキテクチャ設計:イベントトラップと安全な削除プロシージャ
自動クリーンアップの実装にあたっては、以下の3つの技術的要件を満たす必要があります。
1. `ProjectBeforeSave` イベントの捕捉: クラスモジュールを用いたアプリケーションレベルのイベント監視。
2. UI操作ロックと画面更新停止(Win32 API): 削除処理に伴う描画オーバーヘッドの撲滅。
3. 安全な逆順ループとActiveオブジェクトの保護: 現在アクティブなビューや組み込み(Built-in)定義を誤って破棄しない例外ハンドリング。
コンポーネント構成
本システムは、2つのモジュールで構成します。
- `clsProjectEvents` (Class Module): イベントハンドラ。保存処理をフックし、クリーンアップロジックを呼び出す。
- `mdl_ProjectOptimizer` (Standard Module): Win32 API定義、削除ロジック、メモリ解放処理を保持するコアエンジン。
—
3. 完全実装コード
以下に、そのまま運用環境に組み込み可能な完全なVBAコードを示します。Win32 API宣言は64bit(`PtrSafe`)環境に対応しています。
【モジュール1】`clsProjectEvents` (クラスモジュール)
Option Explicit
‘ MS Project Applicationオブジェクトのイベントを捕獲
Private WithEvents App As MSProject.Application
Private Sub Class_Initialize()
Set App = MSProject.Application
End Sub
Private Sub Class_Terminate()
Set App = Nothing
End Sub
‘ ファイル保存直前に割り込むイベントハンドラ
Private Sub App_ProjectBeforeSave(ByVal pj As Project, ByVal SaveAsUi As Boolean, Cancel As Boolean)
On Error GoTo ErrorHandler
‘ 自動クリーンアップエンジンの実行
Call mdl_ProjectOptimizer.OptimizeProjectFile(pj)
Exit Sub
ErrorHandler:
‘ 保存処理そのものを阻害しないよう、エラーログのみ記録して継続
Debug.Print “[ERROR] ProjectBeforeSave Optimization Failed: ” & Err.Description
End Sub
【モジュール2】`mdl_ProjectOptimizer` (標準モジュール)
Option Explicit
‘ ——————————————————————————–
‘ Win32 API 宣言 (描画制御およびメモリ最適化用)
‘ ——————————————————————————–
If VBA7 Then
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Private Declare PtrSafe Function GetActiveWindow Lib “user32” () As LongPtr
Private Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Private Declare Function LockWindowUpdate Lib “user32” (ByVal hwndLock As Long) As Long
Private Declare Function GetActiveWindow Lib “user32” () As Long
Private Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Private Declare Function GetCurrentProcess Lib “kernel32” () As Long
End If
‘ 孤立・隠しタスクを識別するためのフラグ定義 (例: Flag20を一時タスク属性とする)
Private CONST TRANSIENT_TASK_FLAG As Long = pjTaskFlag20
‘ ——————————————————————————–
‘ メイン最適化プロシージャ
‘ ——————————————————————————–
Public Sub OptimizeProjectFile(ByRef targetProj As Project)
Dim hwnd As LongPtr
Dim originalAlerts As Boolean
If targetProj Is Nothing Then Exit Sub
‘ パフォーマンス最適化: 画面更新の停止とアラートの抑制
On Error Resume Next
hwnd = GetActiveWindow()
LockWindowUpdate hwnd
originalAlerts = Application.DisplayAlerts
Application.DisplayAlerts = False
On Error GoTo 0
Err.Clear
On Error GoTo CleanupExit
‘ 1. 不要タスク (隠し・一時フラグ付き) のクリーンアップ
Call PurgeTransientTasks(targetProj)
‘ 2. 未使用のカスタムテーブルのパージ
Call PurgeUnusedTables(targetProj)
‘ 3. 未使用のカスタムフィルタのパージ
Call PurgeUnusedFilters(targetProj)
‘ 4. メモリ領域の強制コンパクション (Working Setのトリミング)
Call TrimProcessMemory
CleanupExit:
‘ UI描画の再開と設定の復元
Application.DisplayAlerts = originalAlerts
LockWindowUpdate 0
‘ ガベージコレクションを確定させるための明示的処理
DoEvents
End Sub
‘ ——————————————————————————–
‘ サブプロシージャ: 一時/孤立タスクのパージ
‘ ——————————————————————————–
Private Sub PurgeTransientTasks(ByRef pj As Project)
Dim i As Long
Dim tsk As Task
‘ コレクション要素の削除は必ず「逆順(Step -1)」で実行する
‘ 正順では削除時にインデックスがシフトし、COM要素の参照エラーを引き起こす
For i = pj.Tasks.Count To 1 Step -1
Set tsk = pj.Tasks(i)
If Not (tsk Is Nothing) Then
‘ Flag20がTrueに設定されている一次計算用タスクを破棄
‘ または、名前が空で期間が0の完全な孤立ゴミタスクを検知
If tsk.GetField(TRANSIENT_TASK_FLAG) = “Yes” Or _
(Trim$(tsk.Name) = “” And tsk.Duration = 0 And tsk.ResourceNames = “”) Then
tsk.Delete
End If
End If
‘ 明示的COMオブジェクト参照解除
Set tsk = Nothing
Next i
End Sub
‘ ——————————————————————————–
‘ サブプロシージャ: 不要なカスタムテーブルの自動削除
‘ ——————————————————————————–
Private Sub PurgeUnusedTables(ByRef pj As Project)
Dim tbl As Table
Dim i As Long
Dim activeTableName As String
‘ 現在のアクティブテーブル名を取得(削除対象から外すため)
On Error Resume Next
activeTableName = Application.ActiveProject.CurrentTable
On Error GoTo 0
‘ タスクテーブルの走査
For i = pj.TaskTables.Count To 1 Step -1
Set tbl = pj.TaskTables(i)
If Not (tbl Is Nothing) Then
‘ 組み込み(BuiltIn)ではなく、かつアクティブでないカスタムテーブルを検出
If Not tbl.BuiltIn And tbl.Name <> activeTableName Then
‘ 「コピー」の接頭辞を持つか、特定命名規則に合致する不要テーブルを破棄
If InStr(1, tbl.Name, “コピー”, vbTextCompare) > 0 Or _
InStr(1, tbl.Name, “Copy of”, vbTextCompare) > 0 Then
On Error Resume Next
pj.TaskTables(tbl.Name).Delete
On Error GoTo 0
End If
End If
End If
Set tbl = Nothing
Next i
End Sub
‘ ——————————————————————————–
‘ サブプロシージャ: ゴミフィルタのパージ
‘ ——————————————————————————–
Private Sub PurgeUnusedFilters(ByRef pj As Project)
Dim flt As Filter
Dim i As Long
Dim activeFilterName As String
On Error Resume Next
activeFilterName = Application.ActiveProject.CurrentFilter
On Error GoTo 0
For i = pj.TaskFilters.Count To 1 Step -1
Set flt = pj.TaskFilters(i)
If Not (flt Is Nothing) Then
If Not flt.BuiltIn And flt.Name <> activeFilterName Then
‘ 複製されたカスタムフィルタの削除
If InStr(1, flt.Name, “コピー”, vbTextCompare) > 0 Or _
InStr(1, flt.Name, “Copy of”, vbTextCompare) > 0 Then
On Error Resume Next
pj.TaskFilters(flt.Name).Delete
On Error GoTo 0
End If
End If
End If
Set flt = Nothing
Next i
End Sub
‘ ——————————————————————————–
‘ サブプロシージャ: プロセスメモリの解放 (Win32 API)
‘ ——————————————————————————–
Private Sub TrimProcessMemory()
On Error Resume Next
‘ MS Projectプロセスのワーキングセットを切り詰め、物理メモリの断片化を解消
Call SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1)
On Error GoTo 0
End Sub
【モジュール3】自動読み込みの初期化 (標準モジュール)
上記イベントを常時有効化するために、プロジェクトオープン時にクラスをインスタンス化します。
Option Explicit
Public g_ProjectEvents As clsProjectEvents
‘ プロジェクトオープン時、またはVBA初期化時に実行
Public Sub RegisterEventHook()
If g_ProjectEvents Is Nothing Then
Set g_ProjectEvents = New clsProjectEvents
Debug.Print “[INIT] MS Project Save-Hook Registered Successfully.”
End If
End Sub
‘ 補足: ThisProject (Project_Open) に以下を記述して自動起動する
‘ Private Sub Project_Open(ByVal pj As Project)
‘ Call RegisterEventHook
‘ End Sub
—
4. 現場運用における技術的留意点と限界
本スクリプトをエンタープライズ環境(Project Server / Project Onlineを含む)へ導入するにあたっては、以下の点に注意してください。
① 逆順ループ(Step -1)の絶対性
COMコレクション(`Tasks`, `Tables`, `Filters`等)から要素を削除する場合、絶対条件として末尾からの逆順ループを行ってください。正順(`1 To Count`)でループを回しながら`.Delete`を実行すると、コレクションのインデックスが動的に詰められ、後続の要素アクセス時に `0x800A03EC`(COM参照エラー)または不完全なループ終了を引き起こします。
② Global.mpt(グローバルテンプレート)汚染の防止
MS Projectには、ローカルファイルで作成・変更したビューやテーブルを共通テンプレートである`Global.mpt`に自動書き込みする機能が存在します。不要なオブジェクトのパージロジックを動かす際は、チーム全体で「ビューの自動再定義」オプションがオフになっているか確認してください。さもなければ、ローカルで消去したカスタムテーブルが次回ファイル起動時に`Global.mpt`から再補填されるという無限ループに陥ります。
③ COMオブジェクトの確定論的解放
VBAは参照カウント型のガベージコレクション(GC)を採用しています。ループ内部で割り当てた `Task` や `Table` オブジェクト変数は、ループの末尾で明示的に `Set obj = Nothing` を実行しない限り、内部参照カウントが残り、バイナリ上のメモリブロックが解放されません。特に大量のタスクを高速処理する場合、この一行がメモリフットプリントの増大を抑制する境界線となります。
④ Win32 APIによるワーキングセットの最適化
コード末尾で呼び出している `SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1)` は、Windows OSに対して当該プロセスの物理メモリ(ワーキングセット)を最小化し、ページファイルへの再配置および空き領域の解放を促す強力なAPIコールです。大量のオブジェクト削除を行った直後にこれを呼ぶことで、MS Project内部のメモリ断片化を即座に解消し、ファイル保存時のI/Oパフォーマンスを最大化できます。
—
5. まとめ
MS Projectファイルの肥大化は、適切なクリーンアップロジックを組み込むことで100%制御可能です。
本稿で提示した構造は、イベント駆動型のフック機構により、ユーザーに特別な操作を意識させることなく、ファイルサイズの健全性を維持します。レガシーなシステム環境や、大規模なMS Project運用を行うシステム管理者・アーキテクトの方は、ぜひ本アプローチをプロジェクト・インフラの標準基盤として組み込んでみてください。
