5

guys: I got a problem about "how to get an IHTMLElementCollection obj which composed of several IHTMLElements" in object-pascal programming , my codes below:

function TExDomUtils.GetElementsByClassName(vDoc:IHTMLDocument3; strClassName:string):IHTMLElementCollection;
var
  vElementsAll : IHTMLElementCollection;
  vElementsRet : IHTMLElementCollection;
  vElement : IHTMLElement;
  docTmp : IHTMLDocument2;
  I ,J: Integer;
begin
  J := 0;
  vElementsAll := vDoc.getElementsByTagName('*');
  for I:=0 to vElementsAll.length - 1 do
  begin
    vElement := vElementsAll.item(I,0) as IHTMLElement;
    if vElement.getAttribute('class',0) = strClassName then
    begin
      // how to get an IHTMLElementCollection obj which composed of several IHTMLElements?
      J := J + 1;
    end;

  end;

  Result := vElementsRet;
end;
MarcoLin
  • 53
  • 1
  • 4
  • 3
    does it have to be `IHTMLElementCollection`? why not return a TList or array of `IHTMLElement`s? – kobik Jan 26 '13 at 11:52
  • @kobik, good point. I think you'll have to use collection different from the `IHTMLElementCollection` since you can find more than one element matching the class name in your document thus you would have to add child elements to the returned collection manually, what is IMHO impossible... – TLama Jan 26 '13 at 12:05
  • @TLama, The main problem is that you cant create a new instance of `IHTMLElementCollection`, e.g. `vElementsRet := CoHTMLElementCollection.Create as IHTMLElementCollection` (class not registered) and then add Elements to it... – kobik Jan 26 '13 at 12:14
  • @kobik, let's say the new instance of `IHTMLElementCollection` you would obtain from the `all` collection (and store its reference to a variable, e.g. the `Result`) the first time you'd find the matching class. The worse is adding elements to that collection. Except using another kind of collection, might probably help to build your own `IHTMLElement` structure and *export* it once you're done by `all`. – TLama Jan 26 '13 at 12:26
  • AHA, may be I just got a ROM brain . now I think add the elements to a list or an array like you said would be good, Thanks! – MarcoLin Jan 26 '13 at 13:21
  • Your current code vs. question you've asked are against themselves. Are you just going to list all elements by the class name ? I get your question as *"How to get all child elements of a certain element ?"*, but your method name looks like that you're trying to get list of all elements that matches a certain class name from the whole document. What is the real purpose of your method then ? – TLama Jan 26 '13 at 13:31
  • @TLama, I wrote this method to get all elements of a certain class name, and then return a list of these elements by adding them into an IHTMLElemntCollection obj. coz I thought if the type of the return value is IHTMLElementCollection, It may be good to see and understanded when my workmates use this method. So I want to find the way to put elements into an IHTMLElemntCollection obj. After I read the comment of you and kobik ,I thought may be It was not the right way to implement it like what I thought before. – MarcoLin Jan 26 '13 at 13:43

1 Answers1

5

You could simply create your own container class, such as TList<IHTMLElement> or an array of IHTMLElements:

type
  THTMLElements = array of IHTMLElement;

function GetElementsByClassName(ADoc: IDispatch; const strClassName: string): THTMLElements;
var
  vDocument: IHTMLDocument2;
  vElementsAll: IHTMLElementCollection;
  vElement: IHTMLElement;
  I, ElementCount: Integer;
begin
  Result := nil;
  ElementCount := 0;
  if not Supports(ADoc, IHTMLDocument2, vDocument) then
    raise Exception.Create('Invalid HTML document');
  vElementsAll := vDocument.all;
  SetLength(Result, vElementsAll.length); // set length to max elements
  for I := 0 to vElementsAll.length - 1 do
    if Supports(vElementsAll.item(I, EmptyParam), IHTMLElement, vElement) then
      if SameText(vElement.className, strClassName) then
      begin
        Result[ElementCount] := vElement;
        Inc(ElementCount);
      end;
  SetLength(Result, ElementCount); // adjust Result length
end;

Usage:

procedure TForm1.FormCreate(Sender: TObject);
begin
  WebBrowser1.Navigate('http://stackoverflow.com/questions/14535755/how-to-get-an-ihtmlelementcollection-obj-which-composed-of-several-ihtmlelements');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Elements: THTMLElements;
  I: Integer;
begin
  // show Tags information for SO page:
  Elements := GetElementsByClassName(WebBrowser1.Document, 'post-tag');
  ShowMessage(IntToStr(Length(Elements)));
  for I := 0 to Length(Elements) - 1 do
    Memo1.Lines.Add(Elements[I].innerHTML + ':' + Elements[I].getAttribute('href', 0));
end;

The main problem with returning the result as IHTMLElementCollection, is that IHTMLElementCollection is created internally by IHTMLDocument, and I could not find any way of creating a new instance of IHTMLElementCollection and adding references of the elements to it e.g.:

vElementsRet := CoHTMLElementCollection.Create as IHTMLElementCollection

will result Class not registered exception.

kobik
  • 21,001
  • 4
  • 61
  • 121
  • 1
    I would probably replace the `vDocument.getElementsByTagName('*')` simply by `vDocument.all` casted to the `IHTMLElementCollection`. [+1] – TLama Jan 26 '13 at 14:31
  • 1
    got it! It works fine with returning the array of IHTMLElement in mine! Thank you all ! – MarcoLin Jan 27 '13 at 02:32