TWebBrowser - Detecting the tag under caret - html

I want to detect on which HTML tag (more exactly hyperlink) is the caret.
procedure THTMLEdit.ShowTag;
var
CursorPos: TPoint;
HtmlElement: IHTMLElement;
iHTMLDoc: IHtmlDocument2;
begin
if Supports(wbBrowser.Document, IHtmlDocument2, iHTMLDoc) then
begin
if GetcaretPos(CursorPos) then
begin
CursorPos := wbBrowser.screentoclient(CursorPos);
HtmlElement := iHTMLDoc.ElementFromPoint(CursorPos.X, CursorPos.Y); // I NEED KEYBOARD CARET HERE, NOT MOUSE CURSOR
if HtmlElement <> NIL
then label1.Caption:= HtmlElement.tagName;
end;
end;
end;
Notes:
TWebBrowser is in DesignMode ( DesignMode := 'On' ).
TWebBrowser is in its own form at design time but at runtime is re-parented in another form (in a panel).
UPDATE:
The thing that I need is IHTMLTxtRange (I think). It works when I double click a link/word. But I don't know how to get the tag under caret when no text/link is selected.

GetcaretPos(CursorPos) returns client (relative) coordinates (See GetCaretPos function)
Remove wbBrowser.screentoclient(CursorPos) and it should work fine. I have tested with your code sample above

Related

Delphi: Simulating a drag and drop from the clipboard to EmbeddedWB’s IHTMLElement

