【テクニカル・上級編】Word VBAで『文書プロパティ』を読み書きする:カスタムプロパティを用いた版管理の自動化 – Word VBA解析バイブル

スポンサーリンク

Word VBAを掌握する極限の知見:カスタムプロパティを用いた版管理の自動化

Word VBAにおけるオブジェクトモデルの理解は、単に「マクロの記録」をトレースすることではない。背後でうごめくCOMコンポーネントのライフサイクル、メモリ管理、そしてホストアプリケーションとの対話を完全に掌握することだ。

今回は、実務の現場で最も要求される「文書プロパティの高度な操作」、特に `CustomDocumentProperties` を軸とした版管理システムの構築について、綺麗事なしの極限の知見を共有する。

1. なぜビルトインではなく「カスタムプロパティ」なのか

Wordには、`BuiltInDocumentProperties` という標準プロパティが存在する。「著者」「タイトル」「会社名」など、一見するとこれらで事足りるように思えるだろう。

しかし、シニアエンジニアであれば、標準プロパティに依存することの致命的なリスクを知っているはずだ。

  • スキーマの硬直性: 標準プロパティはキーが固定されており、システム連携に必要なメタデータ(例: `System_Revision`, `Approval_Status`, `External_ID`)を格納するには型や意味が不適合である。
  • ローカライズ問題: 標準プロパティのインデックスや名前(例: `”Author”` や `”著者”`)は、Officeの表示言語やExcel/Wordのバージョンによって挙動が揺らぐ。

一方、`Document.CustomDocumentProperties` は、開発者が独自のスキーマを完全にコントロールできる。文字列、数値、真偽値、日付データを安全に保持し、外部の文書管理システム(DMS)やRPA、CI/CDパイプラインとの間で確実なデータ連携ブリッジとして機能させることができる。

2. メモリ最適化とオブジェクト参照の鉄則

VBAにおける最大の悪習は、ドット繋ぎの冗長なコードによるメモリリークや、解放されないCOMオブジェクトの残骸だ。特にWordのオブジェクトモデルは、不適切なRangeやDocumentの取得方法を行うと、WinWord.exeのプロセスがメモリ上に残留し、ゾンビプロセスを量産する。

以下のコードは、カスタムプロパティへの書き込みと読み込みを、メモリ効率を極限まで高めた設計で実装したものである。

Option Explicit

‘ ==============================================================================
‘ 概要: 指定された文書のカスタムプロパティを安全に設定・更新する
‘ 特徴: 存在チェック、型不一致エラーの回避、参照の適切な解放を網羅
‘ ==============================================================================
Public Sub SetCustomProperty(ByRef targetDoc As Document, ByVal propName As String, ByVal propValue As Variant, ByVal propType As MsoDocProperties)

Dim props As Office.DocumentProperties
Dim prop As Office.DocumentProperty
Dim isExists As Boolean

On Error GoTo ErrorHandler

‘ プロパティコレクションの参照を取得
Set props = targetDoc.CustomDocumentProperties
isExists = False

‘ 既存のプロパティを走査(遅延バインディングを避け、型安全を担保)
For Each prop In props
If StrComp(prop.Name, propName, vbTextCompare) = 0 Then
isExists = True
Exit For
End If
Next prop

If isExists Then
‘ 既存プロパティの値更新
‘ ※型が異なる場合は一度削除して再作成する必要がある
If prop.Type = propType Then
prop.Value = propValue
Else
prop.Delete
props.Add Name:=propName, LinkToContent:=False, Type:=propType, Value:=propValue
End If
Else
‘ 新規プロパティの追加
props.Add Name:=propName, LinkToContent:=False, Type:=propType, Value:=propValue
End If

CleanUp:
‘ オブジェクト変数の明示的な解放(COMコンポーネントの解放漏れを防ぐ)
Set prop = Nothing
Set props = Nothing
Exit Sub

ErrorHandler:
MsgBox “カスタムプロパティの設定に失敗しました: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

3. 実践:版管理(Revision Control)の自動化アーキテクチャ

単にプロパティを書き込むだけでは、お遊戯の域を出ない。真のエンジニアリングとは、文書のオープン・クローズ、あるいは保存(`BeforeSave` イベント)のライフサイクルにフックし、人間の手によるミスを完全に排除した版管理メカニズムを構築することだ。

以下のコードは、Wordのクラスモジュール(例: `ThisDocument`)またはアドインから制御することを想定した、自動版数インクリメントの実装である。

‘ ==============================================================================
‘ 文書保存時に版数(Major.Minor)を自動インクリメントし、メタデータを更新する
‘ ==============================================================================
Public Sub AutoIncrementVersion(ByRef targetDoc As Document, Optional ByVal isMajorUpdate As Boolean = False)

Dim currentVersion As String
Dim majorVer As Long
Dim minorVer As Long
Dim dotPos As Long

Const PROP_VERSION As String = “Doc_Version”
Const PROP_UPDATED_BY As String = “Doc_LastUpdatedBy”
Const PROP_UPDATED_DATE As String = “Doc_LastUpdatedDate”

