初めてのIE操作(3)~HTMLソースをファイルに出力するメソッド

WebページのHTMLソースを出力するメソッド

前々回、

akashi-keirin.hatenablog.com

前回

akashi-keirin.hatenablog.com

の続き。

Webページ/アプリを操作するには、HTMLがどうなっているのかを知らなければどうにもならない。

ただし、前回すでに起動中のIEオブジェクトを捕まえて、Documentオブジェクトを取得するところまで行っているので、楽勝である。

targetIE.Document.all(0).outerHTML

たったこれだけ。

さすがにこれだけだと面白くないので、取得したHTMLソース文字列をファイルに出力できるようにした。

コード

リスト1 標準モジュール
Private Sub appendTextToFile(ByVal fileFullName As String, _
                            ByVal targetText As String)
  Dim n As Long
  n = FreeFile(0)
  Open fileFullName For Append As n
    Print #n, targetText
  Close #n
End Sub

コイツを利用して、取得したHTMLソースをファイルとして保存するようなFunctionを作る。

スト2 標準モジュール
Public Sub createHTMLSource(ByVal targetIE As InternetExplorer, _
                            ByVal sourceName As String)
  Call appendTextToFile( _
         ThisWorkbook.Path & "\" & sourceName & "_src.html", _        "
         targetIE.Document.all(0).outerHTML)
  Set targetIE = Nothing
End Sub

Document.all(0).outerHTMLで取得した文字列をappendTextToFileメソッドに渡して、ThisWorkbookと同じディレクトリにファイルを出力するメソッド。

出力場所まで指定したければ引数を増やせば良い。

使ってみる

次のコードで実験。

リスト3 標準モジュール
Public Sub test()
  Dim targetIE As New InternetExplorer
  With targetIE
    .Visible = True
    Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606")
    Do While .Busy Or _
             .readyState <> READYSTATE_COMPLETE
      DoEvents
    Loop
  End With
  Set targetIE = getIEByTitle("素人が")
  Call createHTMLSource(targetIE, "akashi_keirin")
End Sub

例によって、テキトーにブレークポイントを設置して、手動待機(w)しながら実行してみる。

実行結果

実行してみると、ThisWorkbookディレクトリに、

f:id:akashi_keirin:20181216205123j:plain

この通りakashi_keirin_src.htmlというファイルができている。

エディタで開いてみると、

f:id:akashi_keirin:20181216205134j:plain

一瞬ぎょっとするような画像だが、よく見ると確かにHTMLソースである。

ここまで来たらリーチがかかったようなもん。オープンリーチかも知れんけど。

HTMLの要素を解析して、操作対象を特定して操作するだけ。それは次回。

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

初めてのIE操作(2)~起動中のIEを取得するFunction

ウインドウのタイトルを元に起動中のIEを取得するFunction

前回

akashi-keirin.hatenablog.com

の続き。

ページのURLや、HTMLソースが分からないときがある。

Webアプリで移動した先のページなんかの場合だ。たいていアドレスバーが切られていたりするし、右クリックしても、ショートカットキー([Ctrl]+[U])を押してもソースが表示されない。

そんな場合にどうするか。

ウインドウが開いているのだから、それを頼りにIEオブジェクトを取得し、そのHTMLドキュメントオブジェクトから取得すれば良い。

そのためには、まず起動中のIEを取得する必要がある。

この件についてはコチラのページを大いに参考にさせていただいた。

既に開かれているInternetExplorerを取得する(起動済みIEオブジェクトの取得)

開いているウインドウを片っ端から調べ、それが「Internet Explorer」であり、かつウインドウのタイトルが一致していればオブジェクトとして取得する、というやり方。

コード

前回のMicrosoft HTML Object LibraryMicrosoft Internet Controlsに加え、Microsoft Shell Controls And AutomationMicrosoft Scripting Runtimeにチェックを入れている。

リスト1 標準モジュール
Public Function getIEByTitle( _
                  ByVal targetTitleKey As String) As InternetExplorer
  Dim ieApp As New InternetExplorer
  Dim shellApp As New Shell
  Dim shellWin As Object
  For Each shellWin In shellApp.Windows    '……(1)'
    If shellWin.Name = "Internet Explorer" Then    '……(2)'
      On Error Resume Next    '……(3)'
      If InStr(1, shellWin.Document.Title, targetTitleKey) > 0 Then    '……(4)'
        If Err.Number = 0 Then    '……(5)'
          Set ieApp = shellWin
          Exit For
        End If
      End If
      On Error GoTo 0    '……(6)'
    End If
  Next
  If ieApp Is Nothing Then Exit Function
  Set getIEByTitle = ieApp    '……(7)'