I have a Delphi XE2 application with a TEmbeddedWB that I use to simulate user actions. The application navigates to a URL, populates the relevant form fields with data and submits the data. The problem is that there is an <input type=file /> field which accepts files that are uploaded.
Having done a lot of reading on the matter I understand there is a security issue doing this programmatically but also found someone making a suggestion that the files could be ‘dragged’ from the clipboard and ‘dropped’ in place. I have since been successful in loading the relevant files (jpeg images) into the clipboard (thanks to CCR.Clipboard) and drop them onto my EmbeddedWB. However, as you are most likely aware, dropping an image on a TWebBrowser resorts to the image being displayed.
My issue is that the web page I’m accessing has a specific DIV element that accepts files to be dropped. Although I have successfully obtained the coordinates of that DIV as an IHTMLElement and even moved the mouse cursor into position (for visual confirmation), dropping an image there still opens it for display instead of uploading it. It’s as though the drop area doesn’t detect the drop, only the web browser does.
Any guidance on this matter will be greatly appreciated. Following is the relevant code.
Methods:
type
TElementsArray = array of IHTMLElement;
...
function TSiteRobot.FindElementByTagAttributeValue(const Document: IHTMLDocument2; TagName, Attribute, AttributeValue: String; out Info: String): IHTMLElement;
var i: integer;
HTMLElem: IHTMLElement;
ElementCount: integer;
OleElem: OleVariant;
ElementsArray: TElementsArray;
begin
Result := nil; //initialise
ElementsArray := GetElementsByTagName(Document, TagName);
if Length(ElementsArray) = 0 then
begin
Info := 'No elements with "'+TagName+'" tag found.';
Exit
end;
Info := 'No element found for tag "'+TagName+'" and attribute "'+Attribute+'" with Value "'+AttributeValue+'"';
for i := Low(ElementsArray) to High(ElementsArray) do
begin
HTMLElem := ElementsArray[i];
try
OleElem := HTMLElem.getAttribute(Attribute,0);
if (not varIsClear(OleElem)) and (OleElem <> null) then
begin
if (String(OleElem) = AttributeValue) then
begin
if HTMLElem <> nil then Result := HTMLElem;
Break;
end;
end;
except raise; end;
end;
end;
function TSiteRobot.GetElementScreenPos(WebBrowser: TEmbeddedWB; HTMLElement: IHTMLElement): TPoint;
var WinRect: TRect;
elTop, elLeft: integer;
HTMLElem2: IHTMLElement2;
begin
HTMLElement.scrollIntoView(True);
Application.ProcessMessages; //let the coordinates get updated since the page moved
GetWindowRect(WebBrowser.Handle, WinRect);
HTMLElem2 := (HTMLElement as IHTMLElement2);
elLeft := HTMLElem2.getBoundingClientRect.left + WinRect.Left;
elTop := HTMLElem2.getBoundingClientRect.top + WinRect.Top;
Result := Point(elLeft, elTop);
end;
procedure TfrmMain.DropFilesAtPoint(Area: TPoint; Wnd: HWND);
var DropTarget: IDropTarget;
DataObj: IDataObject;
DropFiles: PDropFiles;
StgMed: TSTGMEDIUM;
FormatEtc: TFORMATETC;
EnumFormatEtc: IEnumFORMATETC;
dwEffect: integer;
begin
DropTarget := IDropTarget(GetProp(Wnd, 'OleDropTargetInterface'));
OleGetClipboard(dataObj);
DataObj.EnumFormatEtc(DATADIR_GET, EnumFormatEtc);
while (EnumFormatEtc.Next(1, FormatEtc, nil) <> S_FALSE) do
begin
if (FormatEtc.cfFormat = CF_HDROP) and (DataObj.QueryGetData(FormatEtc) = S_OK) then
begin
DataObj.GetData(FormatEtc, StgMed);
DropFiles := GlobalLock(StgMed.hGlobal);
dwEffect := DROPEFFECT_COPY;
DropTarget.Drop(DataObj, Integer(DropFiles), Area, dwEffect); // This is where the image opens in the web browser
GlobalFree(StgMed.hGlobal);
ReleaseStgMedium(StgMed);
end;
end; //while
DataObj._Release;
end;
Calling Code:
var HTMLElem: IHTMLElement;
dndArea: TPoint;
…
HTMLElem := SiteRobot.FindElementByTagAttributeValue(Document, 'SPAN', 'id', 'dndArea', Info);
dndArea := SiteRobot.GetElementScreenPos(WebBrowser, HTMLElem);
dndArea.X := dndArea.X+24; //go ‘deeper’ into the drop area
dndArea.Y := dndArea.Y+24;
SetCursorPos(dndArea.X, dndArea.Y); //cursor moves onto the correct spot in the website every time
(HTMLElem as IHTMLElement2).focus;
DropFilesAtPoint(dndArea, webBrowser.Handle);
I have come to a solution regarding this problem. Rather than using the clipboard, I piggy-backed on Melander’s drag-and-drop PIDLDemo. Adding a TListView component to the form and giving it the ability to drag-and-drop files to the shell does the trick. Using Windows' MOUSE_EVENT I am able to (programmatically) drag the files from the TListView and drop them onto the TEmbeddedWB at the correct location. Presto! The files are accepted and uploaded to the website.
The calling code now looks as follows:
function TfrmMain.GetMickey(val: TPoint): TPoint;
begin
{
http://delphi.xcjc.net/viewthread.php?tid=43193
Mouse Coordinates given are in "Mickeys", where their are 65535 "Mickeys"
to a screen's width.
}
Result.X := Round(val.X * (65535 / Screen.Width));
Result.Y := Round(val.Y * (65535 / Screen.Height));
end;
procedure TfrmMain.DropFilesAtPoint(const Area: TPoint; Wnd: HWND);
var Rect: TRect;
DropPoint,
ListViewPoint,
ListViewItemPoint: TPoint;
begin
GetWindowRect(ListView1.Handle, Rect);
ListViewItemPoint := ListView1.Items.Item[0].GetPosition;
ListViewPoint := Point(Rect.Left + ListViewItemPoint.X+10,
Rect.Top + ListViewItemPoint.Y+10);
ListView1.SelectAll; //ensures all files are dragged together
SetCursorPos(ListViewPoint.X, ListViewPoint.Y);
ListViewPoint := GetMickey(ListViewPoint);
MOUSE_EVENT(MOUSEEVENTF_LEFTDOWN,
ListViewPoint.X, ListViewPoint.Y, 0, 0); //left mouse button down
Sleep(500);
DropPoint := ClientToScreen(Area);
DropPoint := GetMickey(DropPoint);
MOUSE_EVENT(MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE or
MOUSEEVENTF_LEFTDOWN or MOUSEEVENTF_LEFTUP,
DropPoint.X, DropPoint.Y, 0, 0); //move and drop
Application.ProcessMessages;
end;

Delphi TWebBrowser as HTML Editor - get font properties

