【テクニカル・上級編】【中級者向け】表の各行をループし、特定のキーワードが含まれる行を赤字にする強調表示マクロ – Word VBA解析バイブル

スポンサーリンク

1. はじめに:なぜWordの表走査は「重く、壊れやすい」のか

社内帳票や契約書、技術仕様書など、Wordドキュメントの表(Table)を対象にした一括処理マクロは、基幹系バッチやドキュメント生成エンジンにおいて今なお不可欠な存在です。しかし、Excel VBAの感覚でWordの`Table`オブジェクトにアクセスすると、現場のエンジニアは二つの大きな壁に直面します。

1. 破滅的な実行速度の低下:数百行の表を走査するだけで、画面の再描画とCOMマーシャリングのオーバーヘッドによりCPUコアが1つ100%に張り付き、処理がフリーズ同然になる。
2. 謎の文字列判定不一致:画面上は見えている文字列に対して`InStr`をかけてもヒットしない、あるいは改行位置が狂う。

これは、Wordのオブジェクトモデルが「印刷レイアウトの即時描画」を最優先に設計されている点、そしてWord固有のセル終了マーカー(Chr(13) & Chr(7))の挙動を正しくコントロールできていない点に起因します。

本稿では、指定キーワードを含む行を動的に赤字強調する処理を題材に、シニアエンジニアやシステム管理者が担保すべき「エンタープライズ品質のWord走査アーキテクチャ」を完全解説します。

—

2. アーキテクチャ視点の最適化戦略

2.1 セルマーカー(Chr(13) & Chr(7))の排除

Wordのセル内テキスト(`Cell.Range.Text`)を取得すると、末尾に必ず長さ2文字の制御コード(キャリッジリターン `Chr(13)` + ベル文字 `Chr(7)`)が付与されます。これが付いた状態のまま文字列比較や正規表現エンジンに渡すと、予期せぬマッチング失敗を招きます。文字列末尾の2文字を物理的にスライスして切り捨てる処理が不可欠です。

[ セル内のテキスト実体 ] + [ vbCr (0x0D) ] + [ ベル文字 (0x07) ]
└── この2バイトを厳密に切り落とす ──┘

2.2 Win32 APIによる描画パイプラインの完全停止

Wordの`Application.ScreenUpdating = False`は、Tableオブジェクトへの破壊的変更やフォント変更が頻発すると、内部的に部分再描画を呼び出してしまい完全に画面フリーズを抑止できないケースがあります。
確実なパフォーマンスを担保するため、Win32 APIの`LockWindowUpdate`を用いてWordのウィンドウハンドルの描画エンジンを強制ロックし、処理完了後に高精度に復旧します。

2.3 セル結合(Merged Cells)の地雷:Error 5991

表内にセル結合が存在する場合、`Table.Rows(i)`という行単位のインデックスアクセスは実行時エラー 5991(「コレクションの個々の行にはアクセスできません。行の高さが均等でない可能性があります」)をスローして即死します。
堅牢なコードを組む場合、「行」からループするのではなく、「セル」の座標走査に切り替えるか、エラーをトラップして`Cell.RowIndex`から動的にその行のRangeを再構築する防壁設計が必要です。

—

3. プロダクション仕様の実装コード

以下のコードは、数千行規模の帳票でも瞬時にキーワード走査を行い、対象行全体のフォントカラーを`wdColorRed`へ変更する堅牢な実装です。COM参照カウントの蓄積を防ぐため、オブジェクトはスコープ終了時に確実に明示的解放を行います。

Option Explicit

If VBA7 Then
‘ 64-bit Office環境用 API宣言
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hWndLock As LongPtr) As Long
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
‘ 32-bit レガシー環境用 API宣言
Private Declare Function LockWindowUpdate Lib “user32” (ByVal hWndLock As Long) As Long
Private Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32″ (ByRef lpFrequency As Currency) As Long
End If

”’