End Function

Shellオブジェクトを使って、Windowsの「エクスプローラー」にアクセスする、というやり方。

(1)の

For Each shellWin In shellApp.Windows

で、Windowsの「エクスプローラー」に当たるものをしらみつぶしにする。

変数shellWinに入るのはInternetExplorer型のオブジェクトとは限らない(通常の「エクスプローラー」(フォルダを開いた時のアレ)の場合がある)ので、Object型にしている。

ここからForループ内部。

まず(2)の

If shellWin.Name = "Internet Explorer" Then

で、変数shellWin内のオブジェクトのNameプロパティの文字列を調べる。

オブジェクトの正体がIEオブジェクトなら、「Internet Exprorer」が返る。通常のエクスプローラーオブジェクトなら、「エクスプローラー」が返る。

Internet Explorer」以外の値が返っているならさっさと次へ行く。「Internet Explorer」が返っているなら第一関門突破。Then以下へ進む。

(3)で

On Error Resume Next

として、エラーが出ても無視して次へ進むようにしている。これは、(4)の

If InStr(1, shellWin.Document.Title, targetTitleKey) > 0 Then

を評価する際に、変数shellWinオブジェクトにDocumentプロパティがなかったりするとエラーになってしまうため。

エラーが出たからといってここで止まってしまっては、求めているShellのウインドウにたどり着けない。

本題に戻る。(4)の

If InStr(1, shellWin.Document.Title, targetTitleKey) > 0 Then

では、変数shellWinに入っているオブジェクトからDocumentオブジェクトを取得、そのTitleプロパティの文字列に、引数で渡したtargetTitleKeyの文字が含まれているかどうかを調べている。

含まれていれば、InStr関数が1以上の整数を返すので、Then以下に進む。

(5)からの4行

If Err.Number = 0 Then
  Set ieApp = shellWin
  Exit For
End If

では、エラーが出ていないか調べて、大丈夫だったら現在のshellWinを返り値用の変数ieAppにセットしてForループから脱する。

今にして思えば、エラーの状態を調べるというのは不要かも知れない……。

ちなみに、次のループに進む前に(6)の

On Error GoTo 0

エラーをリセットしておく。

最後に(7)の

Set getIEByTitle = ieApp

で捕まえたIEオブジェクトをreturnしておしまい。

使ってみる

次のコードで実行。

スト2 標準モジュール
Public Sub test()
  Dim targetIE As New InternetExplorer
  With targetIE
    .Visible = True
    Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606")
    Do While .Busy Or _
             .readyState <> READYSTATE_COMPLETE
      DoEvents
    Loop
  End With
  Set targetIE = getIEByTitle("素人が")    '……※'
  Debug.Print targetIE.Document.Title    '……※'
  Debug.Print targetIE.Document.locationURL
End Sub

リスト1のgetIEByTitleメソッドにキーワード「"素人が"」を渡して、起動中のIEオブジェクトを捕まえ、そのDocumentオブジェクトのTitleプロパティとlocationURLプロパティの値をイミディエイト・ウインドウに表示させるコード。

ちなみに、実行時は、ステップ実行で読み込み待ちの時間を調整した。

ページを読み込んで準備完了になるまでに次のコードを実行したりするとエラーになるので、本来はいろいろチューニングしないといけないんだけれど、めんどくさいので省略。

実行結果

上記コード中の※のところにブレークポイントを設定して、十分に待ち時間を取って実行した。

f:id:akashi_keirin:20181216202031j:plain

このとおり、ウインドウのタイトルと、ページのURLが表示されている。

WebアプリのページURLが分からないときは、この方法で取得が可能。

実は、ページのHTMLソースも、この時点で取得できたも同然だったりする。

そのあたりは次回。

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

初めてのIE操作(1)

初めてのIE操作(1)

IEベースの業務用Webアプリの操作がくっそダルかったので、生まれて初めてVBAからのIE操作にチャレンジしてみたら、意外と簡単だったので、報告。