I am using TWebBrowser as HTML editor in Delphi7, by setting it's designMode to 'on' in OnDocumentComplete.
I know how to change font properties like bold, italic, font, color, justify, etc. I am using exeCommand with parameters
var
htmlDoc: HTMLDocument;
parameter: OleVariant;
begin
(wbEditor.Document as IHTMLDocument2).ParentWindow.Focus;
htmlDoc := wbEditor.document as HTMLDocument;
htmlDoc.execCommand('bold', false, parameter);
(wbEditor.Document as IHTMLDocument2).ParentWindow.Focus;
end;
Question is how to read 'Bold' and other properties when I change cursor position inside the text.
Let's asume my text is like 'foo bar'. I want to have a 'Bold button' checked when I position my cursor at FOO, but unchecked when I position it at BAR.
???
Hej I found a walkaround on my own, used TEmbeddedWB instead TWebBrowser, and code below on it's OnClock and OnKeyDown events
var
doc: IHTMLDocument2;
sel: IHTMLSelectionObject;
range: IHTMLTxtRange;
begin
doc := wb1.Doc2;
if Assigned(Doc) then
begin
Sel := Doc.selection;
if Assigned(Sel) then
begin
if (Sel.type_ = 'None') or (Sel.type_ = 'Text') then
begin
Range := Sel.createRange as IHTMLTxtRange;
Caption := Range.queryCommandValue('justifyCenter');
end;
end;
end;
end;
thanks myself !!

Total height of html document loaded into TWebBrowser

I want to know how to get TOTAL height of html document loaded into TWebBrowser component (Delphi)?
I have found something like this and it is not working:
webbrowser.oleobject.document.body.scrollheight
I placed it inside OnDocumentComplete event.
I need height because I am calculating PageSize property of ScrollBar (my custom scrollbar - build-in WebBrowser is disabled) which depends on web page height.
Thanks for any feedback, best regards
Something like this should work:
uses MSHTML;
var
HtmlElement: IHTMLElement2;
PageHeight: Integer;
begin
with MyWebBrowser.ControlInterface do
begin
HtmlElement := (Document as IHTMLDocument3).documentElement as IHTMLElement2;
end;
PageHeight := HtmlElement.scrollHeight;
end;
This is the full height. The body element seems to give a bit smaller value (probably thanks to margins):
var
BodyElement: IHTMLElement2;
PageHeight: Integer;
begin
with MyWebBrowser.ControlInterface do
begin
BodyElement := (Document as IHTMLDocument2).body as IHTMLElement2;
end;
PageHeight := BodyElement.scrollHeight;
end;

Replace chars in a HTML string - Except Tags

