后期绑定 IHTML 元素

Late bind an IHTML element

我正在尝试创建一个后期绑定 VBA 项目来搜索网络。有一次我有以下代码(早期绑定):

Dim currPage as HTMLDocument: Set currPage = objIE.document 'where objIE is set with Set objIE = CreateObject("InternetExplorer.application")
'(late bound as it is dim'd as Object)
    Dim myDiv As HTMLDivElement: Set myDiv = currPage.getElementById("fbar")
    Dim elemRect As IHTMLRect: Set elemRect = myDiv.getBoundingClientRect
    'Scroll until bottom of page is in view
    Do Until elemRect.bottom > 0
        currPage.parentWindow.scrollBy 0, 10000
        Set elemRect = myDiv.getBoundingClientRect
    Loop

此代码在后期绑定时变为:(或者我认为)

Dim currPage as Object: Set currPage = objIE.document
    Dim myDiv As Object: Set myDiv = currPage.getElementById("fbar")
    Dim elemRect As Object: Set elemRect = myDiv.getBoundingClientRect
    'Scroll until bottom of page is in view
    Do Until elemRect.bottom > 0
        currPage.parentWindow.scrollBy 0, 10000
        Set elemRect = myDiv.getBoundingClientRect
    Loop

我猜问题出在 IHTMLRect 前面的 I,其中 MSDN tells me 表示网页上没有实际对象的元素与之关联 - 因此将其分配给未指定的 Object 在代码中没有任何意义。 (这是一个完整的猜测)

无论如何,早期绑定代码工作正常,后期绑定代码在 elemRect.bottom

退出执行

为什么会这样,我该如何解决?

VBA 中的对象可以实现多个接口,您可以调用的 methods/properties 取决于您用来访问对象的接口。一个简单的例子:

' This means access the object via the IUnknown interface
' IUnknown is the interface from which all other COM
' interfaces inherit
Dim x As IUnknown
Set x = ThisWorkbook.Worksheets(1)

' Commented out as this won't compile because the
' Name property isn't defined in IUnknown
' MsgBox x.Name

' This means access the object through the default
' interface associated with the Worksheet object type
Dim w As Worksheet
Set w = x

' Now we can get to the name (same object, different interface)
MsgBox w.Name

对于 MSHTML,我猜像 getElementById 这样的方法会返回一个类似于 IHTMLElement 版本之一的接口。这意味着像IHTMLDivElement这样的接口中定义的methods/properties无法访问。

IUnknown 有一个名为 QueryInterface 的方法,用于获取对象实现的不同接口。然而,这不能在 VBA 中直接调用,因为 VBA 方法是使用 Dim 和适当的接口,然后使用 Set。只有在设置了必要的引用时才会编译,这反过来又违背了后期绑定的目的。

有一个使用 CallByName 的解决方法。回到工作表示例,这是可行的:

Dim x As IUnknown
Set x = ThisWorkbook.Worksheets(1)

' Commented out as this won't compile because the
' Name property isn't defined in IUnknown
' MsgBox x.Name

' Can get to the property via CallByName
MsgBox CallByName(x, "Name", VbGet)

对于 MSHTML 问题,这个有效(注意调用类型更改为 VbMethod):

Dim elemRect As Object: Set elemRect = CallByName(myDiv, "getBoundingClientRect", 
    VbMethod)
stTimer = Timer
'Scroll until bottom of page is in view
Do Until elemRect.bottom > 0 Or tElapsed > timeout 'timeout after n seconds
    currPage.parentWindow.scrollBy 0, 10000
    Set elemRect = CallByName(myDiv, "getBoundingClientRect", VbMethod)
    tElapsed = Timer - stTimer
Loop

我对 COM 对象知之甚少,所以可能还有其他我没有考虑到的问题


完整代码(针对类似问题改编自 ). Running the function repeatedly in quick succession produces errors due to IE taking time to shutdown (see this question)。如果您需要连续 运行 多个查询,请重复使用相同的 IE 对象:

Option Explicit

Public Function GOOGLE_COUNT(searchTerm As String, xRes As Long, yRes As Long, Optional timeout As Long = 10) As Long

    Dim url As String
    Dim objIE As Object
    Dim currPage As Object
    Dim stTimer As Double, tElapsed As Single
    Dim valueResult As Object

    'create URL to page with these image criteria
    url = "https://www.google.com/search?q=" & searchTerm & _
                        "&tbm=isch&source=lnt&tbs=isz:ex,iszw:" & xRes & ",iszh:" & yRes

    'initiating a new instance of Internet Explorer and asigning it to objIE
    Set objIE = CreateObject("InternetExplorer.Application")

    'Google images search
    objIE.navigate url
    Do While objIE.Busy = True Or objIE.readyState <> 4: DoEvents: Loop
    Set currPage = objIE.document
    Dim myDiv As Object: Set myDiv = currPage.getElementById("fbar")
    Dim elemRect As Object: Set elemRect = CallByName(myDiv, "getBoundingClientRect", VbMethod)
    stTimer = Timer
    'Scroll until bottom of page is in view
    Do Until elemRect.bottom > 0 Or tElapsed > timeout 'timeout after n seconds
        currPage.parentWindow.scrollBy 0, 10000
        Set elemRect = CallByName(myDiv, "getBoundingClientRect", VbMethod)
        tElapsed = Timer - stTimer
    Loop
    myDiv.ScrollIntoView
    'Count the images
    Set valueResult = currPage.getElementById("rg_s").getElementsByTagName("IMG")
    GOOGLE_COUNT = valueResult.Length
    objIE.Quit

End Function

Sub foo()

MsgBox GOOGLE_COUNT("St. Mary", 1366, 768)

End Sub