業務用Webアプリ操作のために私が乗り越えなければならなかった壁は以下の通り。

  1. VBAからIEを起動して目的のページを表示する
  2. 表示したページのリンクをクリックして、入力画面に移動する
  3. 移動先のページで諸データを入力する
  4. 入力データ確認のためのボタンをクリックする

この後、入力・確認したデータをサーバに送信するためにボタンをクリックしないといけないが、さすがにそこまで自動にするのは怖いのでとりあえず以上の4点。

厄介だったのが、ページのソースが通常の方法では表示されなかったこと。

その辺は、本編で追々……。

Webアプリ操作のために必要なこと

先にまとめておく。

まず、操作したいWebアプリのページのHTMLを取得することが必要。

次に、取得したHTMLソースを解析して、どの要素にどのような操作をすれば良いかを探る。

基本的にこれだけできたら良いだけなので、思っていたより簡単だった。

IEを起動してページを表示する

これは簡単。

基本的に、InternetExplorerオブジェクト(以下「IEオブジェクト」と言う。)を取得しさえすれば、あとはIEオブジェクトのNavigateメソッドを実行するだけ。

コードは、

Dim targetIE As New InternetExplorer
Call targetIE.Navigate("サイトのURL")

とするだけ。

WebアプリのページURLが分かるならこれだけでページを呼び出すことはできる。

URLが分からない場合は、ちょっとややこしいやり方が必要になる。

HTMLソースが通常の方法で表示されない場合と同じ苦労がいる。言い方を変えれば、HTMLソース問題さえ解決すれば、同時に解決するということ。たいした問題ではない。

実験

とりあえず、次のコードでVBAからIEを呼び出してみる。

その前に、参照設定をしておくと便利。

f:id:akashi_keirin:20181216170517j:plain

このように、Microsoft HTML Object LibraryMicrosoft Internet Controlsにチェックを入れておく。

リスト1 標準モジュール
Public Sub test()
  Dim targetIE As New InternetExplorer
  With targetIE
    .Visible = True
    Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606")
	End With
End Sub

VisibleプロパティをTrueにしているので、起動したIEは画面表示される。

Navigateメソッドの引数には拙ブログの前回記事のURLを渡した。

コイツを実行すると、

f:id:akashi_keirin:20181216170529j:plain

ほれ、この通りIEで目標のページが表示された。

この後、開いたページにあれこれやっていくわけだが、それはまた次回。

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

マーカ部分をRange配列として取得する(Word)

Findオブジェクトを用いてマーカ部分を取得する

この記事

akashi-keirin.hatenablog.com

へのid:imihitoさんのコメントにより、Findオブジェクトを用いて「蛍光ペン」のところをRangeオブジェクトとして取得できるということを知った。

参考にしたのは『みんなのワードマクロ』様。

www.wordvbalab.com

WordVBAのFindオブジェクトというやつは、どうもイメージしづらくて苦手だったんだが、この際だからまじめに取り組んでみようと思った。

いや、実は過去に一度、非常にまじめに取り組んだことがあるんですけどね……。

akashi-keirin.hatenablog.com

マーカ部分を配列にして返すFunction

マーカ部分をRangeオブジェクトの配列として保持しておけば、あとはそのHighlightColorIndexプロパティを切り替えるだけで網掛けの有無を切り替えることができる。

Findオブジェクトを使って検索すると、ヒットした部分が選択された状態になるので、Selectionオブジェクトという形で取得が可能になる。

その都度、Range型の配列にぶち込んで行けば良いと思った。

リスト1 標準モジュール
Public Function getHighLightedRange( _
         ByVal targetDocument As Document) As Range()
  Application.ScreenUpdating = False
  Dim targetRange As Range    '……(1)'
  Set targetRange = Selection.Range
  Call targetDocument.Range(0, 0).Select    '……(2)'
  Call Selection.Find.ClearFormatting    '……(3)'
  Call Selection.Find.Replacement.ClearFormatting
  With Selection.Find    '……(4)'
    .Text = ""
    .Replacement.Text = ""
    .Forward = True
    .Wrap = wdFindContinue
    .Format = False
    .Highlight = True
    .MatchCase = False
    .MatchWholeWord = False
    .MatchByte = False
    .MatchAllWordForms = False
    .MatchSoundsLike = False
    .MatchWildcards = False
    .MatchFuzzy = False
  End With
  Call Selection.Find.Execute    '……(5)'
  Dim ar() As Range    '……(6)'
  Dim i As Long
  i = 0
  Do While Selection.Find.Found    '……(7)'
    ReDim Preserve ar(i)
    Set ar(i) = Selection.Range
    i = i + 1
    Call Selection.Collapse(Direction:=wdCollapseEnd)
    Call Selection.Find.Execute
  Loop
  Call targetRange.Select    '……(8)'
  getHighLightedRange = ar
  Application.ScreenUpdating = True