I need to go through a HTML string and replace characters with 0 (zero), except tags, spaces and line breaks. I created this code bellow, but it is too slow. Please, can someone help me to make it faster (optimize)?
procedure TForm1.btn1Click(Sender: TObject);
var
Txt: String;
Idx: Integer;
Tag: Boolean;
begin
Tag := False;
Txt := mem1.Text;
For Idx := 0 to Length(Txt) - 1 Do
Begin
If (Txt[Idx] = '<') Then
Tag := True Else
If (Txt[Idx] = '>') Then
Begin
Tag := False;
Continue;
end;
If Tag Then Continue;
If (not (Txt[Idx] in [#10, #13, #32])) Then
Txt[Idx] := '0';
end;
mem2.Text := Txt;
end;
The HTML text will never have "<" or ">" outside tags (in the middle of text), so I do not need to worry about this.
Thank you!
That looks pretty straightforward. It's hard to be sure without profiling the code against the data you're using, (which is always a good idea; if you need to optimize Delphi code, try running it through Sampling Profiler first to get an idea where you're actually spending all your time,) but if I had to make an educated guess, I'd guess that your bottleneck is in this line:
Txt[Idx] := '0';
As part of the compiler's guarantee of safe copy-on-write semantics for the string type, every write to an individual element (character) of a string involves a hidden call to the UniqueString routine. This makes sure that you're not changing a string that something else, somewhere else, holds a reference to.
In this particular case, that's not necessary, because you got the string fresh in the start of this routine and you know it's unique. There's a way around it, if you're careful.
CLEAR AND UNAMBIGUOUS WARNING: Do not do what I'm about to explain without making sure you have a unique string first! The easiest way to accomplish this is to call UniqueString manually. Also, do not do anything during the loop that could assign this string to any other variable. While we're doing this, it's not being treated as a normal string. Failure to heed this warning can cause data corruption.
OK, now that that's been explained, you can use a pointer to access the characters of the string directly, and get around the compiler's safeguards, like so:
procedure TForm1.btn1Click(Sender: TObject);
var
Txt: String;
Idx: Integer;
Tag: Boolean;
current: PChar; //pointer to a character
begin
Tag := False;
Txt := mem1.Text;
UniqueString(txt); //very important
if length(txt) = 0 then
Exit; //If you don't check this, the next line will raise an AV on a blank string
current := #txt[1];
dec(current); //you need to start before element 1, but the compiler won't let you
//assign to element 0
For Idx := 0 to Length(Txt) - 1 Do
Begin
inc(current); //put this at the top of the loop, to handle Continue cases correctly
If (current^ = '<') Then
Tag := True Else
If (current^ = '>') Then
Begin
Tag := False;
Continue;
end;
If Tag Then Continue;
If (not (current^ in [#10, #13, #32])) Then
current^ := '0';
end;
mem2.Text := Txt;
end;
This changes the metaphor. Instead of indexing into the string as an array, we're treating it like a tape, with the pointer as the head, moving forward one character at a time, scanning from beginning to end, and changing the character under it when appropriate. No redundant calls to UniqueString, and no repeatedly calculating offsets, which means this can be a lot faster.
Be very careful when using pointers like this. The compiler's safety checks are there for a good reason, and using pointers steps outside of them. But sometimes, they can really help speed things up in your code. And again, profile before trying anything like this. Make sure that you know what's slowing things down, instead of just thinking you know. If it turns out to be something else that's running slow, don't do this; find a solution to the real problem instead.
Edit: Looks like I was wrong - UniqueString is not the problem. The actual bottleneck seems to be accessing the string by character. Given that my entire answer was irrelevent, I've completely replaced it.
If you use a PChar to avoid recalculating the string offset, while still updating the string via Txt[Idx], the method is much faster (5 seconds down to 0.5 seconds in my test of 1000 runs).
Here's my version:
procedure TForm1.btn1Click(Sender: TObject);
var
Idx: Integer;
Tag: Boolean;
p : PChar;
Txt : string;
begin
Tag := False;
Txt := Mem1.Text;
p := PChar(txt);
Dec(p);
For Idx := 0 to Length(Txt) - 1 Do
Begin
Inc(p);
If (not Tag and (p^ = '<')) Then begin
Tag := True;
Continue;
end
Else If (Tag and (p^ = '>')) Then
Begin
Tag := False;
Continue;
end;
If Tag Then Continue;
If (not (p^ in [#10, #13, #32])) Then begin
Txt[Idx] := '0';
end;
end;
mem2.Text := Txt;
end;
I did some profiling and came up with this solution.
A test for > #32 instead of [#10,#13,#32] gains some speed (thanks #DavidHeffernan).
A better logic in the loop also gives a bit extra speed.
Accessing the string exclusively with the help of a PChar is more effective.
procedure TransformHTML( var Txt : String);
var
IterCnt : Integer;
PTxt : PChar;
tag : Boolean;
begin
PTxt := PChar(Txt);
Dec(PTxt);
tag := false;
for IterCnt := 0 to Length(Txt)-1 do
begin
Inc(PTxt);
if (PTxt^ = '<') then
tag := true
else
if (PTxt^ = '>') then
tag := false
else
if (not tag) and (PTxt^ > #32) then
PTxt^ := '0';
end;
end;
This solution is about 30% more effective than Mason's solution and 2.5 times more effective than Blorgbeard's.

How to set entire HTML in MSHTML?

How to set entire HTML in MSHTML?
I am trying using this assignment:
(Document as IHTMLDocument3).documentElement.innerHTML := 'abc';
but I got the error:
"Target element invalid for this
operation"
I've also tried using
(Document as IHTMLDocument2).write
but this form only adds HTML into the body section, and I need to replace all the HTML source.
Does somebody have any idea of how I do this?
Thanks in advance.
Here's some of my old code, see if it helps you:
type
THackMemoryStream = class(TMemoryStream);
procedure Clear(const Document: IHTMLDocument2);
begin
Document.write(PSafeArray(VarArrayAsPSafeArray(VarArrayOf([WideString('')]))));
Document.close;
end;
procedure LoadFromStream(const Document: IHTMLDocument2; Stream: TStream);
var
Persist: IPersistStreamInit;
begin
Clear(Document);
Persist := (Document as IDispatch) as IPersistStreamInit;
OleCheck(Persist.InitNew);
OleCheck(Persist.Load(TStreamAdapter.Create(Stream)));
end;
procedure SetHtml(const Document: IHTMLDocument2; const Html: WideString);
var
Stream: TMemoryStream;
begin
Stream := TMemoryStream.Create;
try
THackMemoryStream(Stream).SetPointer(PWideChar(Html), (Length(Html) + 1) * SizeOf(WideChar));
Stream.Seek(0, soFromBeginning);
LoadFromStream(Document, Stream);
finally
Stream.Free;
end;
end;
As an alternative you can also use TEmbededWB which is an extended wrapper around a web browser and has some easy to use methods that provide this functionality.