【テクニカル・上級編】【上級者向け】段落の「フォント」設定を、Wordの「スタイル」の継承関係を考慮して動的に計算する – Word VBA解析バイブル

スポンサーリンク

【上級者向け】Wordの「スタイル継承ツリー」をコードで解体せよ:最終フォント動的解決エンジンの実装

Word VBAにおいて、`Paragraph.Range.Font` プロパティから取得できる値は、必ずしもその段落の「実際の見た目」を正確に表しているわけではない。

初心者はここでつまずく。`Font.Name` が空欄(Nullや空文字列)であれば、「フォントが設定されていない」と誤解し、直接プロパティに値を代入してドキュメントを汚染していく。結果、何千行もあるドキュメントのスタイルは崩壊し、メンテナンス不能のレガシーモンスターが誕生する。

プロのエンジニアが理解すべき大前提は、Wordの書式は「多重継承のツリー構造」によって支配されているという事実だ。
直接指定(Direct Formatting)、段落スタイル(Paragraph Style)、文字スタイル(Character Style)、そしてベーススタイル(BasedOn)の連鎖。これらをすべて解決し、「最終的にどのフォント名、サイズ、太字がレンダリングされるのか」をプログラム上で算出するエンジンを、我々は構築しなければならない。

今回は、Wordのスタイル継承メカニズムの深層に切り込み、COMオブジェクトの寿命管理と再帰アルゴリズムを極限まで最適化した「スタイル解決エンジン」の全貌を公開する。

1. Wordスタイル継承の構造的欠陥と解決アプローチ

Wordのスタイル(`Style` オブジェクト)は、親スタイル(`BaseStyle`)を持つことができる。例えば、「見出し 2」は「見出し 1」をベースにし、「標準」スタイルを根幹としている。

しかし、VBAからこれらを単純に辿ろうとすると、以下の壁にぶつかる。

1. 直接指定の優先度: 段落や文字に直接適用された書式は、スタイルの継承チェーンよりも優先される。
2. プロパティの継承不完全性: `Style.Font` から取得できるプロパティは、未設定の部分がデフォルト値(または親の継承値)ではなく、初期状態(Null)を返す。
3. COM解放漏れによるメモリリーク: 再帰処理やループ内で `Style` や `Range` オブジェクトを安易に取得・放棄すると、Wordのプロセス内に隠れCOMオブジェクトが残留し、巨大な文書の処理時にVBAがクラッシュする。

これらを解決するため、我々は「継承チェーンの逆算アルゴリズム」「厳格なオブジェクト参照管理」を実装する。

2. 実装コード:動的フォント解決エンジン

以下のコードは、指定された段落が最終的にどのフォント名(Name)とフォントサイズ(Size)で描画されるのかを、スタイルの継承元を再帰的に遡って算出する関数群である。

メモリリークを防ぐため、オブジェクト変数は確実に解放し、`.Name` や `.Size` が未定(`wdUndefined`)の場合は親スタイルへフォールバックするロジックを組み込んでいる。

Option Explicit

‘ 最終的に解決されたフォント情報を保持する構造体
Public Type ResolvedFont
Name As String
Size As Single
Bold As Boolean
Italic As Boolean
SourceType As String ‘ どこから継承されたかのデバッグ情報
End Type

‘ =========================================================================
‘ 外部公開関数: 指定段落の最終的なフォント設定を動的に解決する
‘ =========================================================================
Public Function GetEffectiveFont(ByVal targetPara As Paragraph) As ResolvedFont
Dim result As ResolvedFont
Dim currentStyle As Word.Style
Dim directFont As Word.Font

On Error GoTo ErrorHandler

‘ 初期値の設定
result.Name = “”
result.Size = 0
result.Bold = False
result.Italic = False
result.SourceType = “None”

‘ 1. 文字レベルの直接指定(Direct Formatting)を最優先でチェック
Set directFont = targetPara.Range.Font

If Not IsNull(directFont.Name) And directFont.Name <> “” Then
result.Name = directFont.Name
result.SourceType = “Direct (Range)”
End If

If Not IsNull(directFont.Size) And directFont.Size > 0 Then
result.Size = directFont.Size
End If

If Not IsNull(directFont.Bold) Then
result.Bold = CBool(directFont.Bold)
End If

If Not IsNull(directFont.Italic) Then
result.Italic = CBool(directFont.Italic)
End If

‘ 2. 段落スタイルの取得と継承チェーンの遡上
Set currentStyle = targetPara.Style

‘ 名前やサイズが未解決の場合、スタイルツリーを遡る
Call ResolveStyleRecursive(currentStyle, result)