End Function

(1)の

Dim targetRange As Range
Set targetRange = Selection.Range

は、コード実行時のカーソル位置を覚えさせておくだけのもの。

(8)の

Call targetRange.Select

で、コード開始時のカーソル位置にカーソルを置くため。

別になくても構わないが、これをしておかないと、実行するたびにカーソルが最後にヒットした場所に行くことになる。

(2)の

Call targetDocument.Range(0, 0).Select

で、文書の先頭にカーソルを置く。

(3)からの2行

Call Selection.Find.ClearFormatting
Call Selection.Find.Replacement.ClearFormatting

は、Findオブジェクトの初期化。

で、大事なのは次。

(4)からの14行

With Selection.Find    '……(4)'
  .Text = ""
  .Replacement.Text = ""
  .Forward = True
  .Wrap = wdFindContinue
  .Format = False
  .Highlight = True
  .MatchCase = False
  .MatchWholeWord = False
  .MatchByte = False
  .MatchAllWordForms = False
  .MatchSoundsLike = False
  .MatchWildcards = False
  .MatchFuzzy = False
End With

で、Findオブジェクトの設定。

たくさんプロパティがあるが、綴り字でだいたい何を設定しているのかは見当が付くと思う。

ForwardプロパティとHighlightプロパティだけがTrueになっているのが分かると思う。

これを、

f:id:akashi_keirin:20181216001109j:plain

これと見比べれば、今回何をどう設定したのかが分かると思う。

これでFindオブジェクトの設定は終わったので、あとは(5)の

Call Selection.Find.Execute

FindオブジェクトのExecuteメソッドを実行する。

これは、上の画像の[次を検索]をクリックするのと同じ(Find.ForwardプロパティがTrueなので)。

まず、この時点で

f:id:akashi_keirin:20181216001124j:plain

こうなる。一つ目のマーカ部分が選択された状態。

ここからが処理の第二段階。

(6)からの3行

Dim ar() As Range
Dim i As Long
i = 0

で、ヒットしたRangeオブジェクトを受ける配列を準備。要素数が確定しないので、動的に宣言しておく。変数iは、後にReDimするために使う。

(7)からの7行

Do While Selection.Find.Found
  ReDim Preserve ar(i)
  Set ar(i) = Selection.Range
  i = i + 1
  Call Selection.Collapse(Direction:=wdCollapseEnd)
  Call Selection.Find.Execute
Loop

Do ~ Loopを用いて、マーカ部分をRangeオブジェクトとして拾い集めていく。

FindオブジェクトのFoundプロパティがTrueである場合、つまり、Find.Executeメソッドの結果、次のマーカ部分がヒットした場合にはブロック内に進むことになる。

ループ内部に入ると、まずReDim Preserveで配列の要素数を改定する。

Preserveを忘れると、非常にマヌケなことになるので注意。

Set ar(i) = Selection.Range

でヒットして選択されているRangeオブジェクトを配列に格納する。

次にループ内部に突入した場合に備えてiをインクリメントさせておいて、

Call Selection.Collapse(Direction:=wdCollapseEnd)

Selection.Collapseメソッドを使って複数選択状態になっているカーソルを文字のカンチャンに移動する。引数DirectionwdCollapseEndなので、選択されていた箇所の一番末尾にカーソルが移動する。

んで、最後に

Call Selection.Find.Execute

再度Find.Executeメソッドを実行。

これで新たにマーカ部分がヒットしたら、再度ブロック内に突入するし、すでにヒットする部分がなければループを抜けることになる。

ループを抜けたら、全てのマーカ部分が配列にぶち込まれたことになるから、(8)からの2行

