【実務・中級編】【中級者向け】カスタムフィールドの値をExcelから読み込み、VBAでProjectに反映させる – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

プロジェクトを破壊する「安易なループ」を捨てろ。Project VBAでカスタムフィールドを制御する極限の設計論

多くの開発者が、MS Projectの自動化という領域に足を踏み入れた際、致命的なミスを犯す。
それは、「Excelのセル値を、そのままProjectのオブジェクトへ流し込む」という、あまりにも短絡的で、あまりにも脆い実装だ。

「動けばいい」という思考で作られたツールは、データの型が一つ違うだけで、あるいはタスクの並び順が一つ変わっただけで、プロジェクトのスケジュール管理という「命綱」を破壊する。プロジェクト管理におけるデータ整合性の欠如は、単なるバグではない。それは、意思決定を狂わせる「毒」だ。

本稿では、Excelをマスターとし、Projectのカスタムフィールド(Task/Resource)へ値を同期させる際、プロフェッショナルがどのような設計思想を持ち、どのように堅牢なコードを実装すべきかを伝授する。

1. アーキテクチャの設計思想:なぜ「単純なループ」は死を招くのか

初心者が書くコードは、概ね以下のようになる。
`For Each task In ActiveProject.Tasks` → `If task.Name = ExcelValue Then task.Text1 = ExcelValue`

これは最悪の設計だ。理由は二つある。

1. 計算量(Complexity)の爆発: タスク数が1,000件、Excelの行数が1,000行あった場合、$O(N \times M)$ の比較が発生する。Projectのオブジェクトモデルへのアクセスは極めて重い。この「二重ループによるオブジェクト検索」は、実務レベルのデータ量では処理時間を指数関数的に増大させる。
2. 識別子の脆弱性: `Task.ID` や `Task.Name` をキーにしてデータを紐付けていないか? `ID` は並び替え(Sort)によって容易に変化し、`Name` は重複や微細な表記揺れ(全角・半角、スペースの有無)に耐えられない。

プロフェッショナルが採用する戦略

我々が採用すべきは、「ハッシュマップ(Dictionary)によるインデックス化」と「UniqueIDによる不変的な紐付け」だ。

  • UniqueIDの利用: Projectオブジェクトが持つ `UniqueID` は、タスクの並び替えや削除が発生しても変わらない、唯一絶対の識別子である。これこそが、外部データとProjectを接続する唯一の「信頼できる鍵」だ。
  • DictionaryによるO(1)アクセス: 最初にProjectの全タスクを走査し、`UniqueID` をキー、`Taskオブジェクト` を値としたDictionaryを作成する。これにより、Excelの各行に対する検索コストを、劇的に低減させる。

2. 実装:堅牢なデータ同期エンジンの構築

以下に、実務でそのままプロダクションコードとして採用できるレベルの、設計思想に基づいたコードを示す。ここでは、Excelの管理表からProjectのカスタムフィールド(Text1)へ値を流し込むシナリオを想定する。

前提条件

  • ExcelシートのA列に `UniqueID`、B列に `カスタムフィールドの値` が入力されていること。
  • Microsoft Scripting Runtime への参照設定が必要(Dictionary使用のため)。
' ==============================================================================
' Module: Mod_ProjectSync
' Description: ExcelからProjectカスタムフィールドへの堅牢なデータ同期エンジン
' Author: Chief Architect
' ==============================================================================

Option Explicit