‘ 3. それでも決まらない場合のドキュメントデフォルト(Normalスタイル等)へのフォールバック
If result.Name = “” Then
result.Name = targetPara.Range.Document.Styles(“標準”).Font.Name
result.SourceType = result.SourceType & ” -> Default(Normal)”
End If

If result.Size = 0 Then
result.Size = targetPara.Range.Document.Styles(“標準”).Font.Size
End If

CleanUp:
‘ オブジェクトの明示的解放
Set directFont = Nothing
Set currentStyle = Nothing
GetEffectiveFont = result
Exit Function

ErrorHandler:
Debug.Print “Error in GetEffectiveFont: ” & Err.Description
Resume CleanUp
End Function

‘ =========================================================================
‘ 内部プロシージャ: スタイルツリーを再帰的に遡る
‘ =========================================================================
Private Sub ResolveStyleRecursive(ByVal st As Word.Style, ByRef res As ResolvedFont)
If st Is Nothing Then Exit Sub

Dim styleFont As Word.Font
Set styleFont = st.Font

‘ フォント名の解決(未設定なら親へ)
If res.Name = “” Then
If Not IsNull(styleFont.Name) And styleFont.Name <> “” Then
res.Name = styleFont.Name
res.SourceType = “Style: ” & st.NameLocal
End If
End If

‘ フォントサイズの解決
If res.Size = 0 Then
If Not IsNull(styleFont.Size) And styleFont.Size > 0 Then
res.Size = styleFont.Size
End If
End If

‘ 太字の解決
If Not res.Bold Then
If Not IsNull(styleFont.Bold) Then
res.Bold = CBool(styleFont.Bold)
End If
End If

‘ イタリックの解決
If Not res.Italic Then
If Not IsNull(styleFont.Italic) Then
res.Italic = CBool(styleFont.Italic)
End If
End If

‘ 親スタイル(BaseStyle)が存在する場合はさらに遡る
If Not st.BaseStyle Is Nothing Then
‘ 無限ループ防止のための簡易ガード(Wordの標準では循環参照は起きないが念のため)
If st.NameLocal <> st.BaseStyle.NameLocal Then
Call ResolveStyleRecursive(st.BaseStyle, res)
End If
End If

Set styleFont = Nothing
End Sub

3. シニアエンジニアが知るべき実装の急所

上記のコードが、単なるリファレンスの継ぎ接ぎと何が違うのか。現場で生きるエンジニア向けにその設計思想を解説する。

3.1 `IsNull` によるプロパティの「未定義判定」

Word VBAのフォントプロパティは、Boolean型であっても `Null` を返すことがある(複数選択範囲や、スタイルで値が上書きされていない状態)。これを通常の条件分岐(`If font.Bold = True Then` 等)で評価すると、内部で型ミスマッチや予期せぬ評価エラーを引き起こす。
`IsNull()` 関数で明示的に「このレイヤーでは値が定義されていない(=親に委譲する)」と判定することが、正確な継承解決の絶対条件である。

3.2 COMオブジェクトの参照切断(Memory Optimization)

VBAのガベージコレクタは気まぐれだ。特にループ内で `Paragraph.Style` や `Style.BaseStyle` を次々と参照していくと、COMラッパーの参照カウントが適切にデクリメントされず、ExcelやWordプロセスが肥大化する。
プロシージャの抜け際(`CleanUp` ラベル)で、取得したすべてのオブジェクト変数に `Set xxx = Nothing` を明示的に代入する。これは大規模な文書(数百ページの仕様書など)をバッチ処理するシステムにおいては必須の作法である。

4. 実戦での活用シーン:システム間連携とプレフライトチェック

この動的解決エンジンは、単に文字列表示をするためだけのものではない。以下のようなエンタープライズ環境で真価を発揮する。

  • PDF/HTML変換前のプリフライトチェック:

社内ニッチなフォント(例: 游ゴシック以外のレガシーフォントや外字フォント)が段落に紛れ込んでいないか、最終解決後の `Name` を走査して自動検出し、警告を出すシステム。

  • Markdown/JSONへのドキュメント構造抽出:

Word文書をRAG(Retrieval-Augmented Generation)やLLMの入力用にチャンク分割する際、スタイルの継承を踏まえた正確な文字装飾メタデータを付与し、AIの文脈理解精度を極限まで高める。

総括

Word VBAは、GUIの裏側に隠されたCOMの挙動を完全に手綱に握った者だけが、その真のパフォーマンスを引き出すことができる。
「なんとなく動くコード」ではなく、「構造を理解し、メモリと計算量を支配したコード」を書くこと。それこそが、レガシーシステムを支えるシニアエンジニアのプライドである。

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