Project VBAを完全掌握:保存時に「不要カスタムオブジェクト&ゴーストタスク」を自動破壊する軽量化アーキテクチャ
Microsoft Project(以下、MS Project)を運用する開発現場で、誰もが一度は直面する怪現象があります。
「何も大きなデータを追加していないのに、なぜか `.mpp` ファイルサイズが数MBから数十MBへと肥大化し、保存や開く動作が耐えがたいほど重くなる」
この問題の真因は、ユーザーが無意識に繰り返す「一時的なビューの切り替え」「フィルタの適用」「不要な行の作成・放置」によって、プロジェクトファイル内部にゴミオブジェクト(不要なカスタムテーブル、ビュー、フィルタ、未定義のゴーストタスク)が蓄積し続けることにあります。
今回は、単なる気休めのマクロではなく、ファイル保存(`ProjectBeforeSave`)イベントをフックし、プロジェクトの整合性を保ちながら軽量化を自動実行するプロダクションレベルのVBAスクリプトを解説します。
—
1. なぜ.mppファイルは肥大化し、クラッシュするのか?
MS Projectのオブジェクトモデルは、Excelとは異なり「表示(View)」「構造(Table)」「抽出(Filter)」が複雑に密結合しています。
他人の `.mpp` ファイルからタスクをコピペした瞬間、またはビューを少し変更して保存した瞬間に、内部では以下の暗黒面が動いています。
1. カスタムテーブル・ビューの自動増殖
「コピー 1 – タスク一覧」のような不要なテーブルやビューが裏で生成され、リレーションが維持されたまま残存する。
2. ゴーストタスク(Ghost Tasks)の残留
ユーザーがEnterキーを連打して作成し、名前を空欄のまま放置したタスクや、完全に削除されずオブジェクト参照だけが残った空行が、内部インデックスを圧迫する。
3. グローバルテンプレート(Global.mpt)の汚染
個別のファイルで増殖した不要オブジェクトが `Global.mpt` へ逆流入し、組織全体へ「低速化のウイルス」として感染拡大する。
これらを解消するには、「ファイル保存時」に必要なクリーンアップロジックを割り込ませる(Hook)のが最も合理的かつ確実なアプローチです。
—
2. 堅牢なイベントフック構造の設計
Project VBAで保存イベント(`ProjectBeforeSave`)を扱う際、素人が最も陥りがちなバグが「無限ループ」と「コンテキストの喪失」です。
クリーンアップ処理の中でタスクを削除・変更すると、それが原因で再度イベントがキックされたり、保存処理そのものがキャンセルされるリスクがあります。これを回避するために、以下の3重防護アーキテクチャを採用します。
1. イベント抑止フラグの制御:クリーンアップ中はアプリケーションイベントを一時停止する。
2. 逆順ループ(Backward Loop)の実装:コレクション要素を削除する際は、必ず末尾からインデックスを減算しながら処理する。
3. エンタープライズ領域の保護:Project Online / Project Server環境下で、グローバルオブジェクトを破壊しない保護フィルターを実装する。
—
3. 実装:完全自動クリーンアップ・コード
以下のコードをVBAエディタに組み込みます。構成はクラスモジュール1つと標準モジュール1つ、そしてThisProjectです。
1. クラスモジュール: `clsProjectEvents`
アプリケーションレベルのイベントを捕捉します。
‘ ==============================================================================
‘ クラス名: clsProjectEvents
‘ 概要 : MS Project アプリケーションイベントのキャプチャクラス
‘ 役割 : 保存イベントをフックし、自動クリーンアップ処理を安全にキックする
‘ ==============================================================================
Option Explicit
Public WithEvents App As MSProject.Application
Private Sub App_ProjectBeforeSave(ByVal pj As Project, ByVal SaveAsUi As Boolean, Cancel As Boolean)
On Error GoTo ErrorHandler
‘ 読み取り専用ファイル、またはアクティブでないプロジェクトは対象外
If pj.ReadOnly Then Exit Sub
‘ イベントの二重発火を防止するため、描画とアラートを一時停止
App.ScreenUpdating = False
App.DisplayAlerts = False
‘ ————————————————————————–
‘ クリーンアップエンジンの実行
‘ ————————————————————————–
Call Mdl_ProjectCleaner.ExecuteEngine(pj)
ExitHandler:
App.ScreenUpdating = True
App.DisplayAlerts = True
Exit Sub
ErrorHandler:
‘ ログをイミディエイトウィンドウに出力(必要に応じてファイルログへ変更可)
Debug.Print “[ERROR] Event Hook Failed: ” & Err.Number & ” – ” & Err.Description
Resume ExitHandler
End Sub
2. 標準モジュール: `Mdl_ProjectCleaner`
実際の削除・軽量化ロジックを担うエンジン部です。
‘ ==============================================================================
‘ モジュール名: Mdl_ProjectCleaner
‘ 概要 : プロジェクトファイル軽量化・ゴミデータ除去エンジン
‘ 設計要件 :
‘ 1. 空白タスク(ゴーストタスク)の安全な削除
‘ 2. 一時的/コピーによって増殖した不要テーブル・フィルタの破棄
‘ 3. 組み込みオブジェクトおよびエンタープライズ要素の完全保護
‘ ==============================================================================
Option Explicit
‘ 不要オブジェクトを特定するための接頭辞・パターン定義
Private Const TEMP_PREFIX_1 As String = “コピー”
Private Const TEMP_PREFIX_2 As String = “Copy of”
Private Const TEMP_PREFIX_3 As String = “tmp_”
”’
”’
Public Sub ExecuteEngine(ByRef targetProj As Project)
If targetProj Is Nothing Then Exit Sub
Debug.Print “— クリーンアップ開始: ” & targetProj.Name & ” —”
‘ 1. ゴーストタスクの除去
PurgeGhostTasks targetProj
‘ 2. 不要カスタムテーブルの除去
PurgeCustomTables targetProj
‘ 3. 不要カスタムフィルタの除去
PurgeCustomFilters targetProj
Debug.Print “— クリーンアップ完了: ” & targetProj.Name & ” —”
End Sub
”’
”’
Private Sub PurgeGhostTasks(ByRef pj As Project)
Dim t As Task
Dim i As Long
Dim deletedCount As Long
deletedCount = 0
‘ コレクション削除は必ず「末尾から逆順」にループする(インデックスズレ防止)
For i = pj.Tasks.Count To 1 Step -1
Set t = pj.Tasks(i)
‘ タスクが存在し、かつ完全に「空」であるかを判定
If Not (t Is Nothing) Then
‘ 名前が空白、かつ実体定義のないタスク(サマリータスクは除外)
If Trim(t.Name) = “” And t.Assignments.Count = 0 And t.PredecessorTasks.Count = 0 Then
‘ ベースラインが設定されている場合は事故防止のため削除しない
If t.BaselineStart = “NA” And t.Work = 0 Then
t.Delete
deletedCount = deletedCount + 1
End If
End If
End If
Next i
Debug.Print “削除されたゴーストタスク数: ” & deletedCount
End Sub
”’
”’
Private Sub PurgeCustomTables(ByRef pj As Project)
Dim tbl As Table
Dim i As Long
Dim deletedCount As Long
deletedCount = 0
‘ TaskTablesコレクションの走査
For i = pj.TaskTables.Count To 1 Step -1
Set tbl = pj.TaskTables(i)
‘ 組み込み(Built-in)テーブルではなく、かつ命名規則に合致する一時テーブルを判定
If Not tbl.BuiltIn Then
If IsTemporaryName(tbl.Name) Then
On Error Resume Next ‘ 表示中のテーブル削除エラーを回避
pj.TaskTables(i).Delete
If Err.Number = 0 Then
deletedCount = deletedCount + 1
End If
On Error GoTo 0
End If
End If
Next i
Debug.Print “削除された不要テーブル数: ” & deletedCount
End Sub
”’
”’
Private Sub PurgeCustomFilters(ByRef pj As Project)
Dim f As Filter
Dim i As Long
Dim deletedCount As Long
deletedCount = 0
For i = pj.TaskFilters.Count To 1 Step -1
Set f = pj.TaskFilters(i)
If Not f.BuiltIn Then
If IsTemporaryName(f.Name) Then
On Error Resume Next
pj.TaskFilters(i).Delete
If Err.Number = 0 Then
deletedCount = deletedCount + 1
End If
On Error GoTo 0
End If
End If
Next i
Debug.Print “削除された不要フィルタ数: ” & deletedCount
End Sub
”’
”’
Private Function IsTemporaryName(ByVal objName As String) As Boolean
Dim cleanName As String
cleanName = Trim(objName)
If InStr(1, cleanName, TEMP_PREFIX_1, vbTextCompare) = 1 Or _
InStr(1, cleanName, TEMP_PREFIX_2, vbTextCompare) = 1 Or _
InStr(1, cleanName, TEMP_PREFIX_3, vbTextCompare) = 1 Then
IsTemporaryName = True
Else
IsTemporaryName = False
End If
End Function
3. ThisProject モジュール
プロジェクト起動時にイベント監視を開始させます。
‘ ==============================================================================
‘ モジュール名: ThisProject
‘ 概要 : ブック起動時にイベントハンドラを自動イニシャライズする
‘ ==============================================================================
Option Explicit
Private evtHandler As clsProjectEvents
Private Sub Project_Open(ByVal pj As Project)
‘ イベントハンドラの実体化とアプリ参照のバインド
Set evtHandler = New clsProjectEvents
Set evtHandler.App = MSProject.Application
Debug.Print “ProjectSaveCleaner: イベント監視を開始しました。”
End Sub
Private Sub Project_BeforeClose(ByVal pj As Project)
‘ メモリリーク防止のための明示的解放
Set evtHandler.App = Nothing
Set evtHandler = Nothing
End Sub
—
4. プロダクション運用の極意:アーキテクトが押さえるべき注意点
このスクリプトを実務ツールとして全社・チーム展開する場合、以下の設計思想を必ず理解しておいてください。
① インデックスズレを100%防ぐ「逆順ループ」
`pj.Tasks` や `pj.TaskTables` などのコレクションを `For Each` や `For i = 1 To Count` で削除していくのは初級者の典型的なミスです。要素を削除した瞬間に全体のインデックスが1つずつ繰り上がるため、処理がスキップされたり「インデックスが範囲外です」のエラーで停止します。「末尾から1に向かって `Step -1` で走査する」のが絶対原則です。
② ベースライン設定済みタスクの不可侵原則
本コードでは、名前が空のゴーストタスクであっても `t.BaselineStart <> “NA”`(すでにベースラインが記録されている)場合は削除対象から除外しています。プロジェクト管理において「過去に合意された計画のログ」をVBAが勝手に消去することは厳禁です。
③ Enterprise(Project Server / Online)環境との親和性
`pj.TaskTables(i).BuiltIn` のチェックを入れているのは、組み込みのシステムテーブル(「標準」「サマリー」など)を誤って削除し、Project全体のUIを破壊するのを防ぐためです。エンタープライズカスタムフィールドが紐づくテーブルもこの判定によって安全に保護されます。
—
5. まとめ
VBAによる業務自動化の真価は、「ボタンを押したら動く」ことではありません。「ユーザーに意識させず、システムが裏で品質を担保し続ける」状態を作ることです。
今回紹介したイベント駆動型クリーンアップアーキテクチャを既存のプロジェクトテンプレート(`.mpt` または `.mpp`)に組み込んでおくことで、ファイルサイズの肥大化、動作鈍化、クラッシュによるデータ消失リスクを未然に遮断できます。
コードをコピペして組み込み、イミディエイトウィンドウで削除ログが刻まれる快感をぜひ体感してください。エンジニアリングの力で、現場のストレッサーを根本から排除しましょう。
