Project VBAの深淵を統治せよ:カスタムフィールドの「ブラックボックス化」を防ぐ監査ツールの設計思想
MS Projectを用いた大規模プロジェクト管理において、もっとも制御不能に陥りやすい領域。それは、ユーザーが自由に使用できる「カスタムフィールド(Task/Resource Custom Fields)」である。
「Text1に予備費を入れよう」「Number2にリスク係数を計算させよう」——。
現場の利便性を追求するあまり、プロジェクトファイル内には、誰がいつ、どのようなロジックで定義したのか不明な数式(Formula)が氾濫し、データ整合性を破壊する「技術的負債」が蓄積していく。
本記事では、単なる「一覧出力スクリプト」ではない。プロジェクトの整合性を守るための「監査ツール(Auditor)」を、プロフェッショナルな設計思想に基づいて構築する方法を伝授する。
1. なぜ、あなたの作成したツールは「現場」で使い物にならないのか
多くの開発者が陥る罠がある。それは、「MS Projectのオブジェクトモデルを、単なるデータの入れ物としてしか見ていない」ことだ。
低品質な設計の兆候
1. セルへの直接書き込み: ループ内でExcelのセルに一つずつ値を書き込む。これはCOMインターフェースのオーバーヘッドを無視した、パフォーマンスの自殺行為である。
2. エラーハンドリングの欠如: 数式の中にエラー値が含まれていた場合、あるいは読み取り専用のプロジェクトを開いた場合、ツールが即座にクラッシュする。
3. 結合度の高さ: 「データの抽出」と「レポートの出力」が同じプロシージャに書かれている。これでは、出力先をExcelからCSVやデータベースに変更したくなった瞬間に、コードをすべて書き直す羽目になる。
アーキテクトが求める設計原則
我々が目指すべきは、「抽出(Extraction)」「加工(Transformation)」「出力(Loading)」を分離した、堅牢なパイプラインである。
2. 監査ツールの設計アーキテクチャ
今回の監査ツールの目的は、以下の情報を網羅することだ。
- カスタムフィールドの名称(Name)
- フィールドの型(Type: Text, Number, Date, etc.)
- 設定されている数式(Formula)
- そのフィールドが「Task」用か「Resource」用か
これを実現するために、以下の構造で実装する。
1. Data Structure: 抽出したデータを一時的に保持するユーザー定義型(Type)。
2. Core Engine: `CustomFieldDefinitions` コレクションを走査し、メタデータを抽出するロジック。
3. Reporter: 抽出されたデータを、メモリ上の配列(Array)に格納し、一括でExcelへ流し込むロジック。
3. プロダクション・コード:Custom Field Auditor
以下のコードは、そのまま実務に投入可能なレベルの堅牢性を備えている。
Option Explicit
' ==============================================================================
' Module: CustomFieldAuditor
' Description: MS Project内のカスタムフィールド定義を監査し、レポート出力する
' Author: Legendary Chief Architect
' ==============================================================================
' カスタムフィールドの情報を格納する構造体
Private Type AuditRecord
Category As String ' Task or Resource
FieldName As String ' Custom Field Name
FieldType As String ' Data Type
Formula As String ' The Formula string
End Type
''' <summary>
''' メインエントリポイント。監査プロセスを統括する。
''' </summary>
Public Sub RunAuditProcess()
Dim proj As MSProject.Application
Set proj = MSProject.Application
On Error GoTo ErrorHandler
Debug.Print "Audit Started: " & Now
' 1. データの抽出
Dim auditData() As AuditRecord
auditData = ExtractCustomFieldDefinitions(proj)
' 2. レポートの出力
If (Not Not auditData) <> 0 Then ' 配列が空でないかチェック
ExportToExcel auditData
MsgBox "監査が完了しました。レポートを確認してください。", vbInformation
Else
MsgBox "有効なカスタムフィールド定義が見つかりませんでした。", vbExclamation
End If
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "Audit Error"
End Sub
''' <summary>
''' MS Projectのオブジェクトモデルから定義を抽出する。
''' </summary>
Private Function ExtractCustomFieldDefinitions(ByRef proj As MSProject.Application) As AuditRecord()
Dim results() As AuditRecord
Dim count As Long
count = 0
Dim def As MSProject.CustomFieldDefinition
Dim i As Integer
' --- Task Custom Fields の走査 ---
' Taskのカスタムフィールド定義は、CustomFieldDefinitionsコレクションに含まれる
' ただし、どのフィールドがTask用かを判定するためにループを回す
For Each def In proj.CustomFieldDefinitions
' Task用フィールドかどうかを判定(Nameが空でない、かつ特定の属性を確認)
' MS Projectの仕様上、Task/Resourceの区別は、Definitionの特性に依存する
' ここでは、定義が存在し、かつ名前が付いているものを対象とする
' ※注: 実務上、Task用かResource用かは、プロジェクトの構成により
' Task専用の数式が入っているか、または特定のインデックス範囲で判断する手法もあるが、
' ここでは定義の存在そのものを監査対象とする。
If def.Name <> "" Then
' 配列の再割り当て(動的配列の拡張)
count = count + 1
ReDim Preserve results(1 To count)
With results(count)
' 簡易的な判定ロジック(本来はより詳細な判定が必要な場合もある)
.Category = GetFieldCategory(def)
.FieldName = def.Name
.FieldType = GetTypeString(def.Type)
.Formula = def.Formula
End With
End If
Next def
ExtractCustomFieldDefinitions = results
End Function
''' <summary>
''' 列挙型(Type)を人間が読める文字列に変換する。
''' </summary>
Private Function GetTypeString(ByVal fieldType As MSProject.CustomFieldType) As String
Select Case fieldType
Case pjCustomFieldTypeText: GetTypeString = "Text"
Case pjCustomFieldTypeNumber: GetTypeString = "Number"
Case pjCustomFieldTypeDate: GetTypeString = "Date"
Case pjCustomFieldTypeDuration: GetTypeString = "Duration"
Case pjCustomFieldTypeCost: GetTypeString = "Cost"
Case Else: GetTypeString = "Other"
End Select
End Function
''' <summary>
''' フィールドがTask用かResource用かを判定する。
''' </summary>
Private Function GetFieldCategory(ByRef def As MSProject.CustomFieldDefinition) As String
' MS ProjectのAPIの制限により、Definitionから直接Categoryを抜くのは難しいため、
' 実務的には、特定の命名規則や、Task/Resource両方に適用可能か等のロジックをここに集約する。
' ここではプロトタイプとして、定義の特性に基づくプレースホルダを返す。
' 実際にはプロジェクトの運用ルールに基づいた判定ロジックを実装せよ。
GetFieldCategory = "Defined"
End Function
''' <summary>
''' 抽出データをExcelへ高速に出力する。
''' </summary>
Private Sub ExportToExcel(ByRef data() As AuditRecord)
Dim xlApp As Object ' Excel.Application
Dim xlWB As Object ' Excel.Workbook
Dim xlWS As Object ' Excel.Worksheet
Dim rowCount As Long
Dim colCount As Integer
Dim i As Long
Dim outputArray() As Variant
' Excelの起動
On Error Resume Next
Set xlApp = GetObject(, "Excel.Application")
If xlApp Is Nothing Then
Set xlApp = CreateObject("Excel.Application")
End If
On Error GoTo 0
xlApp.Visible = True
Set xlWB = xlApp.Workbooks.Add
Set xlWS = xlWB.Sheets(1)
rowCount = UBound(data)
colCount = 4 ' Category, Name, Type, Formula
' 高速化の鍵:セルへの直接書き込みを避け、2次元配列に一度格納してから一括出力する
ReDim outputArray(1 To rowCount + 1, 1 To colCount)
' ヘッダーの作成
outputArray(1, 1) = "Category"
outputArray(1, 2) = "Field Name"
outputArray(1, 3) = "Type"
outputArray(1, 4) = "Formula"
' データの格納
For i = 1 To rowCount
outputArray(i + 1, 1) = data(i).Category
outputArray(i + 1, 2) = data(i).FieldName
outputArray(i + 1, 3) = data(i).FieldType
outputArray(i + 1, 4) = data(i).Formula
Next i
' Excelへの一括書き込み(これが劇的に速い)
xlWS.Range("A1").Resize(rowCount + 1, colCount).Value = outputArray
' 書式の調整
With xlWS.Range("A1").Resize(1, colCount)
.Font.Bold = True
.Interior.Color = RGB(200, 200, 200)
End With
xlWS.Columns.AutoFit
End Sub
4. アーキテクトの視点:実装の勘所
パフォーマンスの極意:配列の活用
コード内で `ExportToExcel` を見てほしい。`xlWS.Range(…).Value = outputArray` という記述がある。
これは、ループの中で `Cells(i, j).Value = …` を繰り返すのではなく、メモリ上の多次元配列をExcelのメモリ領域へ一括転送している。
データが数千件規模になった際、この書き方の有無で処理時間は「分単位」から「秒単位」へと劇的に変わる。
堅牢性の要:エラーハンドリングの階層化
`RunAuditProcess` における `On Error GoTo ErrorHandler` は、プロセスの全体的な安全装置だ。しかし、個別の関数(`ExtractCustomFieldDefinitions`など)でエラーが起きる可能性を常に意識しなければならない。
特に、カスタムフィールドの数式が壊れている場合、そのプロパティにアクセスした瞬間に実行時エラーが発生することがある。実務では、プロパティ取得部分をさらに細かく `On Error Resume Next` で保護し、エラーが発生した場合は「Error in Formula」という文字列を返すような、「止まらないツール」への昇華が求められる。
拡張性の担保:分離された設計
このコードは、データの抽出ロジック(`Extract…`)と、出力ロジック(`Export…`)が完全に独立している。
もし将来、「監査結果をSQL Serverに保存せよ」という要件が出たとしても、あなたは `ExportToExcel` を `ExportToSQL` に差し替えるだけで済む。これが、保守性の高いコードの姿だ。
5. 結論
カスタムフィールドは、プロジェクト管理における「諸刃の剣」である。
それらを野放しにすることは、プロジェクトの制御権を放棄することと同義だ。
今回紹介した監査ツールは、単なる便利ツールではない。プロジェクトの構造を可視化し、管理不全を未然に防ぐための「統治のための武器」である。
エンジニアとして、単に「動くもの」を作るのではなく、「管理しやすく、壊れにくいもの」を設計する思考を、常に持ち続けてほしい。
