Search code examples
vb.netmshtmlshdocvw

How to get the HTML element and the position in the element's text where a person has clicked


Using internet explorer I would like to get the position where a person has clicked on text. An error of 3 to 4 characters is fine. The text is not editable and is usually in a span element.

I am aware I could set up a click event listener for the HTMLDocument however I do not always have the HTMLDocument object and thus may miss the event.

I have tried getting a IHTMLSelectionObject, then creating a text range with the IHTMLTxtRange, however when the web page is simply clicked as opposed to at least 1 character being selected then the IHTMLTxtRange has a parent of the HTMLBody and not of the element that was clicked.

The HTMLDocument.activeElement is also unreliable. In my tests it never actually returns the element clicked, it usually returns a major parent of the element somewhere up the tree.

Using MSHTML is there another way to achieve this?

I have also tried using the WIN API GetCursorPos however I do not know what to do with this position, I do not know how to convert this into the actual element.

EDIT: I also thought of an interesting idea. When I need to know the element that has the cursor, I set a mouseDown or click event on the whole document. Then fire my own click and catch the event. In the IHTMLEventObj of the event is a FromElement which I had hoped would tell me where the cursor was. It seems it is always nothing for mouseDown and click events. For me at least this object is only used in for example mouseover events.

The following is what I have when at least a character is selected.

 Private Function GetHTMLSelection(ByVal aDoc As IHTMLDocument2, ByRef htmlText As String) As Integer

    Dim sel As IHTMLSelectionObject = Nothing
    Dim selectionRange As IHTMLTxtRange = Nothing
    Dim rangeParent As IHTMLElement4 = Nothing
    Dim duplicateRange As IHTMLTxtRange = Nothing
    Dim i As Integer
    Dim x As Integer
    Dim found As Boolean

    Try
        'get a selection
        sel = TryCast(aDoc.selection, IHTMLSelectionObject)

        If sel Is Nothing Then
            Return -1
        End If
        'the range of the selection.
        selectionRange = TryCast(sel.createRange, IHTMLTxtRange)

        If selectionRange Is Nothing Then
            Return -1
        End If
        'the the parent element of the range.
        rangeParent = TryCast(selectionRange.parentElement, IHTMLElement4)

        'duplicate our range so we can manipulate it.
        duplicateRange = TryCast(selectionRange.duplicate, IHTMLTxtRange)

        'make the dulicate range the whole element text.
        duplicateRange.moveToElementText(rangeParent)

        'get the length of the whole text
        i = duplicateRange.text.Length

        For x = 1 To i
            duplicateRange.moveStart("character", 1)

            If duplicateRange.compareEndPoints("StartToStart", selectionRange) = 0 Then
                found = True
                Exit For
            End If

        Next

        If found Then
            Debug.Print("Position is: " + x.ToString)
            htmlText = duplicateRange.text
            Return x
        Else
            Return -1
        End If


    Catch ex As Exception
        Return -1
    Finally

    End Try


End Function

Solution

  • I cannot post answer with a nice function that shows how to do this but I will explain the important parts.

    1. user the Win32 API GetCursorPos to get the point on the screen where the user last clicked.
    2. If you have iFrames which means more than one HTMLDocument then you need to loop through your iFrames and use the HTMLFrameElement clientWidth and clientHeight along with a IHTMLWindow3 screenTop and screenLeft to find out which HTMLDocument your point is on.
    3. Convert this point to a relative point using the IHTMLWindow you found in number 2.
    4. Once you have the right HTMLDocument and a point relative to this document you can then use the elementFromPoint method on a IHTMLDocument2 object.
    5. Once you have this you now know the point and element that was clicked on.

      Private Function getElementTextPosition() As Boolean

          Dim sel As IHTMLSelectionObject = Nothing
          Dim selectionRange As IHTMLTxtRange = Nothing
          Dim duplicateRange As IHTMLTxtRange = Nothing
          Dim i As Integer = 0
          Dim found As Boolean
          Dim x As Integer
      
          Try
              'elementWithCursor is a IHTMLElement class variable
              If elementWithCursor IsNot Nothing Then
                  ReleaseComObject(elementWithCursor)
                  elementWithCursor = Nothing
              End If
              'docWithCursor is also a IHTMLDocument2 class variable
              'cursorPointInDoc is the point relative to the actual document 
              elementWithCursor = TryCast(docWithCursor.elementFromPoint(cursorPointInDoc.X, cursorPointInDoc.Y), IHTMLElement)
      
              If elementWithCursor Is Nothing Then
                  Return False
              End If
      
              'get a selection
              sel = TryCast(docWithCursor.selection, IHTMLSelectionObject)
      
              If sel Is Nothing Then
                  Return False
              End If
      
              selectionRange = TryCast(sel.createRange, IHTMLTxtRange)
      
              If selectionRange Is Nothing Then
                  Return False
              End If
      
              'First check if We have selection text so we will use that as the selected text
              '_SelectedText relates to a class property
              If selectionRange.text IsNot Nothing Then
                  _SelectedText = selectionRange.text
                  selectionRange.collapse(True)
              Else
                  'the the parent element of the range.
                  selectionRange.moveToPoint(cursorPointInDoc.X, cursorPointInDoc.Y)
              End If
      
              'duplicate our range so we can manipulate it.
              duplicateRange = TryCast(selectionRange.duplicate, IHTMLTxtRange)
      
              'make the dulicate range the whole element text.
              duplicateRange.moveToElementText(elementWithCursor)
      
              'get the length of the whole text
              i = duplicateRange.text.Length
      
              For x = 0 To i
      
                  If duplicateRange.compareEndPoints("StartToStart", selectionRange) = 0 Then
                      found = True
                      Exit For
                  End If
      
                  duplicateRange.moveStart("character", 1)
      
              Next
      
              If found Then
                  '_CursorPositionInText is a class property and relates to the position where the person clicked in the html text.
                  _CursorPositionInText = x
                  _HTMLElementText = elementWithCursor.innerText
      
                  Return True
              Else
                  Return False
              End If
      
          Catch ex As Exception
              Return False
          End Try
      
      End Function