MicrosoftEdgeのIEモードをVBAで操作する

スポンサーリンク

InternetExplorerが2022年6月13日以降使用できなくなる?

先日ネットの記事でInternetExplorerが使えなくなるとの話がありました。
どうやらサポート終了日以降はIEを起動しても強制的にMicrosoftEdgeにリダイレクトされるとのことでした。

以前からIEはセキュリティ上の問題が指摘されていていずれ使えなくなるという話はありましたが、サポート終了日以降も起動くらいは問題ないと思っていたので、この対応にかなり焦りました。

手作業の業務についてはMicrosoftEdgeのIEモードを使用すればほぼほぼ問題ないのですが、VBAを使用したDOM(DocumentObjectModel)を取得する方法が一切使えなくなるのがネックでした。

今回私が慌てて代替の方法を色々と検討した結果、先人が素晴らしいコードを作成してくれているのを見つけました。
また、それを活用することで今までのIE操作で使用していたコードをほぼそのまま使用できることがわかりましたのでお困りの方にお伝えしたくコードと使い方をご紹介します。

VBAでIEを操作する方法の代替手段

一般的なブラウザ操作の自動化手段について

VBAでIEを操作して情報を取得したり、ブラウザ上の操作を自動化する方法についての代替手段はこれまで以下のものが挙げられていました。
ただ後述するようにこれらの方法では色々な制約があり、これらの方法は試してみたものの私の行っている業務の代替手段としては使用できませんでした。

①SeleniumBasicを使う

Pythonや他のプログラムなどでも使用されるSeleniumのVBA版を使用してブラウザの操作を自動化する方法です。

IEからDOMを取得する方法と少し挙動は異なりますが、どちらかというとIEからDOMを取得する方法よりSeleniumを使う方法の方が主流のため、ネット上で情報も多く手に入ります。

ウェブサイトのHTMLから特定の要素を簡単に特定できるCSSセレクタを使えて、Seleniumに慣れているエンジニアの方が多いからか、色々なサイトでSeleniumをおすすめされています。

しかし私のようにエンジニアではなく普段会社で事務仕事をしている場合Seleniumを使用する場合以下のような壁に当たります。

1.VBAでDOMを取得してIEを操作する場合と異なり、ブラウザの起動から終了までをすべてプログラムで操作する必要がある。

開いていたページをそのまま操作することができないので、二段階認証が必要なログインの処理などが間に入ると操作ができないケースがあります。

2.Seleniumを使っていることがサーバー側に伝わるためにページの情報が取得できないことがある

例えばGoogleのサインインした後の個人のページなどはSeleniumでアクセスしようとすると拒否され、操作ができないことがあります。

3.私も詳細がわからないのですが、SeleniumとWebドライバでブラウザを開こうとすると拒否されるケースがありました。

おそらく会社の社内ネットワークセキュリティのどこかに引っかかっていると思うのですが、原因は特定できておりません。
とりあえず環境次第でSeleniumは使用できない場合があるようです。

4.そもそもSeleniumBasicをインストールすることが禁止されている。

会社からの貸与PCは自由にアプリケーションをインストールすることができない場合も多く、私もここが大きな問題となります。

また、そのほかVBAで使用するためのSeleniumBasicは少し前から更新されておらず、ドライバを手動で更新してから使用しないといけないなど今後の安定的な使用について若干の不安がありました。

②PowerAutomateDeskTopなどのRPAを使用する

Windows標準のRPAであるPowerAutomateDeskTopやその他のRPAツールで操作を代替する紹介をされている場合も多いです。

ただ、専門的な操作をしようとすると有料のサービスを使用する必要があることや、IEからDOMを取得するVBAを書く場合と異なる操作が多く、習熟するのに少し時間がかかる印象でした。

スポンサーリンク

満を持してのEdgeを操作するVBAコードの登場

Seleniumは使えず、(作り慣れていないので)RPAを作るには時間がかかるうえに大掛かりな変更が必要になることから別の手段を検討していたところ、以下の2つの記事からEdgeでDOMを取得して操作する方法が紹介されていました。

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

https://social.msdn.microsoft.com/Forums/ja-JP/c0765a67-b8ba-40dc-ac52-aac7be9f1d6a/ie123981246912509125401248812364202224180626376152608512395320662?forum=vbajp

上の初心者備忘録は専門的な知識を使ったコードをたくさん紹介してくださっているサイトで、私も普段よく参考にさせていただいています。

この記事は以前のChromiumベースになる前のEdge操作について書かれていますが、新しいEdgeについてもIEモードではClass「InternetExplorer_Server」が存在していて、基本的に同じ処理ができるそうです。