Call targetRange.Select
getHighLightedRange = ar

で、もとあった位置にカーソルを戻し、配列をreturnしておしまい。

使ってみる

次のコードで使ってみる。

スト2 標準モジュール
Public Sub testGetHighLightedRange()
  Dim Doc As Document
  Set Doc = ActiveDocument
  Dim highLightedRange() As Range
  highLightedRange = getHighLightedRange(Doc)
  Dim i As Long
  For i = LBound(highLightedRange) To UBound(highLightedRange)
    Debug.Print highLightedRange(i).Text
  Next
End Sub

リスト1のgetHighLightedRangeの返り値として取得したRange配列をForループで回して、それぞれのTextプロパティの値をイミディエイト・ウインドウに、表示させる。

例によって、

f:id:akashi_keirin:20181216001137j:plain

このような文書を用意して実行してみる。

f:id:akashi_keirin:20181216001149j:plain

この通り、マーカ部分の文字列が表示された。

マーカ部分をRangeオブジェクトで取得できた証拠。

おわりに

これで、マーカのOn/Offが切り替え可能になった。

UndoRecordオブジェクトというものがある(Word)

UndoRecordオブジェクトというものがある

前回の

akashi-keirin.hatenablog.com

に、id:imihito さんからコメントをいただいた。

曰く、

また、Undoの履歴は、以下のようにするとひとまとめに出来たはずです
```vb
Application.UndoRecord.StartCustomRecord
'処理
Application.UndoRecord.EndCustomRecord
```

と。

な、なんだってー!(MMR風)

さっそくやってみた。

UndoRecordオブジェクトを使う

例によって公式のUndoRecordオブジェクトの項を見ると、

UndoRecord オブジェクト (Word)

元に戻すスタックにエントリ ポイントを提供します。

注釈

