MSDNフォーラムに、“変更履歴をオンにした状態で文字列を置換するマクロを実行したところ、履歴として残っている削除済みの文字列もヒットしてしまうので上手く処理できない”、との質問がありました。
これはWordマクロを扱う人の間ではよく知られるWordの仕様で、Word MVPの新田さんのブログにサンプルコードコードも含めて対処法がまとめられています。
変更履歴をオンにしたままで文字修正(置換や書き換えなど)を行うマクロを実行する場合には注意が必要ですね。他のマクロでも同じことが起こりえます。
以下の2つにご注意ください。
1.変更履歴をオンにした場合、文字列を書き直した場合でも履歴として削除したはずの文字列がデータとして残ります。そこで、削除済みの文字列を検索対象から除外する場合には、削除済みの文字列を非表示にします。
ところが、Rangeオブジェクトで文字列を検索すると、削除済みの文字列を非表示にした場合でもヒットしてしまいます。Selectionオブジェクトで検索をする場合には、削除済みの文字列を非表示にしておけばヒットしません。
2.変更履歴をオンにした場合、ワイルドカードを用いた置換は誤変換が起こることがあります。
https://www.wordvbalab.com/code/1945/ より
要するに変更履歴の表示をオフにしてから処理すれば良いわけですが、ふと「変更履歴の種類を判別して“削除”を除外すれば良いのではないか?」と思いついたので、実際に試してみることにしました。
(Revision).Typeプロパティによる変更履歴の種類判定
さっそく書いたテスト用のコードが下記になります。
本文中の「Word」文字列を検索して、ヒットしたRangeオブジェクトから変更履歴を表すRevisionオブジェクトを取得、Typeプロパティで変更履歴の種類を取得するコードです。
Public Sub Test() Dim rng As Word.Range Set rng = ActiveDocument.Range With rng.Find .Text = "Word" .Forward = True .Wrap = wdFindStop .Format = False .MatchFuzzy = False .MatchCase = False .MatchWholeWord = False .MatchByte = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = False Do While .Execute rng.Select Debug.Print "Revisions.Count:" & rng.Revisions.Count, _ "Revision.Type:" & rng.Revisions(1).Type Stop Loop End With End Sub
実行した結果は下図の通りです。
種類はすべて“削除”を表す「2 (wdRevisionDelete)」となり、それどころか変更履歴が無い部分さえも変更履歴数が「1」となっています。
なんでだよ!?
思わずツッコんでしまいます。
さすが有名なWordの挙動、一筋縄ではいかないようです。
XMLからの情報取得
このようなとき、私はよくSelectionオブジェクトやRangeオブジェクトのXMLプロパティから取得できる情報を使います(下記記事参照)。
今回も変更履歴の“削除”と“挿入”でXMLの比較をしたところ、aml:annotation要素のw:type属性の値で判別できそうだという事が分かりました。
“削除”のXML
<w:p wsp:rsidR="009F28A1" wsp:rsidRDefault="00D0652C"> <aml:annotation aml:id="0" w:type="Word.Deletion" aml:author="***" aml:createdate="2021-01-28T10:32:00Z"> <aml:content> <w:r wsp:rsidDel="00227F40"> <w:rPr> <w:rFonts w:hint="fareast"/> </w:rPr> <w:delText>〇〇〇</w:delText> </w:r> </aml:content> </aml:annotation> </w:p>
“挿入”のXML
<w:p wsp:rsidR="00AB545F" wsp:rsidRDefault="00982971"> <aml:annotation aml:id="0" w:type="Word.Insertion" aml:author="***" aml:createdate="2021-01-28T14:24:00Z"> <aml:content> <w:r> <w:t>〇〇〇</w:t> </w:r> </aml:content> </aml:annotation> </w:p>
これを受けて最終的に出来上がったのが下記のコードです。
変更履歴をオンにしたままで文字列の置換を行うWordマクロ
Option Explicit Public Sub Sample() Const SrcText As String = "Word" '置換元文字列 Const DestText As String = "WordXXX" '置換先文字列 ActiveDocument.Range(0, 0).Select With Selection.Find .ClearFormatting .Text = SrcText .Forward = True .Wrap = wdFindStop .Format = False .MatchFuzzy = False .MatchCase = False .MatchWholeWord = False .MatchByte = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = False Do While .Execute If Selection.Range.Revisions.Count > 0 Then Select Case LCase(GetAnnotationType(Selection.XML)) Case LCase("Word.Deletion") '変更履歴・削除を除外 Case Else Selection.Text = DestText Selection.Collapse Direction:=wdCollapseEnd End Select Else Selection.Text = DestText Selection.Collapse Direction:=wdCollapseEnd End If DoEvents Loop End With Selection.HomeKey Unit:=wdStory MsgBox "処理が終了しました。", vbSystemModal + vbInformation End Sub Private Function GetAnnotationType(ByVal SourceXML As String) As String Dim elm As Object Dim ret As String With CreateObject("MSXML2.DOMDocument") .async = False If .LoadXML(SourceXML) = True Then If .getElementsByTagName("aml:annotation").Length > 0 Then Set elm = .getElementsByTagName("aml:annotation").Item(0) ret = elm.getAttribute("w:type") End If End If End With GetAnnotationType = ret End Function
実行結果は下図の通りで、意図した通りに文字列を置き換えることができました。
というわけで、変更履歴の種類を判別して無事に置換処理できることが確認出来たわけですが、最初に挙げた、新田さんが紹介されている対処法の方が圧倒的にシンプルで良いですね!
この記事へのコメントはありません。