MicrosoftEdgeのIEモードの操作サンプルコード

上記の2つの記事の情報を合わせて、かつ長くなるDOM取得部分を一個のコードで使いまわせるように以下の通りコードを書いてみました。

以下の「検索実行」のプロシージャを実行するとEdgeを開き、IEモードに切り替えた後、Googleで「VBA・GAS・Pythonで業務を楽しく効率化」を検索するという操作を自動化しています。

宣言部分

Option Explicit

#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal ms As LongPtr)
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal ms As Long)
#End If

Private Declare Function GetTopWindow Lib "user32" (ByVal hWnd As Long) As Long
Private Declare Function GetParent Lib "user32" (ByVal hWnd As Long) As Long
Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private Declare Function EnumChildWindows Lib "user32" (ByVal hWndParent As Long, ByVal lpEnumFunc As Long, lParam As Long) As Long
Private Const GW_HWNDNEXT = &H2
Private Declare Function GetNextWindow Lib "user32" Alias "GetWindow" (ByVal hWnd As Long, ByVal wFlag As Long) As Long
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 IIDFromString Lib "ole32" (lpsz As Any, lpiid As Any) As Long
Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" (ByVal lpString As String) As Long
Private Const SMTO_ABORTIFHUNG = &H2

Private Declare Function SendMessageTimeout Lib "user32" Alias "SendMessageTimeoutA" (ByVal hWnd As Long, ByVal msg As Long, _
                            ByVal wParam As Long, ByVal lParam As Long, ByVal fuFlags As Long, ByVal uTimeout As Long, lpdwResult As Long) As Long
Private Declare Function ObjectFromLresult Lib "oleacc" (ByVal lResult As Long, riid As Any, ByVal wParam As Long, ppvObject As Object) As Long
        
'オブジェクトの位置取得?
Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(0 To 7) As Byte
End Type

Private Declare PtrSafe Function IUnknown_QueryService Lib "shlwapi.dll" _
    (ByVal punk As IUnknown, guidService As GUID, riid As GUID, ppvOut As IAccessible) _
        As Long

Private hIES As Long
Private IeEdge As Object
Private hWnd As Long

'EdgeをIEモードに切り替える場合に使用する
'対象ウインドウを最前面にする
Private Declare Sub SetForegroundWindow Lib "user32" (ByVal hWnd As Long)

'キーボード操作
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, _
                                                ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
Private Const KEYEVENTF_EXTENDEDKEY = &H1
Private Const KEYEVENTF_KEYUP = &H2
Private Const fKEYDOWN = KEYEVENTF_EXTENDEDKEY
Private Const fKEYUP = KEYEVENTF_EXTENDEDKEY Or KEYEVENTF_KEYUP

Private Const VK_UP = &H26
Private Const VK_Alt = &H12
Private Const VK_F = &H46
Private Const VK_RETURN = &HD

Private Sub DOM取得()

    'Chromium版EdgeのIEモードをDOM操作(32ビット版Excel)
    
    Dim con As Object, items As Object
    Dim pId As Long
    Const ProcessName = "msedge.exe"

    Dim msg As Long
    Dim res As Long
    Dim IID_IHTMLDocument2 As GUID

    Dim Dom

    Set con = CreateObject("WbemScripting.SWbemLocator").ConnectServer
    hWnd = GetTopWindow(0)
        Do
            If GetParent(hWnd) = 0 Then
                'ウィンドウハンドルからプロセスIDを取得し、プロセス名を使用してEdgeのウィンドウかどうかを判別する
                GetWindowThreadProcessId hWnd, pId
                Set items = con.ExecQuery("Select * From Win32_Process Where (ProcessId = '" & pId & "') And (Name = '" & ProcessName & "')")
                If items.Count > 0 Then
                    'Edgeの子ウィンドウ列挙
                    EnumChildWindows hWnd, AddressOf EnumChildProcIES, 0 'Internet Explorer ServerのhWndをhIESに格納
                    If hIES <> 0 Then Exit Do
                End If
            End If
            hWnd = GetNextWindow(hWnd, GW_HWNDNEXT)
        Loop While hWnd <> 0

    If hIES = 0 Then Exit Sub

    'IHTMLDocument2取得
    msg = RegisterWindowMessage("WM_HTML_GETOBJECT")
    SendMessageTimeout hIES, msg, 0, 0, SMTO_ABORTIFHUNG, 1000, res
    If res Then
        With IID_IHTMLDocument2
            .Data1 = &H332C4425
            .Data2 = &H26CB
            .Data3 = &H11D0
            .Data4(0) = &HB4
            .Data4(1) = &H83
            .Data4(2) = &H0
            .Data4(3) = &HC0
            .Data4(4) = &H4F
            .Data4(5) = &HD9
            .Data4(6) = &H1
            .Data4(7) = &H19
        End With
        
        'IeEdgeを使いたいために適当にDomという変数を宣言して左辺に
        '正しくは「If ObjectFromLresult(res, IID_IHTMLDocument2, 0, IeEdge) = 0 Then」などの使用方法が望ましい?
        
        Dom = ObjectFromLresult(res, IID_IHTMLDocument2, 0, IeEdge)
    
    End If

