Project VBAを掌握する極限の知見:保存時の不要定義一括削除によるファイル軽量化の最適化
私は長年にわたり、Project VBAシステムやレガシーアーキテクチャの最前線に立ち、数多のプロジェクトファイルを解析し、その挙動を最適化してきました。今日の主題は、一見すると地味ながら、その影響がシステム全体のパフォーマンスと安定性に深く関わる極めて重要なテーマです。すなわち、「Projectファイルの保存時に、不要なカスタムビュー、フィルタ、グループを一括削除してファイルを極限まで軽量化する最適化」。これは単なるファイルサイズ削減に留まらない、プロジェクト運用の健全性を保つための本質的なアプローチです。
Projectファイルの肥大化と性能劣化のメカニズム
Microsoft Projectファイル(.mpp)は、単なるデータコンテナではありません。それは、タスク、リソース、割り当てといったプロジェクト固有のデータに加え、ビュー、フィルタ、グループ、カレンダー、カスタムフィールド、VBAモジュールといったプロジェクトの「定義体」をも内包する、複雑なOLE Compound Document形式のファイルです。
この定義体が、時間の経過と共にプロジェクトファイルを肥大化させる主要因となります。
1. 一時的な定義の生成: ユーザーが一時的に特定の情報を見るために作成したカスタムビューやフィルタが、プロジェクトを閉じる際に保存されてしまう。
2. テンプレートからの継承: 組織のグローバルテンプレート(Global.mpt)や他のプロジェクトファイルから、意図せず多くの定義がコピーされてしまう。
3. 開発プロセスにおける残留物: 開発者がテスト目的で作成したVBAモジュールや、デバッグ用の定義が残存する。
4. バージョン管理の欠如: 定義体に変更を加えるたびに、古い定義が削除されずに残り続ける。
これらの定義体は、プロジェクトデータそのものよりもはるかに小さな情報量に見えるかもしれません。しかし、ファイル内での格納方式、インデックスの再構築、そしてアプリケーションがこれらの定義を読み込み、メモリ上に展開するプロセスにおいて、無視できないオーバーヘッドを生み出します。特に、数百、数千のタスクを持つ大規模プロジェクトにおいて、これらの余剰な定義は以下の形で性能劣化を引き起こします。
- ファイルオープン・保存時間の顕著な増大: 定義体の読み書きは、ディスクI/OとCPU処理を消費します。
- メモリ使用量の増加: Projectアプリケーションが起動し、プロジェクトファイルを開く際に、全ての定義体がメモリにロードされます。これは、特にメモリが制約される環境下や、複数のProjectインスタンスを扱う場面で深刻な問題となります。
- UI応答性の低下: ビューの切り替え、フィルタの適用、グループ化の操作など、定義体を参照するUI操作の度に、内部的な処理が遅延します。
- ネットワーク経由でのアクセスボトルネック: WAN経由でプロジェクトファイルを開く際、肥大化したファイルは転送時間を増加させ、ユーザー体験を損ないます。
- Project Server/Project Onlineとの同期オーバーヘッド: オンライン環境への発行や同期時にも、不要な定義体がデータ転送量を増やし、同期処理の負荷を高めます。
この問題は、単に「遅い」というユーザーの不満に繋がるだけでなく、システム全体の安定性を損ない、時にはアプリケーションのクラッシュさえ誘発する可能性を秘めています。
最適化戦略の核心:不要な定義体の特定とVBAによる制御
我々が目指すのは、プロジェクトのコアデータに影響を与えることなく、その周縁部に蓄積された「澱」を徹底的に排除することです。Project VBAは、この目的を達成するための強力なツールを提供します。`Project`オブジェクトの`Views`、`Filters`、`Groups`コレクションを通じて、これらの定義体にプログラム的にアクセスし、削除することが可能です。
しかし、闇雲に削除するわけにはいきません。「不要」とは何かを厳密に定義し、誤って必要な定義を削除しないための堅牢なロジックを組み込む必要があります。
- 組み込み定義の保護: Projectに標準で備わっている定義は削除してはなりません。これらは`BuiltIn`プロパティで識別できます。
- 共有定義の考慮: プロジェクトによっては、複数のファイル間で共有されるカスタム定義が存在する場合があります。これらの定義は、削除することで他のプロジェクトに影響を与える可能性があるため、慎重な判断が必要です。本稿では、プロジェクトファイル内のローカルなカスタム定義に焦点を当てますが、システム全体を管理する立場であれば、この共有の概念を深く理解する必要があります。
- 使用状況の判断: 究極的には、その定義が現在使用されているか、将来使用される可能性があるか、という判断は難しいものがあります。しかし、例えば「メニューに表示されない(`ShowInMenu = False`)」「名称が一時的なもの(例: “Copy of…”)」といったヒントを手がかりに、削除候補を絞り込むことは可能です。
VBAによる実装:保存時の自動クリーンアップ
Projectファイルを保存する直前に、この最適化処理を自動実行するのが最も効果的なアプローチです。`Application_ProjectBeforeSave` イベントハンドラを利用することで、ユーザーが意識することなくクリーンアップが実行され、常に最適な状態のファイルが維持されます。
以下に、この目的を達成するためのVBAコードを示します。コードは、堅牢性、パフォーマンス、そして保守性を最大限に考慮して設計されています。
‘ 標準モジュール(例: Module1)に記述
Option Explicit
‘ ==============================================================================
‘ Windows API宣言:ファイルサイズを正確に取得するため
‘ FileLen関数は2GBの壁があるため、64bit環境でも動作するGetFileSizeExを使用します。
‘ これにより、最適化の効果を数値で確認する基盤とします。
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function GetFileSizeEx Lib “kernel32” (ByVal hFile As LongPtr, lpFileSize As LARGE_INTEGER) As Long
Private Declare PtrSafe Function CreateFile Lib “kernel32” Alias “CreateFileA” ( _
ByVal lpFileName As String, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As Long, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As LongPtr) As LongPtr
Private Declare PtrSafe Function CloseHandle Lib “kernel32” (ByVal hObject As LongPtr) As Long
Else
Private Declare Function GetFileSizeEx Lib “kernel32” (ByVal hFile As Long, lpFileSize As LARGE_INTEGER) As Long
Private Declare Function CreateFile Lib “kernel32” Alias “CreateFileA” ( _
ByVal lpFileName As String, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As Long, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long) As Long
Private Declare Function CloseHandle Lib “kernel32” (ByVal hObject As Long) As Long
End If
‘ API呼び出しに必要な定数と構造体
Private Const GENERIC_READ = &H80000000
Private Const FILE_SHARE_READ = &H1
Private Const OPEN_EXISTING = 3
Private Type LARGE_INTEGER
LowPart As Long
HighPart As Long
End Type
‘ ==============================================================================
‘ 機能:プロジェクトファイルの保存直前に不要な定義をクリーンアップする
‘ 目的:ファイルの肥大化を防ぎ、パフォーマンスを維持する
‘ ==============================================================================
Public Sub CleanProjectDefinitions(ByVal prj As Project)
‘ 処理開始前にファイルサイズを取得し、最適化の効果を比較できるようにする
Dim lInitialFileSize As Currency
On Error Resume Next ‘ ファイルパス取得失敗時のエラーを無視
lInitialFileSize = GetProjectFileSize(prj.FullName)
On Error GoTo 0 ‘ エラーハンドラをリセット
Application.StatusBar = “プロジェクト定義を最適化しています…”
Application.ScreenUpdating = False ‘ 画面更新を停止し、処理を高速化
Application.EnableEvents = False ‘ イベント発生を一時停止し、無限ループなどを防ぐ
On Error GoTo ErrorHandler
‘ === 各種定義の削除処理を呼び出す ===
Call RemoveUnusedViews(prj)
Call RemoveUnusedFilters(prj)
Call RemoveUnusedGroups(prj)
‘ === 処理後のクリーンアップ ===
CleanUp:
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.StatusBar = “”
‘ 処理後にファイルサイズを再取得し、効果をログ出力またはメッセージボックスで通知
Dim lFinalFileSize As Currency
On Error Resume Next
lFinalFileSize = GetProjectFileSize(prj.FullName)
On Error GoTo 0
If lInitialFileSize > 0 And lFinalFileSize > 0 Then
Debug.Print “— Project Optimization Report —”
Debug.Print “Initial File Size: ” & Format(lInitialFileSize, “#,
0″) & ” bytes”
Debug.Print “Final File Size : ” & Format(lFinalFileSize, “#,
0″) & ” bytes”
Debug.Print “Reduced by : ” & Format(lInitialFileSize – lFinalFileSize, “#,
0″) & ” bytes”
Debug.Print “———————————–”
Else
Debug.Print “ファイルサイズの取得に失敗しました。”
End If
Exit Sub
ErrorHandler:
MsgBox “プロジェクト定義のクリーンアップ中にエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical
GoTo CleanUp ‘ クリーンアップ処理へジャンプ
End Sub
‘ ==============================================================================
‘ 機能:不要なカスタムビューを削除する
‘ ==============================================================================
Private Sub RemoveUnusedViews(ByVal prj As Project)
Dim vw As View
Dim i As Long
Dim lRemovedCount As Long
‘ コレクションを逆順にループすることで、削除によるインデックスのずれを防ぐ
For i = prj.Views.Count To 1 Step -1
Set vw = prj.Views.Item(i)
‘ 組み込みビューは削除しない
If Not vw.BuiltIn Then
‘ ここに削除ロジックを記述。例:
‘ – 名前が特定のパターンに一致する (例: “Temporary View “)
‘ – メニューに表示されない (vw.ShowInMenu = False)
‘ – 使用頻度が低い (これはProject VBAだけでは判断が難しい)
‘ 今回は「組み込みではないもの」を削除対象の第一基準とする
‘ より厳密な判断が必要な場合は、ユーザーに確認を求めるなどのロジックを追加
‘ Debug.Print “削除候補ビュー: ” & vw.Name
‘ 以下の条件に合致すれば削除
‘ 例: “Copy of “で始まるビュー、または特定の文字列を含むビューなど
If InStr(1, vw.Name, “Copy of “, vbTextCompare) > 0 Or _
InStr(1, vw.Name, “一時”, vbTextCompare) > 0 Then ‘ 例として「一時」という名前のビュー
vw.Delete
lRemovedCount = lRemovedCount + 1
‘ Debug.Print “削除済みビュー: ” & vw.Name
End If
End If
Next i
Debug.Print “削除されたカスタムビュー数: ” & lRemovedCount
End Sub
‘ ==============================================================================
‘ 機能:不要なカスタムフィルタを削除する
‘ ==============================================================================
Private Sub RemoveUnusedFilters(ByVal prj As Project)
Dim flt As Filter
Dim i As Long
Dim lRemovedCount As Long
For i = prj.Filters.Count To 1 Step -1
Set flt = prj.Filters.Item(i)
‘ 組み込みフィルタは削除しない
If Not flt.BuiltIn Then
‘ 削除ロジック。例:
‘ – 名前が特定のパターンに一致する
If InStr(1, flt.Name, “Copy of “, vbTextCompare) > 0 Or _
InStr(1, flt.Name, “一時”, vbTextCompare) > 0 Then
flt.Delete
lRemovedCount = lRemovedCount + 1
End If
End If
Next i
Debug.Print “削除されたカスタムフィルタ数: ” & lRemovedCount
End Sub
‘ ==============================================================================
‘ 機能:不要なカスタムグループを削除する
‘ ==============================================================================
Private Sub RemoveUnusedGroups(ByVal prj As Project)
Dim grp As Group
Dim i As Long
Dim lRemovedCount As Long
For i = prj.Groups.Count To 1 Step -1
Set grp = prj.Groups.Item(i)
‘ 組み込みグループは削除しない
If Not grp.BuiltIn Then
‘ 削除ロジック。例:
‘ – 名前が特定のパターンに一致する
If InStr(1, grp.Name, “Copy of “, vbTextCompare) > 0 Or _
InStr(1, grp.Name, “一時”, vbTextCompare) > 0 Then
grp.Delete
lRemovedCount = lRemovedCount + 1
End If
End If
Next i
Debug.Print “削除されたカスタムグループ数: ” & lRemovedCount
End Sub
‘ ==============================================================================
‘ Windows APIを使用してプロジェクトファイルのサイズを正確に取得する関数
‘ ==============================================================================
Private Function GetProjectFileSize(ProjectPath As String) As Currency
Dim hFile As LongPtr
Dim liSize As LARGE_INTEGER
Dim lResult As Long
GetProjectFileSize = 0 ‘ 初期値を設定
‘ ファイルを開く
‘ GENERIC_READ: 読み取りアクセス
‘ FILE_SHARE_READ: 他のプロセスもファイルを読み取れるようにする
‘ OPEN_EXISTING: ファイルが存在する場合に開く
hFile = CreateFile(ProjectPath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0)
If hFile = -1 Then
‘ ファイルが開けない場合のエラー処理
Debug.Print “Error: Could not open file ” & ProjectPath & ” for size check.”
Exit Function
End If
‘ ファイルサイズを取得
lResult = GetFileSizeEx(hFile, liSize)
Call CloseHandle(hFile) ‘ ハンドルを閉じるのを忘れない
If lResult <> 0 Then
‘ HighPart 2^32 + LowPart で正確なファイルサイズを取得
‘ VBAのCurrency型は最大約9.22E+14 (約922TB)なので、多くのProjectファイルに対応可能
GetProjectFileSize = CDec(liSize.HighPart) 4294967296# + CDec(liSize.LowPart)
Else
Debug.Print “Error: Could not get file size for ” & ProjectPath & “.”
End If
End Function
ThisProjectオブジェクトにイベントハンドラを記述
上記の`CleanProjectDefinitions`プロシージャを、Projectファイルが保存される直前に自動的に呼び出すためには、`ThisProject`オブジェクトに以下のイベントハンドラを記述します。
‘ ThisProject オブジェクトに記述
Option Explicit
Private Sub Project_BeforeSave(ByVal pj As Project)
‘ CleanProjectDefinitionsを呼び出す前に、
‘ 現在のプロジェクトが保存されていることを確認する。
‘ 新規作成プロジェクトで「名前を付けて保存」する際には、
‘ まだファイル名が確定していないため、ファイルサイズ比較ができない。
If Not pj.Path = “” Then
Call CleanProjectDefinitions(pj)
Else
Debug.Print “新規プロジェクト保存のため、最適化はスキップされました。”
End If
End Sub
コード解説と極限の知見
1. Windows API `GetFileSizeEx` の活用:
VBAの`FileLen`関数は、2GBを超えるファイルサイズに対応できない場合があります。特に大規模プロジェクトファイルではこの制限にぶつかる可能性があります。`Kernel32.dll`の`GetFileSizeEx`APIは、64ビット整数でファイルサイズを返すため、この問題を回避し、極めて正確なファイルサイズを把握できます。これにより、最適化の効果を定量的に評価する絶対的な基準を提供します。`CreateFile`と`CloseHandle`のペアも忘れずに。API呼び出しの失敗時(`hFile = -1`)のエラーハンドリングは堅牢性のため必須です。
2. `Application.ScreenUpdating` と `Application.EnableEvents`:
パフォーマンス最適化の鉄則です。UIの更新は多くのCPUサイクルを消費するため、処理中は必ず`False`に設定し、処理後に`True`に戻します。`Application.EnableEvents = False`は、クリーンアップ処理中に意図しないイベント連鎖が発生し、無限ループや予期せぬ挙動を引き起こすのを防ぎます。
3. オブジェクトのライフサイクルとメモリ最適化:
`Set vw = prj.Views.Item(i)` のようにオブジェクト参照を設定した場合、明示的に`Set vw = Nothing` を行うことで、メモリからの解放を促すことができます。今回のループ処理では、各イテレーションの終わりに`vw`などの変数スコープが終了するため、VBAのガベージコレクタが自動的に解放しますが、大規模なオブジェクト操作や複雑な参照がある場合は、明示的な解放が推奨されます。
4. 逆順ループ (`For i = prj.Views.Count To 1 Step -1`):
コレクションからアイテムを削除する際の絶対的な原則です。順方向でループすると、アイテムを削除した際にコレクションのインデックスがずれてしまい、スキップが発生したり、`Subscript out of range`エラーが発生したりします。逆順にすることで、この問題を回避し、確実に全ての対象を評価・削除できます。
5. `BuiltIn` プロパティの活用:
Projectアプリケーションに組み込まれている標準の定義(例: 「ガントチャート」ビュー、「完了したタスク」フィルタなど)は、誤って削除してはなりません。`BuiltIn`プロパティは、これらの定義を識別するための重要な手がかりとなります。
6. 削除ロジックのカスタマイズ:
提供したコードでは、`”Copy of “`や`”一時”`といった特定の文字列を名前に含む定義を削除対象としています。これはあくまで一例であり、実際の運用では貴社の命名規則や運用ポリシーに基づいて、より厳密な削除条件を定義する必要があります。例えば、作成日時が古い、特定のユーザーが作成した、など、より高度な判断基準を導入することも検討できます。ただし、Project VBAの標準オブジェクトモデルでは、定義の「作成者」や「作成日時」といったメタデータに直接アクセスすることは困難であるため、命名規則による管理が現実的です。
7. エラーハンドリング:
`On Error GoTo ErrorHandler` を設定し、予期せぬエラーが発生した場合でも、アプリケーションがクラッシュせず、`ScreenUpdating`や`EnableEvents`が適切に復元されるようにすることが重要です。これにより、ユーザー体験の悪化を防ぎ、システムの安定性を保ちます。
レガシー環境とシステム間連携における考慮事項
この最適化スクリプトは、単一のProjectファイルに閉じた問題に見えるかもしれませんが、レガシー環境やシステム間連携においては、その影響範囲が大きく広がります。
レガシーProjectバージョンとの互換性
古いProjectバージョン(例: Project 2007, 2010)では、オブジェクトモデルに差異がある場合があります。本稿のコードは`VBA7` (Project 2010以降の64bit対応VBA) を意識した`PtrSafe`宣言を含んでいますが、APIの定数や型の定義が古いVBA環境で動作しない可能性も考慮し、`#If VBA7 Then … #Else … #End If` ディレクティブで条件分岐させるのが堅牢なアプローチです。常にターゲットとなるProjectバージョンで十分なテストを行うことが不可欠です。
Project Server/PWAとの連携
Project Server (PWA) 環境では、プロジェクトファイルはサーバーサイドに格納され、クライアントアプリケーション(Project Professional)はサーバーからファイルを取得し、変更を加えて再び発行します。この際、ファイルが肥大化していると、以下の問題が発生します。
- 発行・同期時間の増大: サーバーとのデータ転送量が増え、発行や同期に時間がかかります。これはユーザーの生産性を直接的に損ねます。
- サーバーリソースの消費: 肥大化したファイルの処理は、Project Serverのデータベースやアプリケーションサーバーのリソースを余計に消費し、サーバー全体のパフォーマンスに影響を与えます。
- バージョン履歴の肥大化: サーバー側でファイルのバージョン履歴を管理している場合、一つ一つのファイルが肥大化していれば、ストレージ容量を不必要に消費します。
このため、PWAに発行する前に、クライアントサイドでこのクリーンアップ処理を実行することが極めて重要です。`Project_BeforeSave`イベントは、ローカル保存だけでなく、PWAへの発行時にもトリガーされるため、このスクリプトはPWA連携においても有効に機能します。
バージョン管理システムとの連携
プロジェクトファイルがGitやSVNなどのバージョン管理システムで管理されている場合、ファイルの肥大化はリポジトリのサイズを増大させ、クローン、コミット、差分比較といった操作のパフォーマンスを低下させます。定期的なクリーンアップにより、ファイルサイズを最小限に保つことは、バージョン管理システムの運用効率を向上させ、開発・管理コストを削減します。
極限の知見と落とし穴
この最適化は強力ですが、その導入には慎重な計画と運用が必要です。
- 誤削除のリスクとロールバック戦略:
最も危険なのは、誤って必要な定義を削除してしまうことです。特に、複数のプロジェクトで共有されているグローバルテンプレートの定義や、将来的に使用される可能性のある定義を判断なしに削除することは避けるべきです。
導入前には必ずテスト環境で十分な検証を行い、万が一の事態に備えて、最適化前のプロジェクトファイルのバックアップを自動的に作成する機能を追加することも検討してください。
- ユーザーへの説明責任と透明性:
このスクリプトがバックグラウンドで実行される場合、ユーザーはなぜファイルサイズが小さくなったのか、何が削除されたのかを理解できないかもしれません。これにより、予期せぬ混乱や不信感が生じる可能性があります。
導入時には、この最適化の目的、削除対象となる定義のタイプ、そしてそれがユーザーにどのようなメリットをもたらすのかを明確に説明する必要があります。`Debug.Print`で出力されるログをユーザーフレンドリーな形で表示する機能を追加することも有効です。
- 定期的なメンテナンスの自動化:
`Application_ProjectBeforeSave` イベントは強力ですが、プロジェクトが開かれるたびにクリーンアップが実行されるため、プロジェクトを開いて何も変更せずに閉じるだけでも保存処理が走ってしまいます。これを避けるために、週次や月次で特定のプロジェクト群に対してバッチ処理としてクリーンアップを実行する別のVBAマクロやスクリプトを検討することも有効です。
- パフォーマンス計測と効果の可視化:
最適化の導入後も、その効果を継続的に計測し、可視化することが重要です。`GetProjectFileSize` APIで取得したファイルサイズだけでなく、ファイルオープン時間、保存時間、特定の操作の応答性などを定期的にベンチマークし、最適化が期待通りの効果を発揮しているかを確認します。これにより、さらなる改善点や、新たな最適化ポイントを発見する手助けとなります。
結論
Microsoft Projectファイルの最適化は、単なる技術的な「おまけ」ではありません。それは、プロジェクト管理システムの健全な運用を支え、ユーザーの生産性を確保し、ITインフラの負荷を軽減するための必須の運用ポリシーの一部です。
ここで紹介したVBAスクリプトは、その強力な一歩となるでしょう。オブジェクトのライフサイクルを深く理解し、Windows APIを適切に活用することで、我々はVBAというツールを「極限」まで使いこなし、レガシーシステムに新たな生命を吹き込むことができるのです。この知識が、貴社のプロジェクト運用の堅牢性と効率性を飛躍的に向上させる一助となれば幸いです。