‘ 1. 現在の版数を取得(存在しない場合は初期値 “0.1”)
currentVersion = GetCustomPropertyString(targetDoc, PROP_VERSION, “0.1”)

‘ 2. セマンティックバージョニング風の解析
dotPos = InStr(currentVersion, “.”)
If dotPos > 0 Then
majorVer = CLng(Left(currentVersion, dotPos – 1))
minorVer = CLng(Mid(currentVersion, dotPos + 1))
Else
majorVer = 1
minorVer = 0
End If

‘ 3. 版数のインクリメントロジック
If isMajorUpdate Then
majorVer = majorVer + 1
minorVer = 0
Else
minorVer = minorVer + 1
End If

currentVersion = CStr(majorVer) & “.” & CStr(minorVer)

‘ 4. カスタムプロパティへの永続化
SetCustomProperty targetDoc, PROP_VERSION, currentVersion, msoPropertyTypeString
SetCustomProperty targetDoc, PROP_UPDATED_BY, Environ$(“USERNAME”), msoPropertyTypeString
SetCustomProperty targetDoc, PROP_UPDATED_DATE, Format(Now, “yyyy-mm-dd hh:nn:ss”), msoPropertyTypeString

‘ 5. 文書内のコントロール(ContentControlsなど)への同期もここで行うと完璧
Call SyncContentControl(targetDoc, “CC_Version_Display”, currentVersion)

End Sub

‘ 補助関数: 文字列プロパティの安全な取得
Private Function GetCustomPropertyString(ByRef targetDoc As Document, ByVal propName As String, ByVal defaultValue As String) As String
Dim prop As Office.DocumentProperty
On Error GoTo ErrorHandler

For Each prop In targetDoc.CustomDocumentProperties
If StrComp(prop.Name, propName, vbTextCompare) = 0 Then
GetCustomPropertyString = CStr(prop.Value)
Set prop = Nothing
Exit Function
End If
Next prop

GetCustomPropertyString = defaultValue
Exit Function

ErrorHandler:
GetCustomPropertyString = defaultValue
End Function

‘ 補助関数: コンテンツコントロール(文書内の表地など)との同期
Private Sub SyncContentControl(ByRef targetDoc As Document, ByVal ccTag As String, ByVal value As String)
Dim cc As ContentControl
For Each cc In targetDoc.ContentControls
If cc.Tag = ccTag Then
cc.Range.Text = value
Exit For
End If
Next cc
Set cc = Nothing
End Sub

4. レガシー環境と外部システム連携(API・RPA連携の極意)

この仕組みの真価は、Word単体で完結させない点にある。カスタムプロパティにメタデータを刻み込むことで、Wordを起動することなく、外部のC#アプリケーションやPowerShell、Pythonから文書のステータスを高速に読み取ることが可能になる。

以下は、PowerShellを用いて、Wordプロセスを完全に隠蔽(Headlessに近い状態)し、COM経由でカスタムプロパティを高速抽出するスニペットだ。社内システムやRPA(UiPath等)から文書管理サーバーへメタデータを吸い上げる際のデザインパターンとして活用してほしい。

==============================================================================
PowerShellによる高速メタデータ抽出(Wordプロセス非表示)
==============================================================================
$filePath = “C:\Documents\Specification_v1.docx”
$word = New-Object -ComObject Word.Application
$word.Visible = $false
$word.DisplayAlerts = 0 # 警告ダイアログの抑制(破損チェック等のフリーズを防ぐ)

try {
# 読み取り専用かつ構造を破壊しないオープン
$doc = $word.Documents.Open($filePath, $false, $true)

$props = @{}
foreach ($prop in $doc.CustomDocumentProperties) {
$props[$prop.Name] = $prop.Value
}

# JSONとして出力し、上位システムへ引き渡す
$props | ConvertTo-Json -Compress

$doc.Close([Microsoft.Office.Interop.Word.WdSaveOptions]::wdDoNotSaveChanges)
}
finally {
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc) | Out-Null
$word.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($word) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}

チーフアーキテクトからの警鐘

レガシーなOffice自動化において最も恐ろしいのは、例外発生時にCOMオブジェクトが宙ぶらりんになり、タスクマネージャーに `WINWORD.EXE` が無限増殖していく現象だ。

上記PowerShellコードのように、取得したCOMオブジェクトは必ず逆順で `Marshal.ReleaseComObject` にかけ、ガベージコレクションを明示的に誘発させること。VBA内であっても、エラーハンドラを通じた `Set xxx = Nothing` の徹底は、24時間稼働するサーバーサイドや、数千ファイルを一括処理するバッチプログラムにおいて生死を分ける。

カスタムプロパティによる版管理の自動化は、単なる「便利なマクロ」ではない。文書という非構造化データを、エンタープライズなシステム群と強固に結合させるためのエレガントかつ堅牢なアーキテクチャなのである。現場のコードに直ちに組み込み、その優位性を体感してほしい。

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