End Sub

Private Function EnumChildProcIES(ByVal hWnd As Long, ByVal lParam As Long) As Long
  
    Dim buf As String * 255
    Dim ClassName As String

    GetClassName hWnd, buf, Len(buf)
    ClassName = Left(buf, InStr(buf, vbNullChar) - 1)
    If ClassName = "Internet Explorer_Server" Then
        hIES = hWnd
        EnumChildProcIES = False
        Exit Function
    End If
    EnumChildProcIES = True

End Function

実行部分

Sub 検索実行()

    Edge起動
    
    'GoogleのページをIEモードで開いた後に実行
    DOM取得 'IEモードのEdgeのDOMをIeEdgeに格納
    
    '最前面に持ってくるとIEモードのポップアップが消える
    Call SetForegroundWindow(hWnd)
    Sleep 3000
    
    IeEdge.getElementsByName("q")(0).Value = "VBA・GAS・Pythonで業務を楽しく効率化" '検索バーに文字を入力
    Sleep 1000
    IeEdge.getElementsByName("btnG")(0).Click '検索クリック

    While LCase(IeEdge.readyState) <> "complete"
        Sleep 1000
    Wend
    
End Sub

Private Sub Edge起動()

    'EdgeでGoogleのページを開く
    CreateObject("Shell.Application").ShellExecute "microsoft-edge:https://www.google.co.jp/"
    Sleep 3000

    'Alt+F
    keybd_event VK_Alt, 0, fKEYDOWN, 0   'Altを押す
    keybd_event VK_F, 0, fKEYDOWN, 0   'fを押す
    keybd_event VK_F, 0, fKEYUP, 0   'fを離す
    keybd_event VK_Alt, 0, fKEYUP, 0   'Altを離す

    Sleep 1000

    '上5回
    Dim i
    For i = 1 To 5
        keybd_event VK_UP, 0, fKEYDOWN, 0   'UPを押す
        keybd_event VK_UP, 0, fKEYUP, 0   'UPを離す
        Sleep 100
    Next i
        
    'Enter
    keybd_event VK_RETURN, 0, fKEYDOWN, 0   'Enterを押す
    keybd_event VK_RETURN, 0, fKEYUP, 0   'Enterを離す

    Sleep 1000

    'Enter
    keybd_event VK_RETURN, 0, fKEYDOWN, 0   'Enterを押す
    keybd_event VK_RETURN, 0, fKEYUP, 0   'Enterを離す

    Sleep 3000
    
End Sub

あくまでIEモードの操作に限られますのでいずれ使えなくなるとは思いますが、これまでVBAでIEを操作していたコードをほぼそのまま転用できるのは大きなメリットです。

IEモードの切り替え部分が若干不安定なこともありますが、最初から特定のページをIEモードで開くように設定しておけば切り替えるコードを使う必要がなく、より安定した挙動になると思います。

このコードはWindowsAPIをたくさん使用していて私も意味が理解できていない部分が多くありますので、勉強してコードの意味が理解できるようになればまた紹介記事を記載します。