”’ 特定キーワードを内包する行全体を高速に赤字ハイライトする
”’

”’ 走査対象のWord Tableオブジェクト ”’ 検出対象文字列 Public Sub HighlightRowsByKeyword(ByVal targetTable As Word.Table, ByVal searchKeyword As String)
On Error GoTo ErrHandler

If targetTable Is Nothing Then Exit Sub
If LenB(searchKeyword) = 0 Then Exit Sub

‘ 高精度タイマー初期化
Dim freq As Currency, startTime As Currency, endTime As Currency
QueryPerformanceFrequency freq
QueryPerformanceCounter startTime

‘ アプリケーション設定の退避
Dim prevScreenUpdating As Boolean
prevScreenUpdating = Application.ScreenUpdating
Application.ScreenUpdating = False

‘ Win32 APIによる描画完全固定
#If VBA7 Then
Dim wordHWnd As LongPtr
wordHWnd = Application.hWnd
#Else
Dim wordHWnd As Long
wordHWnd = Application.hWnd
#End If
Call LockWindowUpdate(wordHWnd)

‘ 内部走査用変数
Dim targetDoc As Word.Document
Set targetDoc = targetTable.Parent

Dim tblRange As Word.Range
Set tblRange = targetTable.Range

Dim rowCount As Long
Dim rIdx As Long
Dim cIdx As Long
Dim colCount As Long

‘ 結合セル起因のエラー5991を回避するためRows.Countを検証
Dim isUniformGrid As Boolean
isUniformGrid = True
On Error Resume Next
rowCount = targetTable.Rows.Count
If Err.Number <> 0 Then
isUniformGrid = False
Err.Clear
End If
On Error GoTo ErrHandler

If Not isUniformGrid Then
‘ 非対称・結合セル含有テーブルのフォールバック走査
Call ScanNonUniformTable(targetTable, searchKeyword)
GoTo FinalizeProcess
End If

‘ — 対称テーブル(均一グリッド)の超高速走査 —
Dim currentRowRange As Word.Range
Dim currentCellRange As Word.Range
Dim cellText As String
Dim isMatched As Boolean

colCount = targetTable.Columns.Count

For rIdx = 1 To rowCount
isMatched = False

‘ 列方向にスキャン(早期脱出アルゴリズム)
For cIdx = 1 To colCount
Set currentCellRange = targetTable.Cell(rIdx, cIdx).Range
cellText = currentCellRange.Text

‘ Word固有のセルマーカー(Chr(13) & Chr(7))を末尾からパージ
If Len(cellText) > 2 Then
cellText = Left$(cellText, Len(cellText) – 2)

‘ 大文字・小文字を区別しないバイナリ/テキスト混在高速比較
If InStr(1, cellText, searchKeyword, vbTextCompare) > 0 Then
isMatched = True
Set currentCellRange = Nothing
Exit For ‘ 該当行で1つでもヒットすれば列ループを即座にブレーク
End If
End If

Set currentCellRange = Nothing
Next cIdx

‘ 条件合致時、行全体のフォントカラーを動的に変更
If isMatched Then
Set currentRowRange = targetTable.Rows(rIdx).Range

‘ 行全体の色変更(プロパティの一括注入)
With currentRowRange.Font
.Color = wdColorRed
.Bold = True
End With

Set currentRowRange = Nothing
End If
Next rIdx

FinalizeProcess:
‘ API描画ロック解除
Call LockWindowUpdate(0)
Application.ScreenUpdating = prevScreenUpdating

‘ COM明示的解放
Set currentCellRange = Nothing
Set currentRowRange = Nothing
Set tblRange = Nothing
Set targetDoc = Nothing

‘ 実行メトリクスの出力
QueryPerformanceCounter endTime
Debug.Print “走査完了: ” & Format$((endTime – startTime) / freq, “0.0000”) & ” 秒”
Exit Sub

