【Project VBA極限知見】Windowsシェルを従えよ:カスタムプロパティによるメタデータ駆動型プロジェクト管理の全貌
プロジェクト管理の現場において、MS Projectのファイル群(`.mpp`)が単なる「スケジュール表のコンテナ」であるという認識は、シニアエンジニアの視点からは致命的な思考停止と言わざるを得ない。数千に及ぶプロジェクトファイルを運用する大規模組織において、ファイル名やフォルダ階層だけに依存した管理は、必ず破綻する。
真にスケーラブルなPMOアーキテクチャを構築する者であれば、ファイル自体に構造化されたメタデータを内包させ、Windowsエクスプローラーやデスクトップサーチのインデックス層からダイレクトにクエリ可能な状態を作り上げることの重要性を理解しているはずだ。
今回は、MS ProjectのVBAオブジェクトモデルとWindows APIの境界領域を突破し、カスタムプロパティ(ドキュメントプロパティ)を完全制御、さらにインデックス最適化とメモリマネジメントまで踏み込んだ極限の知見を共有する。
—
1. プロジェクト属性設計のパラダイムシフト:なぜカスタムプロパティか
MS Projectの `Project` オブジェクトには、標準で用意されているフィールド(Cost, Work等)の他に、任意のキーバリューを保持できるカスタムプロパティ(BuiltInDocumentProperties / CustomDocumentProperties)が存在する。
これをエクスプローラー側で「詳細ペイン」や「検索インデックス」に露出させることで、Projectを起動することなく、PowerShellやWindows Search API経由で横断的なメタデータ検索が可能になる。
しかし、VBA標準のドキュメントプロパティ操作には、型の暗黙的変換の罠、存在しないプロパティへのアクセス時のランタイムエラー、そしてCOMオブジェクトの参照リークという、実務者を悩ませる地雷が埋め込まれている。これらを完全にハックする。
—
2. 【実装コード】堅牢性と速度を極めたカスタムプロパティ自動付与エンジン
以下のコードは、単なるプロパティの設定に留まらず、オブジェクトのライフサイクル管理、型安全性の担保、例外時のフォールバックを完備したプロダクションクオリティのモジュールである。
Option Explicit
‘ —————————————————————–
‘ モジュール名: M01_MetadataManager
‘ 概要: Projectのカスタムプロパティを安全かつ高速に操作するエンジン
‘ —————————————————————–
‘ プロパティのデータ型定義(MSOドキュメントプロパティ仕様に準拠)
Public Enum MsoDocPropertiesType
msoPropertyTypeNumber = 1
msoPropertyTypeBoolean = 2
msoPropertyTypeDate = 3
msoPropertyTypeText = 4
msoPropertyTypeFloat = 5
End Enum
/
- 指定したプロジェクトのカスタムプロパティにメタデータをアトミックに書き込む
- @param targetProj 対象のProjectオブジェクト
- @param propName プロパティ名(英数字推奨)
- @param propValue 設定する値
- @param propType MsoDocPropertiesType列挙体
/
Public Sub SetProjectMetadata(ByRef targetProj As MSProject.Project, _
ByVal propName As String, _
ByVal propValue As Variant, _
ByVal propType As MsoDocPropertiesType)
Dim customProps As Object ‘ late binding or early binding with Microsoft Office 16.0 Object Library
Dim prop As Object
Dim isExist As Boolean
On Error GoTo ErrorHandler
‘ パフォーマンスとメモリリーク防護のため、オブジェクト参照は最小限のスコープで取得
Set customProps = targetProj.CustomDocumentProperties
isExist = False
‘ 既存プロパティの走査(COMコレクションの遅延評価を考慮)
For Each prop In customProps
If StrComp(prop.Name, propName, vbTextCompare) = 0 Then
isExist = True
Exit For
End If
Next prop
If isExist Then
‘ 既存の場合は値を更新(型の整合性を保つ)
prop.Value = CastToAppropriateType(propValue, propType)
Else
‘ 新規作成
‘ 注意: Addメソッドの引数シグネチャ (Name, LinkToContent, Type, Value)
customProps.Add Name:=propName, _
LinkToContent:=False, _
Type:=propType, _
Value:=CastToAppropriateType(propValue, propType)
End If
CleanUp:
‘ 【重要】COMオブジェクトの明示的解放
‘ VBAのガベージコレクタに依存せず、スコープ外でのメモリ残存を防ぐ
If Not prop Is Nothing Then Set prop = Nothing
If Not customProps Is Nothing Then Set customProps = Nothing
Exit Sub
ErrorHandler:
Debug.Print “[Error] SetProjectMetadata Failed: ” & Err.Description
‘ 業務システム連携時は独自ログ基盤へのフォールバックをここに記述
Resume CleanUp
End Sub
/
- 渡されたVariant値を指定されたMSOプロパティ型に厳密にキャストする
/
Private Function CastToAppropriateType(ByVal val As Variant, ByVal pType As MsoDocPropertiesType) As Variant
Select Case pType
Case msoPropertyTypeNumber, msoPropertyTypeFloat
CastToAppropriateType = CDbl(val)
Case msoPropertyTypeBoolean
CastToAppropriateType = CBool(val)
Case msoPropertyTypeDate
CastToAppropriateType = CDate(val)
Case msoPropertyTypeText
CastToAppropriateType = CStr(val)
Case Else
CastToAppropriateType = CStr(val)
End Select
End Function
—
3. シニアエンジニアが知るべきメモリ管理とCOMの深層
上記のコードに登場する `Set customProps = targetProj.CustomDocumentProperties` という一行。ここにはVBAランタイムの致命的な罠が潜んでいる。
COMラッパーの参照カウントとメモリリーク
VBAで `targetProj.CustomDocumentProperties` を呼び出すたびに、裏ではCOM(Component Object Model)のインターフェースがラップされ、新しいオブジェクト参照(IUnknownポインタ)がヒープ上に生成される。
もし、これをループ内で毎度呼び出したり、プロシージャ終了時に `Nothing` を代入して参照カウント(AddRef/Release)をデクリメントさせない場合、プロセスが終了するまでVBAのメモリ空間にゾンビオブジェクトが残留する。
数千件のプロジェクトファイルを一括バッチ処理するシンスクリプトにおいて、このリークは数分でメモリ不足(Out of Memory)を引き起こす。「使い終わったCOMオブジェクトは、例外発生時であっても確実に `Nothing` を代入して解放する」。これはレガシー環境を生き抜いてきたエンジニアの鉄則である。
—
4. Windowsエクスプローラー検索へのインデックス統合
カスタムプロパティにメタデータを書き込んだだけでは不十分だ。ユーザーがWindowsエクスプローラーの検索窓から `System.StructuredQuery` やプロパティ指定検索(例: `author:PMO_Group` やカスタム定義したプロパティ名)を行えるようにするには、Windows Searchのプロパティストア(Property System)と連携させる必要がある。
拡張プロパティの活用ポイント
1. 命名規則の統一:
エクスプローラー側で列(カラム)として表示させる場合、`System.Document.Company` などの既存システムプロパティと競合しないカスタムプレフィックス(例: `PRJ_Phase`, `PRJ_RiskLevel`)を付与する。
2. インデックスの強制更新:
大量の`.mpp`を一括生成・更新した直後は、Windows Searchのクローラーが追いついていない。必要に応じて、バッチの最終段階でWindows Searchインベクサーサービス(`WSearch`)に対する再インデックス要求をPowerShell経由でキックするアーキテクチャが望ましい。
—
5. システム間連携の極限:APIを介したメタデータ抽出
プロジェクトファイルを開かずに(ヘッドレス、あるいはバックグラウンドのストリームとして)メタデータを読み取る仕組みを構築できれば、PMOのポータルサイトやBIツール(Power BI等)からのデータ収集効率は劇的に跳ね上がる。
VB.NETやC#によるバックエンドサービスを想定した場合、COM互換の `Microsoft.Office.Interop.MSProject` を用いるのではなく、OLEStructuredStorage(Structured Storage API)を直接叩いてプロパティストリームをパースするのが、真にパフォーマンスを極めたアーキテクチャである。
しかし、VBA環境の制約下においては、MS Projectを不可視モード(`Application.FileOpen` の `ReadWrite:=False` かつ `Invisible:=True`)で安全にロードし、最小限のオーバーヘッドでプロパティを吸い上げるラッパー関数を用意するのが現実解となる。
Public Function ReadProjectMetadata(ByVal filePath As String, ByVal propName As String) As Variant
Dim appProj As MSProject.Application
Dim prj As MSProject.Project
Dim customProps As Object
Dim prop As Object
Dim result As Variant
Set appProj = New MSProject.Application
appProj.Visible = False
On Error GoTo Catch
‘ 読み取り専用かつ非表示でオープン(リソース消費を最小化)
appProj.FileOpen Name:=filePath, ReadOnly:=True, Notify:=False
Set prj = appProj.ActiveProject
Set customProps = prj.CustomDocumentProperties
result = Empty
For Each prop In customProps
If StrComp(prop.Name, propName, vbTextCompare) = 0 Then
result = prop.Value
Exit For
End If
Next prop
Finally:
On Error Resume Next
If Not prj Is Nothing Then appProj.FileCloseEx pjDoNotSave
If Not appProj Is Nothing Then appProj.Quit
‘ オブジェクトの徹底的な解放
Set prop = Nothing
Set customProps = Nothing
Set prj = Nothing
Set appProj = Nothing
ReadProjectMetadata = result
Exit Function
Catch:
Debug.Print “[Error] ReadProjectMetadata: ” & Err.Description
Resume Finally
End Function
—
総括
プロジェクトのカスタムプロパティの制御は、単なる「おまけの機能」ではない。それは、無秩序に散らばるファイル資産を、「クエリ可能なデータベース」へと昇華させるための唯一無二のブリッジである。
メモリのライフサイクルを支配し、WindowsのOSレイヤーまで見通した設計眼を持つ者だけが、真に堅牢でメンテナンス性の高いエンタープライズVBAソリューションを構築できる。コードの細部に宿る神を恐れず、すべてのオブジェクトを掌中に収めよ。