コメント

  1. nyarou より:

    はじめまして。EdgeのIEモードをVBA操作したいと考えており、こちらの記事を参考にさせて頂いております。わかればご教授願いたいのですが、64bit版のエクセルで動かすコードはわかりますでしょうか?こちらの記事のコードは32bit版用だと思われるのですが、64bit版用として現在試しているのが、次の2点です。

    ①”Long”を”LongPtr”に書き換えた。
    ②”Private Declare Function”の箇所で、”Declare”と”Function”の間に”PtrSafe”を付け加えた。

    Edgeを開き、IEモードで開くところまでは動いたのですが、どうしてもgoogle検索の検索ボックスにキーワード挿入する箇所(IeEdge.getElementsByName(“q”)(0).Value = “VBA・GAS・Pythonで業務を楽しく効率化”)で「オブジェクト型の変数は設定されていません (エラー 91)」というエラーとなってしまいます。考えられる原因がもしわかるようでしたらご教授して頂きたいです。

    • okumasahito より:

      コメントありがとうございます。

      申し訳ありません。
      正直64bit版のExcelは使ったことがなく、正直この部分が原因だと
      どのように修正すればよいかわからないです。

      ただ、原因は別のところなのかな?と思いまして…。
      ブラウザはIEモードにきちんと切り替わっておりますでしょうか?
      (アドレスバーの左にIEのアイコンが表示されているかどうかで確認ができると思います。)

      今私のPCで動作確認してみたら記事作成時からブラウザのバージョンアップで
      IEモードへの変更がうまくいかないようです。

      そのため、検索文字を入力する段階でDOMを取得できておらず同じエラー91が出ました。
      解決方法としては以下の手順をお試しいただけますでしょうか。
      ①プロシージャの「Edge起動」を以下2行にしてIEモードへ変更するステップの削除
      CreateObject(“Shell.Application”).ShellExecute “microsoft-edge:https://www.google.co.jp/
      Sleep 3000
      この2行のあとをすべてコメントアウトもしくは削除
      ②Edgeの「設定」→「既定のブラウザ」→「InternetExplorerの互換性」の欄で
      InternetExplorerモード(IEモード)でのサイトの再読み込みを許可:許可
      InternetExplorerモードページにhttps://www.google.co.jp/を追加

      この状況で再度実行していただき、エラーがどのように生じるか
      ご確認いただけると嬉しいです。

      • nyarou より:

        お返事ありがとうございます。

        IEモードの切り替え自体は、記事のコード(キーボードイベント)により問題無く動いており、視覚でも動作確認できております。念のため、ご指摘の通りに修正して実行してみましたが、同じ箇所でエラーになってしまいました。

        • okumasahito より:

          ご確認ありがとうございます。
          それではおそらく64bit版Excelの手順でWindowsApi対応しないといけなそうですね。

          関数の変更については以下のサイトが参考になりそうでした。
          https://vbabeginner.net/howto-use-win32-api/
          以下のテキストファイルに変換後の記載方法も記載されているので、user32のAPIは修正できると思われます。
          https://vbabeginner.net/data/Win32API_PtrSafe.TXT

          問題は以下3つのAPIだと思いますが、こちらに関しては情報がなく私もわかりませんでした。
          Private Declare Function IIDFromString Lib “ole32” (lpsz As Any, lpiid As Any) As Long
          Private Declare Function ObjectFromLresult Lib “oleacc” (ByVal lResult As Long, riid As Any, ByVal wParam As Long, ppvObject As Object) As Long
          Private Declare PtrSafe Function IUnknown_QueryService Lib “shlwapi.dll” _
          (ByVal punk As IUnknown, guidService As GUID, riid As GUID, ppvOut As IAccessible) _
          As Long
          私も他のサイトの記載を引用して作ったコードなので、詳細が把握てきておらずお役に立てなくて申し訳ありません。
          私も探ってみますがかなり難易度が高い気がしており、解決できる自信がないです。

        • okumasahito より:

          追記情報:以下サイトに64bit Excel対応の記事がありました。
          https://qiita.com/ymd65536-ms/items/320f889e9fc35fe375f1

          こちらのコードで動くかどうか確認をお願いできますでしょうか。
          私の環境ではエラー91が出て動かないのですが、もしかしたら?と期待しております。

          Option Explicit

          Public Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal ms As LongPtr)
          Private Declare PtrSafe Function GetTopWindow Lib “user32” (ByVal hwnd As LongPtr) As LongPtr
          Private Declare PtrSafe Function GetParent Lib “user32” (ByVal hwnd As LongPtr) As LongPtr
          Private Declare PtrSafe Function GetWindowThreadProcessId Lib “user32” (ByVal hwnd As LongPtr, lpdwProcessId As LongPtr) As LongPtr
          Private Declare PtrSafe Function RegisterWindowMessage Lib “user32” Alias “RegisterWindowMessageA” (ByVal lpString As String) As LongPtr
          Private Declare PtrSafe Function SendMessageTimeout Lib “user32” Alias “SendMessageTimeoutA” (ByVal hwnd As LongPtr, ByVal msg As LongPtr, ByVal wParam As LongPtr, ByVal lParam As LongPtr, ByVal fuFlags As LongPtr, ByVal uTimeout As LongPtr, lpdwResult As LongPtr) As LongPtr
          Private Declare PtrSafe Function IIDFromString Lib “ole32” (lpsz As Any, lpiid As Any) As Long
          Private Declare PtrSafe Function ObjectFromLresult Lib “oleacc” (ByVal lResult As LongPtr, riid As Any, ByVal wParam As LongPtr, ppvObject As Object) As LongPtr
          Private Declare PtrSafe Function EnumChildWindows Lib “user32” (ByVal hWndParent As LongPtr, ByVal lpEnumFunc As LongPtr, ByVal lParam As LongPtr) As LongPtr
          Private Declare PtrSafe Function GetClassName Lib “user32” Alias “GetClassNameA” (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As LongPtr) As LongPtr
          Private Declare PtrSafe Function GetNextWindow Lib “user32” Alias “GetWindow” (ByVal hwnd As LongPtr, ByVal wFlag As LongPtr) As LongPtr

          Private Const GW_HWNDNEXT = &H2
          Private hIES As LongPtr

          Public Function GetWindow(Title As String) As Object

          Dim con As Object
          Dim items As Object
          Dim HtmlDoc As Object
          Dim hwnd As LongPtr: hwnd = 0
          Dim pid As LongPtr: pid = 0
          Dim buf As String * 255
          Dim ClassName As String

          Const ProcessName = “msedge.exe”

          Set con = CreateObject(“WbemScripting.SWbemLocator”).ConnectServer
          hwnd = GetTopWindow(0)

          Do
          GetClassName hwnd, buf, Len(buf)
          ClassName = Left(buf, InStr(buf, vbNullChar) – 1)

          If InStr(ClassName, “Chrome_WidgetWin_”) > 0 Then

          ‘ウィンドウハンドルからプロセスIDを取得し、Edgeのウィンドウかどうかを判別する
          GetWindowThreadProcessId hwnd, pid
          Set items = con.ExecQuery(“Select ProcessId From Win32_Process Where (ProcessId = ‘” & pid & “‘) And (Name = ‘” & ProcessName & “‘)”)
          If items.Count > 0 Then
          ‘Edgeの子ウィンドウ列挙

          EnumChildWindows hwnd, AddressOf EnumChildProcIES, 0
          If hIES <> 0 Then
          Set HtmlDoc = GetHTMLDocumentFromIES(hIES)
          If HtmlDoc Is Nothing Then
          Else
          If InStr(HtmlDoc.Title, Title) > 0 Then
          Set GetWindow = HtmlDoc
          Exit Do
          End If
          End If
          End If
          End If
          End If
          hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)
          Loop While hwnd <> 0

          End Function

          Private Function EnumChildProcIES(ByVal hwnd As LongPtr, ByVal lParam As LongPtr) As LongPtr
          Dim buf As String * 255
          Dim ClassName As String

          GetClassName hwnd, buf, Len(buf)
          ClassName = Left(buf, InStr(buf, vbNullChar) – 1)
          If ClassName = “Internet Explorer_Server” Then
          hIES = hwnd
          EnumChildProcIES = False
          Exit Function
          End If
          EnumChildProcIES = True
          End Function

          Private Function GetHTMLDocumentFromIES(ByVal hwnd As LongPtr) As Object
          Dim msg As LongPtr, res As LongPtr
          Dim iid(0 To 3) As LongPtr
          Dim ret As Object, obj As Object
          Const SMTO_ABORTIFHUNG = &H2
          Const IID_IHTMLDocument2 = “{332c4425-26cb-11d0-b483-00c04fd90119}”

          Set ret = Nothing ‘初期化
          msg = RegisterWindowMessage(“WM_HTML_GETOBJECT”)
          SendMessageTimeout hwnd, msg, 0, 0, SMTO_ABORTIFHUNG, 1000, res
          If res Then
          IIDFromString StrPtr(IID_IHTMLDocument2), iid(0)
          If ObjectFromLresult(res, iid(0), 0, obj) = 0 Then Set ret = obj
          End If
          Set GetHTMLDocumentFromIES = ret
          End Function

          Sub edge操作()

          Dim IeObj As Object
          IeObj.getElementsByName(“q”)(0).Value = “VBA・GAS・Pythonで業務を楽しく効率化” ‘検索バーに文字を入力

          End Sub

          • nyarou より:

            お返事と参考URLの紹介ありがとうございます。

            edge操作のプロシージャの部分は、edgeを立ち上げるコードは不要なのでしょうか?このコードのままだと、動かなそうでした。難しそうでしたら、あとは調べてみますので、大丈夫です。

          • okumasahito より:

            ご連絡ありがとうございます。
            お力になれず申し訳ありません。
            WindowsAPIについて私も勉強してみます。