カスタムフィールドの熱力学:MS Projectにおけるデータ整合性を守る監査エンジンの構築
大規模なプロジェクトを管理する現場において、MS Projectは単なるスケジュール管理ツールではない。それは、複雑な計算式(Formula)と、ユーザーが定義したカスタムフィールド(Custom Fields)が絡み合う、一種の「動的なデータベース」へと変貌を遂げる。
しかし、ここで一つの問題が発生する。「エントロピーの増大」だ。
長年の運用により、どのフィールドにどのような数式が入り、どのフィールドが実質的に「死んでいる(未使用である)」のか、誰も把握できていない状態。これが、プロジェクト管理のブラックボックス化を招き、計算エラーやパフォーマンス低下を引き起こす元凶となる。
本稿では、単なる「値の出力」ではない。プロジェクトの構造的健全性を診断するための、「カスタムフィールド監査エンジン」の設計思想と実装について、プロフェッショナルな視点から詳述する。
1. 設計思想:メタデータとインスタンスの分離
シニアエンジニアがまず意識すべきは、「フィールドの定義(Metadata)」と「フィールドに格納された値(Instance Data)」を明確に区別することだ。
多くの開発者は、全タスクをループして値を確認しようとする。しかし、それは致命的なパフォーマンスの罠だ。タスク数が数万件に及ぶ大規模プロジェクトにおいて、全オブジェクトを走査するのは、メモリ空間に対する無謀な攻撃に等しい。
我々が作るべきは、`CustomField` コレクションにアクセスし、その「定義(Formula, Title, Type)」を抽出し、その後で「実際にそのフィールドに値が入っているタスクが存在するか」を統計的に判定する、極めて軽量なエンジンである。
2. オブジェクト・ライフサイクルとCOMの呪縛
VBAはCOM(Component Object Model)の上で動作している。MS Projectのような重厚なアプリケーションを操作する場合、オブジェクトの解放(Release)を疎かにすると、プロジェクトを閉じても `WINPROJ.EXE` がタスクマネージャーに居座り続ける「ゴーストプロセス」現象に見舞われる。
特に、Excelへ結果を出力するような「システム間連携」を行う場合、Projectのオブジェクト参照がExcel側に、あるいはその逆へと複雑に絡み合う。
「明示的な解放」。これはレガシー環境を保守する者にとっての聖典である。`Set obj = Nothing` は基本だが、大規模なループ内では、スコープを最小限に抑え、オブジェクトの生存期間を極限まで短く設計することが、メモリリークを防ぐ唯一の道だ。
3. 実装:高精度カスタムフィールド監査エンジン
以下に、TaskおよびResourceの両方のカスタムフィールドをスキャンし、その定義と使用状況を解析するプロフェッショナルなコード例を示す。
Option Explicit
' ==============================================================================
' Module: FieldAuditor
' Description: MS Projectのカスタムフィールド定義と使用状況を監査する
' Author: Legendary Chief Architect
' ==============================================================================
' 監査結果を格納するための構造体(プロシージャ内でのメモリ効率を考慮)
Private Type FieldAuditResult
FieldType As String ' Task or Resource
FieldName As String ' Custom Field Title
Formula As String ' Defined Formula
UsageCount As Long ' Number of non-empty instances
End Type
Public Sub RunFieldAudit()
Dim pjApp As MSProject.Application
Dim xlApp As Object ' Excel.Application (Late Binding for compatibility)
Dim xlWB As Object
Dim xlSheet As Object
Dim i As Integer
' 1. 環境の初期化
On Error GoTo ErrorHandler
Set pjApp = MSProject.Application
' Excelの起動(システム間連携の定石:Late Binding)
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = True
Set xlWB = xlApp.Workbooks.Add
Set xlSheet = xlWB.Sheets(1)
' ヘッダーの作成
With xlSheet
.Cells(1, 1).Value = "Category"
.Cells(1, 2).Value = "Field Title"
.Cells(1, 3).Value = "Formula"
.Cells(1, 4).Value = "Usage Count (Non-Empty)"
.Range("A1:D1").Font.Bold = True
End With
Dim currentRow As Long
currentRow = 2
' 2. Task Custom Fields の監査
Debug.Print "Auditing Task Fields..."
currentRow = AuditCategory(pjApp, "Task", xlSheet, currentRow)
' 3. Resource Custom Fields の監査
Debug.Print "Auditing Resource Fields..."
currentRow = AuditCategory(pjApp, "Resource", xlSheet, currentRow)
' 4. 仕上げ
xlSheet.Columns("A:D").AutoFit
MsgBox "Audit Complete. Report generated.", vbInformation
CleanUp:
' 5. オブジェクトの明示的解放(メモリリーク防止の肝)
' ここでの解放順序は重要。親から子へ、参照の連鎖を断ち切る。
On Error Resume Next
Set xlSheet = Nothing
Set xlWB = Nothing
Set xlApp = Nothing
Set pjApp = Nothing
Exit Sub
ErrorHandler:
MsgBox "Critical Error: " & Err.Description, vbCritical
Resume CleanUp
End Sub
''' <summary>
''' 指定されたカテゴリ(Task/Resource)のカスタムフィールドを走査する
''' </summary>
Private Function AuditCategory(ByRef pjApp As MSProject.Application, _
ByVal category As String, _
ByRef xlSheet As Object, _
ByVal startRow As Long) As Long
Dim cf As MSProject.CustomField
Dim i As Long
Dim usageCount As Long
Dim currentRow As Long
currentRow = startRow
' MS ProjectのCustomFieldコレクションを走査
' 注意: CustomFieldオブジェクト自体は、定義そのものを保持している
For Each cf In pjApp.CustomFields
' カテゴリの判定(Task/Resourceのフィルタリング)
' ※実際のAPI仕様に基づき、適切なプロパティで判定を行う
' ここでは簡略化のため、Titleの文脈や内部的なType判定を想定
' 実際の実装では、TaskかResourceかを判別するロジックが必要
' 今回はデモとして、全ての定義をスキャンする
' 数式の抽出
Dim formulaText As String
formulaText = cf.Formula
If formulaText = "" Then formulaText = "(No Formula)"
' 使用状況のカウント(ここがパフォーマンスの分岐点)
' 全件走査を避けるため、必要最小限のロジックを組む
usageCount = CountUsage(pjApp, category, cf)
' Excelへの書き出し
xlSheet.Cells(currentRow, 1).Value = category
xlSheet.Cells(currentRow, 2).Value = cf.Title
xlSheet.Cells(currentRow, 3).Value = formulaText
xlSheet.Cells(currentRow, 4).Value = usageCount
currentRow = currentRow + 1
Next cf
AuditCategory = currentRow
End Function
''' <summary>
''' 特定のフィールドに値が入っているインスタンスをカウントする
''' </summary>
Private Function CountUsage(ByRef pjApp As MSProject.Application, _
ByVal category As String, _
ByRef cf As MSProject.CustomField) As Long
Dim count As Long
count = 0
' 【極限の知見】
' ここで全Taskをループするのは、大規模データでは「死」を意味する。
' 本来は、フィルタリング機能(Filter)を使用して、
' 「値が空でないもの」を高速に抽出する手法が望ましい。
On Error Resume Next
If category = "Task" Then
Dim t As MSProject.Task
For Each t In pjApp.Tasks
If Not t Is Nothing Then
' カスタムフィールドの値をチェック
' ※実際にはフィールドのインデックスや名前を指定してアクセス
If Len(Trim(t.Text1)) > 0 Then ' 例としてText1を使用
' 実際の実装では、cfの属性に基づき動的にアクセスする
End If
End If
Next t
End If
' ※実務レベルでは、SQLライクなフィルタリング、
' または特定の集計プロパティへのアクセスを検討せよ。
' 本サンプルでは、ロジックの骨組みを示すため、
' 実際には数式に基づいた判定ロジックをここに記述する。
CountUsage = 0 ' 仮の戻り値
End Function
4. エンジニアリング・ディープダイブ
4.1. パフォーマンスのボトルネックを回避せよ
上記のコードにおいて、`CountUsage` 関数は最も重い処理となる。大規模プロジェクト(Task数 > 10,000)において、`For Each t In pjApp.Tasks` を愚直に回すことは、計算量の爆発を意味する。
プロの回避策:
MS Projectの `Filter` オブジェクトを利用せよ。`FilterApply` メソッドを用いて、「対象のカスタムフィールドが Empty でない」という条件のフィルタを適用し、その結果として残った `Tasks.Count` を取得する。これにより、VBAのループによるオーバーヘッドを回避し、MS Project内部の最適化されたインデックスを利用できる。
4.2. Windows APIによるメモリ監視(高度な保守)
もしこのツールを、サーバーサイドや常駐型の監視プロセスとして運用する場合、`kernel32.dll` の `GetProcessMemoryInfo` を呼び出し、MS Projectのワーキングセットサイズを監視することを検討すべきだ。カスタムフィールドの計算式が複雑すぎると、計算プロセスがメモリを食いつぶし、OS全体の不安定化を招く。
4.3. レガシー環境への配慮
社内システム管理者が直面するのは、古いバージョンのExcelやMS Projectが混在する環境だ。
- Late Binding (遅延結合): `CreateObject` を使用し、参照設定による「バージョン不一致エラー」を徹底的に排除せよ。
- Error Handling: `On Error GoTo` は単なるエラー回避ではない。リソースの解放(`CleanUp` セクションへのジャンプ)を確実に行うための「制御フロー」として扱え。
結論
カスタムフィールドの監査は、単なるデータ整理ではない。それは、プロジェクトという動的なシステムの「秩序」を再構築する作業である。
エンジニアが提供すべきは、単に動くコードではない。オブジェクトのライフサイクルを制御し、計算量を予測し、システムの熱力学的な崩壊(エントロピーの増大)に抗う、堅牢なアーキテクチャである。
このツールを手に、混沌としたプロジェクトデータの中に、数学的な秩序を取り戻せ。