''' <summary>
''' Excelの管理表に基づき、Projectのタスク情報を更新するメインルーチン
''' </summary>
Public Sub SyncExcelToProject()
    Dim xlApp As Object
    Dim xlWB As Object
    Dim xlSheet As Object
    Dim proj As Project
    Dim taskDict As Object ' Scripting.Dictionary
    Dim lastRow As Long
    Dim i As Long
    
    ' データの整合性を担保するためのカウンター
    Dim successCount As Long: successCount = 0
    Dim errorCount As Long: errorCount = 0
    
    On Error GoTo ErrorHandler

    ' 1. オブジェクトの初期化
    Set proj = ActiveProject
    If proj Is Nothing Then
        MsgBox "Projectファイルが開かれていません。", vbCritical
        Exit Sub
    End If

    ' Excelへのアクセス (現在アクティブなブックを想定)
    Set xlWB = ThisWorkbook
    Set xlSheet = xlWB.Sheets(1) ' 対象のシート名を指定すべき
    lastRow = xlSheet.Cells(xlSheet.Rows.Count, "A").End(-4162).Row ' -4162 = xlUp

    ' 2. Projectタスクのインデックス化 (O(N) のスキャン)
    ' UniqueIDをキーにすることで、検索コストを最小化する
    Set taskDict = CreateObject("Scripting.Dictionary")
    
    Dim t As Task
    For Each t In proj.Tasks
        If Not t Is Nothing Then
            ' UniqueIDは不変のキーとして利用
            If Not taskDict.Exists(t.UniqueID) Then
                taskDict.Add t.UniqueID, t
            End If
        End If
    Next t

    ' 3. 同期プロセスの実行
    ' Excelの各行を走査し、Dictionaryを用いて直接オブジェクトへアクセス
    For i = 2 To lastRow ' 1行目はヘッダーと想定
        Dim targetUID As Long
        Dim newValue As Variant
        
        targetUID = xlSheet.Cells(i, 1).Value ' A列: UniqueID
        newValue = xlSheet.Cells(i, 2).Value  ' B列: 更新値
        
        ' バリデーション:UniqueIDが数値か、Dictionaryに存在するか
        If targetUID > 0 And taskDict.Exists(targetUID) Then
            Dim targetTask As Task
            Set targetTask = taskDict(targetUID)
            
            ' 4. 型の検証とエラーハンドリングを内包した書き込み
            If SafeWriteCustomField(targetTask, newValue) Then
                successCount = successCount + 1
            Else
                errorCount = errorCount + 1
            End If
        Else
            ' 存在しないUIDへのエラーログ出力(実務ではログファイルへの出力が望ましい)
            Debug.Print "Error: UniqueID " & targetUID & " not found in Project."
            errorCount = errorCount + 1
        End If
    Next i

    ' 5. 結果報告
    MsgBox "同期完了" & vbCrLf & _
           "成功: " & successCount & " 件" & vbCrLf & _
           "失敗: " & errorCount & " 件", vbInformation

    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical
End Sub

''' <summary>
''' カスタムフィールドへの書き込みを安全に行うためのラッパー関数
''' </summary>
''' <param name="t">対象のTaskオブジェクト</param>
''' <param name="val">書き込みたい値</param>
''' <returns>書き込み成功ならTrue</returns>
Private Function SafeWriteCustomField(ByRef t As Task, ByVal val As Variant) As Boolean
    On Error GoTo WriteError
    
    ' ここで型のチェックを厳格に行う
    ' 例: Text1に書き込む場合でも、極端に長い文字列や不正な文字を弾くロジックを挟む
    If IsError(val) Then GoTo WriteError
    
    ' Projectのカスタムフィールドへの代入
    ' 本来はここで、Text1なのかNumber1なのかを引数で制御する設計が理想的
    t.Text1 = CStr(val)
    
    SafeWriteCustomField = True
    Exit Function

WriteError:
    ' 書き込み失敗時のログ記録
    Debug.Print "Write Error on Task: " & t.Name & " (UID: " & t.UniqueID & ") | Value: " & val
    SafeWriteCustomField = False
End Function

3. エンジニアとしての「審美眼」:設計のポイント解説

このコードが、単なる「コピペ用のスクリプト」と何が違うのか。その設計の肝を解説する。

① O(N) vs O(N^2) の戦い

`taskDict` を作成する工程で、一度だけ全タスクを走査している。これにより、Excelの行数が増えても、各行の検索コストは定数時間 $O(1)$ で済む。大規模なプロジェクト(数千タスク)において、この差は「数秒」か「数分」かという決定的な違いとなって現れる。

② 境界条件とバリデーションの分離

`SafeWriteCustomField` というラッパー関数を設けている点が重要だ。
メインのロジック(同期フロー)と、データの整合性を守るロジック(書き込みルール)を分離することで、「一つの不正なデータによって、全体の処理が停止する」という最悪の事態を防いでいる。 実務において、全データがエラーで止まるツールは、ツールとしての価値を失う。

③ 識別子の不変性(Immutability)

`UniqueID` を採用している。これは、Projectにおける「設計の鉄則」だ。
「タスク名が変わった」「タスクが上に移動した」「タスクがソートされた」……これら全ての事象に対して、`UniqueID` は決して揺るがない。この「揺るぎなさ」こそが、自動化ツールの信頼性を担保する。

結論:保守性の高いコードを書くために

業務自動化エンジニアが目指すべきは、「動くコード」ではない。「運用に耐え、変更に強く、失敗を予測できるコード」だ。

ExcelとProjectを連携させる際、君たちが直面するのは、データの不整合という名の「混沌」だ。その混沌を制御するためには、オブジェクトのライフサイクルを理解し、計算量を意識し、エラーを「例外」ではなく「想定内のイベント」として扱う設計思想が必要不可欠である。

次にコードを書くときは、自分に問いかけてほしい。
「もし、Excelのデータが壊れていたら、私のツールはプロジェクトを破壊するか、それとも正しくエラーを報告するか?」

その答えが、君が「スクリプト・キディ」か「アーキテクト」かを分かつ境界線となる。

タイトルとURLをコピーしました