作成し、Word の [元に戻すスタックにユーザー定義の undo レコードを変更するには、 UndoRecordオブジェクトを使用します。

次のコード例は、 UndoRecordオブジェクトをインスタンス化します。

VB
Dim objUndo As UndoRecord 
Set objUndo = Application.UndoRecord

だそうである。機械的な飜訳だからか、異様に読みづらい。

英語版で見てみよう。

UndoRecord object (Word)

Provides an entry point into the undo stack.

Remarks

Use the UndoRecord object to create and modify custom undo records in the Word undo stack.

Example

The following code example instantiates an UndoRecord object.

VB
Dim objUndo As UndoRecord 
Set objUndo = Application.UndoRecord

Wordのアンドゥ情報を積んでおく場所への入り口を提供する、みたいな感じ?

メソッドやプロパティへのリンクが記事中にないので、サイドバーみたいなところから、MethodsStartCustomRecordへと辿っていく。すると、

UndoRecord.StartCustomRecord method (Word)

Initiates the creation of a custom undo record.

Syntax

expression.

expression A variable that represents an 'UndoRecord' object.

Parameters
Name Repuired/Optional Data type Description
Name Optional String Specifies the name of the custom undo record. This string is limited to 64 characters. If a longer string is supplied, the string is truncated to 64 characters.
注意

If this parameter is omitted or is an empty string, Word uses the name of the first command executed as the name of the undo record.

Remarks

StartCustomRecord begins the creation of a custom undo record, which records all actions done to the application while it is active under a record defined by Name .

Example
VB
Sub TestUndo() 
Dim objUndo As UndoRecord 
 
Set objUndo = Application.UndoRecord 
objUndo.StartCustomRecord ("My Custom Undo") 
    'Add some actions here 
objUndo.EndCustomRecord 
     
End Sub

UndoRecordオブジェクトを作成して、そのStartCustomRecordメソッドとEndCustomRecordメソッドを、アンドゥの対象とする処理の前後で実行すれば良いようだ。

使ってみる

前回のリスト1を少し改変する。

リスト1 標準モジュール
Public Sub test()
  Dim winAPI As New WindowsAPI
  Dim startTime As Long
  startTime = winAPI.getNowTickCount
  Dim Doc As Document
  Set Doc = ThisDocument
  Dim targetChar As Range
  Dim tmpUndo As UndoRecord    '……(1)'
  Set tmpUndo = Doc.Parent.UndoRecord
  Call tmpUndo.StartCustomRecord("TempUndo")
  For Each targetChar In Doc.Characters
    With targetChar
      If .HighlightColorIndex = wdYellow Then
        .HighlightColorIndex = wdNoHighlight
      End If
    End With
  Next
  Call tmpUndo.EndCustomRecord    '……(2)'
  Dim endTime As Single
  endTime = (winAPI.getNowTickCount - startTime) / 1000
  Call MsgBox("処理時間:" & endTime & vbCrLf & _
              "文字数 :" & Doc.Characters.Count)
End Sub

前回から変わったのは、(1)、(2)のところ。

(1)からの3行

Dim tmpUndo As UndoRecord
Set tmpUndo = Doc.Parent.UndoRecord
Call tmpUndo.StartCustomRecord("TempUndo")

では、変数tmpUndoUndoRecordオブジェクトをぶち込んで、StartCustomRecordメソッドを実行。

メインの処理の記述の後、(2)の

Call tmpUndo.EndCustomRecord

EndCustomRecordメソッドを実行する。

これで、コード実行後、アンドゥで元に戻せることになる。

やってみる

f:id:akashi_keirin:20181215140403j:plain

ひとまず、この状態でリスト1を実行。

f:id:akashi_keirin:20181215140412j:plain

こうなる。

んで、「元に戻す」アイコンにマウスカーソルを当てると、

f:id:akashi_keirin:20181215140431j:plain

「元に戻す TempUndo」と表示されている。

なるほど、StartCustomRecordメソッドの引数は、こんなところに生きるのか。

f:id:akashi_keirin:20181215140420j:plain

「元に戻す」アイコンをクリックすると、

f:id:akashi_keirin:20181215140510j:plain

おおっ! マーカが復活した!

おわりに

控え目に言っても、これは超便利ではなかろうか。

id:imihito さん、本当にありがとうございました!

Document.Charactersコレクションの要素を一つづつ処理するのは大変(Word)

WordでDocument.Charactersオブジェクトを一つづつ処理するのは異様に時間がかかる

資料の修正箇所に網掛けをしたものと、同じ内容で網掛けのないバージョンを要求されることがある。

そして、網掛けありバージョンと網掛けなしバージョンが併立するということが起こる。

ここまではまあ良い。

ヒサンなのは次のようなケースだ。

  • 資料に再修正の指示が出る。
  • 再修正し、網掛けありバージョンと網掛けなしバージョンを作成する
  • 再修正を繰り返すうちに、どこかで保存のタイミングを誤る
  • 網掛けありバージョンと網掛けなしバージョンの内容にずれが生ずる

およそ、事務系の仕事をしていれば、誰でも経験するのではなかろうか。

VBAで網掛けを外す

……と、ここまで「網掛け」という用語を用いているが、実際にはHighLight機能を使うことにする。「蛍光ペン」というやつだ。白黒印刷すれば、網掛けとほぼ同じ効果なので、ひとまずこれで代用する。

やり方としては、DocumentオブジェクトのCharactersコレクションの要素を一つづつ取り出して、HighLightIndexプロパティがwdYellowだったらwdNoHighLightに変える、というだけのものとする。

リスト1 標準モジュール
Public Sub test()
  Dim winAPI As New WindowsAPI    '……(1)'
  Dim startTime As Long
  startTime = winAPI.getNowTickCount
  Dim Doc As Document    '……(2)'
  Set Doc = ThisDocument
  Dim targetChar As Range
  For Each targetChar In Doc.Characters    '……(3)'
    With targetChar
      If .HighlightColorIndex = wdYellow Then    '……(4)'
        .HighlightColorIndex = wdNoHighlight
      End If
    End With
  Next
  Dim endTime As Single    '……(5)'
  endTime = (winAPI.getNowTickCount - startTime) / 1000
  Call MsgBox("処理時間:" & endTime & vbCrLf & _
              "文字数 :" & Doc.Characters.Count)
End Sub

(1)からの3行

Dim winAPI As New WindowsAPI
Dim startTime As Long
startTime = winAPI.getNowTickCount

はお気になさらねえでくだせえ。

WinAPIクラスというのは、割とよく使うWindowsAPIの関数を、手軽に使えるように自作のクラスモジュールにまとめたもの。

ここでは、あの有名なGetTickCount関数をラップしていると思ってください。

メインの処理は(2)からの10行

Dim Doc As Document
Set Doc = ThisDocument
Dim targetChar As Range
For Each targetChar In Doc.Characters    '……(3)'
  With targetChar
    If .HighlightColorIndex = wdYellow Then    '……(4)'
      .HighlightColorIndex = wdNoHighlight
    End If
  End With
Next

Document.Charactersコレクションの要素を一つづつ取り出して、HighlightColorIndexプロパティの値によって処理を切り替えている。

Charactersコレクションの要素がCharacterオブジェクトではなくRangeオブジェクトだというあたりが注意かな。

中身も詳しく見ておく。(3)からの7行

For Each targetChar In Doc.Characters
  With targetChar
    If .HighlightColorIndex = wdYellow Then    '……(4)'
      .HighlightColorIndex = wdNoHighlight
    End If
  End With
Next

では、For Each ~ Nextを用いて、Charactersコレクションの要素一つ一つを処理する。

ループ内部では、(4)からの3行

If targetChar.HighlightColorIndex = wdYellow Then
  targetChar.HighlightColorIndex = wdNoHighlight
End If

で、Charactersコレクションの要素であるRangeオブジェクト(変数targetCharの中身)のHighlightColorIndexプロパティの値を調べ、値がwdYellowだったら、すなわち黄色マーカが施されていたら、HighlightColorIndexプロパティの値をwdNoHighlightに変える、すなわちマーカを除去する、という処理を行う。

残りの、(5)からの4行(実質3行)

Dim endTime As Single
endTime = (winAPI.getNowTickCount - startTime) / 1000
Call MsgBox("処理時間:" & endTime & vbCrLf & _
            "文字数 :" & Doc.Characters.Count)

は、(1)の続き。処理後の時間を取得して、処理に掛かった時間を表示するだけ。

実行

f:id:akashi_keirin:20181215104022j:plain

このようなWord文書を用意する。文書内には、ご覧のようにところどころ黄色マーカを施してあるw

この状態で、リスト1を実行してみる。ちなみに、画面左下の文字数カウントでは「1082文字」と出ている。

f:id:akashi_keirin:20181215104035j:plain

なんと、3.785秒もかかっている。しかも、なぜか文字数(正確にはDocument.Characters.Countの値だけど)は1218になっている。

おわりに

1000字チョイなんて、コンピュータにとっては屁みたいな文字数だと思ったのだが、なぜこんなにヒマがかかるのだろうか。

しかも、今回のコードは単にマーカ部分をマーカなしにするだけであり、ひとたび実行すると、元の状態を復元することはできない(WordのVBAの場合、アンドゥでそこそこ戻れるけど。)。

マーカあり/なしを切り替えるには、マーカされている部分を記憶しておく必要がある。そのためには状態を取得するためにCharactersオブジェクトの要素にアクセスする必要がある。

たぶん、やり方がマズいのだろうけど、何か良いやり方はないものか。

飛び地状態のセル範囲にも名前を付けることができる(Excel)

飛び地状態のセル範囲にも名前を付けることができる

Excel使いこなしマンには常識だったのかも知れないが、便利だなと思ったので記しておく。

飛び地状態のセル範囲に名前を付けてみる

まず、

f:id:akashi_keirin:20181210230238j:plain

こんな風に、投げやりに文字列を入れておく。

別に入れなくてもいいんだけど。

んで、[Ctrl]キーを押しながら、ポチポチと選択。

f:id:akashi_keirin:20181210230250j:plain

次に、イミディエイト・ウインドウに、

Selection.Name = "Dekosuke"

と打ち込んで、[Enter]。

すると、

f:id:akashi_keirin:20181210230303j:plain

画面上は何の変化もないけれど、

f:id:akashi_keirin:20181210230315j:plain

一旦選択状態を解除して、イミディエイト・ウインドウに、

Sheet1.Range("Dekosuke").Select

と打ち込んで、[Enter]。

f:id:akashi_keirin:20181210230410j:plain

この通り、飛び地状態のセル範囲がキッチリ選択される。

おわりに

長く使い続けるマクロで使用することはオススメできないが、サクッと書いて使い捨てにするマクロになら使える場面もあるかも知れない。

不規則なセル範囲に一括して処理を施したい場合なんかに使えそうです。Application.Unionメソッドを使うより、よっぽど楽です。