ErrHandler:
‘ 異常発生時の確実なUI復帰
Call LockWindowUpdate(0)
Application.ScreenUpdating = prevScreenUpdating

Set currentCellRange = Nothing
Set currentRowRange = Nothing
Set tblRange = Nothing
Set targetDoc = Nothing

Debug.Print “[Error ” & Err.Number & “] ” & Err.Description
Err.Raise Err.Number, Err.Source, Err.Description
End Sub

”’

”’ 結合セルを含む不規則な表に対するフォールバック走査ルーチン
”’

Private Sub ScanNonUniformTable(ByVal targetTable As Word.Table, ByVal searchKeyword As String)
Dim cellObj As Word.Cell
Dim cellText As String
Dim matchedRows As Object
Set matchedRows = CreateObject(“Scripting.Dictionary”)

‘ 全セルをフラットに反復
For Each cellObj In targetTable.Range.Cells
cellText = cellObj.Range.Text
If Len(cellText) > 2 Then
cellText = Left$(cellText, Len(cellText) – 2)
If InStr(1, cellText, searchKeyword, vbTextCompare) > 0 Then
‘ 行インデックスをディクショナリのキーとして退避(重複防止)
matchedRows(cellObj.RowIndex) = True
End If
End If
Next cellObj

‘ ヒットした行のセルを再着色
Dim targetRowIndex As Variant
For Each cellObj In targetTable.Range.Cells
If matchedRows.Exists(cellObj.RowIndex) Then
With cellObj.Range.Font
.Color = wdColorRed
.Bold = True
End With
End If
Next cellObj

Set matchedRows = Nothing
End Sub

—

4. チーフアーキテクトが教える勘所と落とし穴

4.1 ガベージコレクションとCOM参照リーク

.NETやJavaと異なり、VBAは単純な参照カウント(Reference Counting)でオブジェクト寿命を管理しています。Wordの`Cell.Range`や`Row.Range`は呼び出すたびに内部で新しいCOMラッパーオブジェクトを生成します。

‘ 良くない実装例
targetTable.Cell(r, c).Range.Font.Color = wdColorRed

このように1行で記述すると、中間に生成された隠蔽`Range`オブジェクトが即座に解放されず、数万回のループの中でメモリを圧迫し、Officeクラッシュの引き金になります。大規模処理を回す際は、前述のコードのように`Dim currentCellRange As Word.Range`へ明示的に格納し、使用後に`Set currentCellRange = Nothing`で確実に参照カウントをデクリメントさせてください。

4.2 バックグラウンドバッチ処理(Word非表示実行)の注意点

基幹システムからC#やPowerShellを介してWordを裏で起動(`Visible = False`)し、当マクロを流す運用を行う場合、以下のAPI設定に配慮が必要です。

  • `LockWindowUpdate`は可視状態のウィンドウハンドルに対して有効です。バックグラウンド実行(`Visible = False`)の場合は`Application.hWnd`が`0`を返すか、無効なハンドルとなる場合があるため、明示的に`If Application.Visible Then Call LockWindowUpdate(…)`とガードを設けるのがベストプラクティスです。

—

5. まとめ

  • 制御文字の除去:Wordのセル内文字列は末尾2文字(`vbCr & Chr(7)`)を削ぎ落としてから評価する。
  • Win32描画排他:`LockWindowUpdate`により、Word内部の無駄なUI更新スレッドを物理的に遮断する。
  • 結合セル例外の回避:均一グリッドの確認を先に行い、結合セルが存在する場合は`Cells`の全走査と`RowIndex`のマップ走査へ自動フォールバックさせる。
  • COMリソースの即時解放:ループ内で生成される`Word.Range`は1反復ごとに明示的`Nothing`化を徹底する。

これらを徹底することで、どれほど複雑で肥大化した社内帳票であっても、クラッシュ知らずの高速・堅牢な自動フィルタリング処理を実現できます。既存資産の保守や新規ツール設計にぜひ役立ててください。

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