Excel VBAでIEを操作するのによく使う機能まとめ

#はじめに
今は、RPAといったソフトウェアが出てきたり、PC操作の自動化のハードルが下がってきているのではないかと思います。と同時に、システムを１件１件叩くなどの作業はもはや人の仕事とは言えず、単なる苦行だということが常識になりつつあるのではないでしょうか。
RPA入れてくれたり、Pythonと行ったライブラリが充実している物を入れたりできる環境であれば良いですが、ある物しか使えないなどの制約の大きな場面もあるのではないかと思います。
今回は、そのような中で、VBAを使い、Google先生に聞きながら、インターネット検索のみでシステム作業の自動化をしてみたので、主に使った機能などをまとめてみます。

エクセル操作については、以下にまとめて行きます。
[Excel VBAでエクセル操作をするのによく使う機能まとめ](https://qiita.com/Tamarin_3/items/f33c8e2002becd2ec1b9)

#事前準備
VBAでIE操作等をする時に使った参照設定です。（試行錯誤していたので、結局使ってないものもあるかもしれません）
基本的には、最新の物で良いかと思います。
+ Visual Basic For Applications
+ Microsoft Excel 16.0 Object Library
+ OLE Automation
+ Microsoft Office 16.0 Object Library
+ Microsoft HTML Object Library
+ Microsoft Internet Controls
+ UIAutomation Client
+ UIAutomation ClientPriv
+ UIA
+ UIAutomationCorePriv
+ UIAutomation BlockingCore 1.0Type Library
+ Microsoft Forms 2.0 Object Library
+ Microsoft Outlook 16.0 Object Library
+ Microsoft ActiveX Data Objects 2.8 Library
+ Windows Script Host Object Model

#基本の形
##オブジェクトの宣言
基本は、InternetExplorlerオブジェクトとHTMLドキュメントを使います。
ほとんどのページで、同じ変数名使われてるかと思いますが、任意の名前でも大丈夫です。

```vb
Dim objIE As InternetExplorer
Dim htmldoc As HTMLDocument
```
いろんな操作を自動化する場合は、クラス化してしまうのもいいかもしれません。
IEクラスや、○○システムクラスなどを作って、使う変数をまとめてしまうと使い回しがしやすいと思います。

```vb:IEClass
Option Explicit

Public objIE As InternetExplorer
Public htmldoc As HTMLDocument
```
## IEの起動〜終了
IEを起動するところから、閉じるまでの基本形です。
IEオブジェクトを準備し、URLで起動し、htmlを取得します。
起動にはある程度時間がかかるので、起動の際や、画面遷移、ボタン押下の際などは、IEの起動待ちを入れるのが通常です。よく使うので、関数化しておくのが良いと思います。

クラスを使用するときは、宣言して、実体化させます。

```vb
Sub IEOperation()

    Dim ie As IEClass 'クラスの宣言
    Set ie = New IEClass 'クラスの実体化

    ie.objIE = New InternetExplorer
    ie.objIE.Visible = True 'True:IEを表示、False:IEを非表示
    ie.objIE.navigate "URL" 'URLに入れたIEを起動
    Call WaitIE(ie.objIE) 'IEの読み込み待ち関数

    Set ie.htmldoc = ie.objIE.document '開いたIEのドキュメントをセット

    'IE操作を入れる------------------------


    'IE操作終了---------------------------

    ie.objIE.Quit 'IEを閉じる

End Sub

Function WaitIE(objIE As InternetExplorer) 'IEの読み込み待ち関数

    Do While objIE.Busy = True Or objIE.readyState < 4
        DoEvents
    Loop

End Function
```
#DOM操作
基本的には、htmlを取得して、DOM(Document Object Model）操作をしていきます。最初はよくわからなくても、やっていくうちに慣れていくと思います。
[参考：DOMとは](https://www.vba-ie.net/element/dom.php)

##HTMLの参照方法
DOM操作を行うためには、実際のHTMLの情報を予め知っておく必要があります。
テキストボックス、ボタン等、知りたい物の箇所で、右クリック→要素の検証をクリックするか、F12を押下すると、HTMLが表示されます。

##HTMLが見れない場合
JavaScript等で右クリックが禁止されていたり、HTMLが見れないケースもある様です。
そんな場合にHTMLをメモ帳で吐き出すプログラムがあり、以下のサイトからダウンロード可能です。
書籍を購入しなくてもダウンロードできる様ですが、書籍も参考になると思います。
https://book.impress.co.jp/books/3384.php

##frame内の操作をする場合
frame内の操作を行いたい場合、一旦そのframeのhtmlを取得してからDom操作を行う形になります。
frameの後に続く数字はframe番号で、frameが複数ある場合は、番号を入れます。

```vb
dim framedoc as HTMLDocument

set framedoc = htmldoc.frame(0).document

```

##テキスト入力
テキスト入力に関しては、該当箇所のidもしくはnameの値を取得して、以下のように代入します。

```vb
With ie.html
    .getElementById("id名").Value = "代入する値"
    .getElementsByName("name名").Value = "代入する値"
End With
```
##ボタン押下
ボタンに関しては、ボタンの作り方によって、いくつか押し方があります。
それにしても何故同じシステムで、いろいろなボタンの作り方が混在してたりするのだろう。。優れたフロントエンドエンジニアの方というのは、こういうところに現れてくるものなのでしょうか。。
以下にどういった情報が拾えた時にどのような押し方ができるのかを示します。

###inputタグで、Valueが分かる場合
以下のようなhtmlの場合に使用します。

```html
<input type="button" value="***" onclick="***.***()">
```

繰り返し使ったりするので、関数の形で書いています。
使用するときは、htmlドキュメントと、Valueの値を引数にセットしてコールします。

```vb
Function ButtonInputValueClick(htmldoc As HTMLDocument,ButtonValue As String)

    Dim obj As Variant

    For Each obj In htmldoc.getElementsByTagName("input")
        if Replace(obj.Value," ","") = ButtonValue Then
            obj.Click
            Exit For
        End If
    Next

End Function
```
htmlからinputタグの物を全て順番に取ってきて、Valueが押したいボタンの物の時にクリックするという物です。
もしValueが同じ物があったら考慮が必要かもです。取得した際にスペース入っていることが多かったので、Replaceで削除しています。Webページによっても変わってくると思います。

###ButtonタグでIDやNameが分かる場合

IDやNameがふられている場合は、比較的シンプルになっています。

```vb
Function ButtonClickID(htmldoc As HTMLDocument, Id As String)

    Dim Button As HTMLInputElement
    Set Button = htmldoc.getElementById(Id)
    Button.Click

End Function

Function ButtonClickName(htmldoc As HTMLDocument, name As String)

    Dim Button As HTMLInputElement
    Set Button = htmldoc.getElementsByName(name)(0)
    Button.Click

End Function
```

##リンククリック
システムによっては、ボタンだけでなく、リンクの形式になっているものをクリックする必要があるのではないかと思います。
以下のような形ですね。





```html
<a href="URL">アンカーテキスト</a>
```

リンクをクリックする際は、上記のアンカーテキストの内容を取得しておき、以下のように処理します。
以前、inputタグを順番に取得した処理がありましたが、今度はリンクなので、aタグを取得します。

```vb
Function LinkClickInnerText(htmldoc As HTMLDocument, InnerText As String)

    Dim obj As Variant

    For Each obj In htmldoc.getElementsByTagName("a")
        If obj.innerText = InnerText then
            obj.Click
            Exit For
        End If
    Next
End Function
```

##リスト選択
ドロップダウンリストになっている箇所の選択は以下のように記述します。
呼び出す場合は、Idと、リストの何番目かを指定してあげます。

```vb
Function ListSelectId(htmldoc As HTMLDocument,Id As String, ListIndex As Long)

    Dim obj As HTMLSelectElement
    Set obj = htmldoc.getElementById(Id)
    obj.selectedIndex = ListIndex

End Function
```
##テーブルデータ取得
システムの参照画面などは、テーブルの形になっていることが多く、そのデータを取得する必要があるケースは結構あるのではないかと思います。
htmlからtableを全て取得し、欲しいtableの情報になったら、取得を行います。

```vb
Function getTableData(Byval htmldoc As HTMLDocument)

    Dim obj As Object
    Dim rown As Long
    Dim columnn As Long
    Dim getData As String

    For Each obj In htmldoc.getElementsByTagName("table") 'tableタグを全て取得し、順番にobjに格納
        If Left(Replace(obj.innerText, vbCrLf,""),○) = "○○" Then '取得したtableのうち、どのtableのデータが欲しいかを判別
            

            getData = obj.rows(行番号).Cells(列番号).innerText

            '必要に応じて、欲しいデータの行数や、列数を調べる
            '------------------------------------------------------------------
            'For rown = 0 To obj.rows.Length - 1 '行数分ループ

            '    For columnn = 0 To obj.rows(rown).Cells.Length - 1 '列数分ループ
            '        Debug.print obj.rows(rown).Cells(columnn).innerText
            '    Next

            'Next
            '------------------------------------------------------------------

        End If

    Next

End Function

```



#ページ遷移
ボタンをクリックしたり、メニューを選択したりすると、ページ遷移が起こるのが一般的ですよね。そういったページ遷移した場合の処理をする必要があります。
##同じIEのままページが遷移する場合
同じIEのままページが遷移する場合は、読み込み待ちと、遷移後のhtmlを取得し直す必要があります。
これもよく使うので、関数化しておくと良いのではないかと思います。

```vb
Function NextIE(Byval ie As IEClass)
    Call WaitIE(ie.objIE)
    Set ie.html = Nothing '以前のHtmlを破棄
    Set ie.html = ie.objIE.document '遷移後のhtmlを読み込み
End Function
```

##別のIEが起動する場合
ボタンクリック等によって、別のIEが立ち上がり、そのIEを操作しなくてはいけない場合、新たにIEオブジェクトを作る必要があります。
今までは最初にIEオブジェクトを作って、そのオブジェクトにURLをセットしてアクセスしてましたが、今度は、起動済みのIEを新しく作ったIEオブジェクトにセットします。
セットの仕方はいくつかあると思いますが、開いている全てのWindowをチェックし、そのタイトルが一致する物をIEオブジェクトにセットします。

```vb
Function IEWindowFind(Byval Windowname As String)As InternetExplorer

    Dim ie As InternetExplorer
    Dim sh As Object
    Dim win As Object
    Dim DocumentTitle As String

    Set sh = CreateObject("Shell.Application")

    For Each win in sh.Windows
        DocumentTitle = ""
        On Error Resume Next 'エラーは無視する
        If DocumentTitle = windowname Then
            set ie = win
            Exit For
        End If
    Next

    Set IEWindowFind = ie

End Function
```

呼び出す際は、そのウィンドウが見つかるまでループする処理を行います。
別のIEが開かない場合でも、ボタンをクリックした後にシステムの内部で複数の処理が動いて、WaitIEでOKになった後も、次の処理がBUSYになる場合もあります。そんなケースでエラーが起きる場合も、ループ処理を入れるようにします。
※無限ループになる可能性があるので、必要に応じて、タイムアウトにして、エラーにする処理をいれる必要があります。

```vb
Option Explicit
'スリープ用のAPI宣言
Private Declare Sub Sleep _
    Lib "kernel32" (Byval dwMiliseconds As Long)

Sub getWindow()

    Dim ie2 As IEClass
    Set IE2 = NEW IEClass

    Set ie2.objIE = Nothing
    Do While ie2.objIE Is Nothing
        Set ie2.objIE = IEWindowFind("ウィンドウネーム")
        Sleep(1000) '1秒待機
    Loop

    Call NextIE(ie2)

End Sub
```

IEオブジェクトにセットした後は、各種のDOM操作を行います。

##WEBページダイアログが起動する場合
システムによっては、IEではなく、WEBページダイアログが起動することがあります。これはIEとは異なるので、対象の取得の仕方が異なりますが、htmlで書かれているので、一旦取得さえしてしまえば、同じようにDOM操作が可能です。

###WEBページダイアログが起動する直前の動作
何かのボタンを押下した際に、WEBページダイアログが起動するといった場合、制御がエクセルから離れてしまうため、タイムアウト処理を行うように、ボタンをクリックする必要があります。

```vb
Public Const timeout As Long = 100

Sub timeout()
    '途中省略
    ie.htmldoc.Script.setTimeout "document.getElementById('ボタンのID').click()", timeout

End Sub
```

###WebページダイアログのWindowハンドルの取得と、htmlの取得

続いて、WEBページダイアログのhtmlの取得を行います。流れとしては、まず、WEBページダイアログのWindowハンドル番号の取得をし、その番号のhtmlを取得します。
Windowハンドル番号は、起動しているWindowの数など、その時に応じて可変の数字となります。

```vb
Option Explicit
'Windowハンドル取得のAPI宣言
Declare Function FindWindow Lib "USER32" Alias "FindWindowA" _
(Byval ipClassName As String, Byval lpWindowName As String)As Long

Sub WebPageDialog()

    Dim hWnd As Long 'Windowハンドル番号格納用
    Dim htmldoc As IHTMLDocument

    Do
        DoEvents
        Sleep(1000)
        hWnd = FindWindow("Internet_Explorer_TridentDlgFrame", vbNullString)
    Loop Until hWnd

Reprocessing:

    Do
        Set htmldoc = IEDOMFromhWnd(hWnd) 'ダイアログウィンドウのhtmlドキュメントを取得
        On Error GoTo errlabel
        htmldoc.getElementById("Webページダイアログの部品ID").Checked = True '何でも良いのでDOM操作
        Sleep(1000)
    Loop Until Not(htmldoc is Nothing)

    Exit Sub

errlabel:
    Resume Reprocessing
End Sub

Function IEDOMFromhWnd()
    '下記リンク先のダイアログのhtmlドキュメント取得を参照　
End Function
```
FindWindowはクラス名や、Window名を指定して、そのWindowのハンドル番号を探す関数です。
今回は、クラス名として、Internet_Explorer_TridentDlgFrameを指定していますが、これがWebページダイアログのクラス名です。

クラス名が不明の場合、クラス名を調べる方法もあるみたいです。
[クラス名がわからない時の調べ方](http://sak.cool.coocan.jp/w_sak3/doc/sysbrd/vb_t02.htm)

途中でエラー処理をしていますが、WEBページダイアログはIEの読み込み待ちのような機能はないのか、知らなかったため、html取得を試みた後、何かしらDOM操作を行い、エラーになった場合（htmlが取得できなかった場合）は再度処理を行うようにしています。

関数のIEDOMFromhWndに関しては、以下のリンク先を参照ください。
[ダイアログのhtmlドキュメント取得](http://www.mvps.org/emorcillo/en/code/vb6/iedom.shtml)

```vb
'
' Requires: reference to "Microsoft HTML Object Library"
'

Private Type UUID
   Data1 As Long
   Data2 As Integer
   Data3 As Integer
   Data4(0 To 7) As Byte
End Type

Private Declare Function GetClassName Lib "user32" _
   Alias "GetClassNameA" ( _
   ByVal hWnd As Long, _
   ByVal lpClassName As String, _
   ByVal nMaxCount As Long) As Long

Private Declare Function EnumChildWindows Lib "user32" ( _
   ByVal hWndParent As Long, _
   ByVal lpEnumFunc As Long, _
   lParam As Long) As Long

Private Declare Function RegisterWindowMessage Lib "user32" _
   Alias "RegisterWindowMessageA" ( _
   ByVal lpString As String) As Long

Private Declare Function SendMessageTimeout Lib "user32" _
   Alias "SendMessageTimeoutA" ( _
   ByVal hWnd As Long, _
   ByVal msg As Long, _
   ByVal wParam As Long, _
   lParam As Any, _
   ByVal fuFlags As Long, _
   ByVal uTimeout As Long, _
   lpdwResult As Long) As Long
      
Private Const SMTO_ABORTIFHUNG = &H2

Private Declare Function ObjectFromLresult Lib "oleacc" ( _
   ByVal lResult As Long, _
   riid As UUID, _
   ByVal wParam As Long, _
   ppvObject As Any) As Long

Private Declare Function FindWindow Lib "user32" _
   Alias "FindWindowA" ( _
   ByVal lpClassName As String, _
   ByVal lpWindowName As String) As Long

'
' IEDOMFromhWnd
'
' Returns the IHTMLDocument interface from a WebBrowser window
'
' hWnd - Window handle of the control
'
Function IEDOMFromhWnd(ByVal hWnd As Long) As IHTMLDocument
Dim IID_IHTMLDocument As UUID
Dim hWndChild As Long
Dim lRes As Long
Dim lMsg As Long
Dim hr As Long

   If hWnd <> 0 Then
      
      If Not IsIEServerWindow(hWnd) Then
      
         ' Find a child IE server window
         EnumChildWindows hWnd, AddressOf EnumChildProc, hWnd
         
      End If
      
      If hWnd <> 0 Then
            
         ' Register the message
         lMsg = RegisterWindowMessage("WM_HTML_GETOBJECT")
            
         ' Get the object pointer
         Call SendMessageTimeout(hWnd, lMsg, 0, 0, _
                 SMTO_ABORTIFHUNG, 1000, lRes)

         If lRes Then
               
            ' Initialize the interface ID
            With IID_IHTMLDocument
               .Data1 = &H626FC520
               .Data2 = &HA41E
               .Data3 = &H11CF
               .Data4(0) = &HA7
               .Data4(1) = &H31
               .Data4(2) = &H0
               .Data4(3) = &HA0
               .Data4(4) = &HC9
               .Data4(5) = &H8
               .Data4(6) = &H26
               .Data4(7) = &H37
            End With
               
            ' Get the object from lRes
            hr = ObjectFromLresult(lRes, IID_IHTMLDocument,_
                     0, IEDOMFromhWnd)
               
         End If

      End If
      
   End If

End Function

Private Function IsIEServerWindow(ByVal hWnd As Long) As Boolean
Dim lRes As Long
Dim sClassName As String

   ' Initialize the buffer
   sClassName = String$(100, 0)
   
   ' Get the window class name
   lRes = GetClassName(hWnd, sClassName, Len(sClassName))
   sClassName = Left$(sClassName, lRes)
   
   IsIEServerWindow = StrComp(sClassName, _
                      "Internet Explorer_Server", _
                      vbTextCompare) = 0
   
End Function

'
' Copy this function to a .bas module
'
Function EnumChildProc(ByVal hWnd As Long, lParam As Long) As Long
   
   If IsIEServerWindow(hWnd) Then
      lParam = hWnd
   Else
      EnumChildProc = 1
   End If
   
End Function
```

###WEBページダイアログのHTMLの見方
WEBページダイアログについては、右クリック→要素の検証や、F12でhtmlを参照することができないので、以下の方法でhtmlを参照します。

1. HTMLを見たいWEBページダイアログを表示
2. そのWEBページダイアログで、Ctrl+Pを押し、印刷のウィザードを表示
3. その状態で、以下のフォルダを確認

C:¥ユーザー¥自身のユーザー¥AppData¥Local¥Temp¥Low

htmlを見て、情報を取得したら、IEと同じようにDOM操作を行います。

#ファイルダウンロードダイアログの操作

ファイルダウンロードダイアログの操作については、以下の記事が参考になります。WinAutomationという技術を使用しています。

https://qiita.com/callmekohei/items/487aefe1db0fd86cc7cf

https://www.ka-net.org/blog/?p=4855


##長くなりそうなので、随時追記していきます