How to attach an event to IHTMLDocument2 link elements in Delphi? - delphi

I'm using this code to get all the links from an IHTMLDocument2:
procedure DoDocumentComplete(const pDisp: IDispatch; var URL: OleVariant);
var
Document:IHTMLDocument2;
Body:IHTMLElement;
Links:IHTMLElementCollection;
i:integer;
tmp:IHTMLElement;
begin
try
Document := (pDisp as IWebbrowser2).Document AS IHTMLDocument2;
Body := Document.body;
Links := Document.links;
for i := 0 to (Links.length-1) do
begin
tmp := (Links.item(i, 0) as IHTMLElement);
//tmp.onclick := HOW SHOULD I ADD THE CALLBACK HERE?
//ShowMessage(tmp.innerText);
end;
except
on E : Exception do
ShowMessage(E.ClassName+' error raised, with message : '+E.Message);
end;
end;
How could I attach a function/procedure to .onclick to do a simple task like show an alert with the anchor text when the link is clicked?

One way is to sink events from the TWebBrowser with an object which implements IDispatch (like http://groups.google.com/group/borland.public.delphi.oleautomation/msg/a57d99e0e52c78ce)
you would set
tmp.onclick := TEventObject.Create(callbackProcedure) as IDispatch;

I wouldn't recommend using the onXXX-handlers (like onClick) directly as this will replace any previouly attached handler. This can actually change/destroy behavior of the page. If you are working with a web page which is not under your control you better use attachEvent:
(tmp as IHTMLElement2).attachEvent('onclick', callbackProcedureDisp);
And don't forget to detach with detachEvent:
(tmp as IHTMLElement2).detachEvent('onclick', callbackProcedureDisp);
Attention: it is possible to attach the same handler multiple times. In this case your handler would also be called multiple times.
If you are only interested in onclick you could just add one handler to the root element and don't have to travel through all elements. MSDN states the event bubbles, so you could just attach one event handler to the document element and check the srcElement member of the IHTMLEventObj every time the event fires.

Related

When iterating through controls on a form, how can I identify particular buttons?

I need to make some changes to a TaskDialog before it is shown to the user. It's fairly simple to use Windows API calls to work with each of the controls on the dialog box. I need to be more sure which button I have found. I would have expected to find a place where I could read the result the button would give if pressed.
in other words, if I pressed a button that would cause a return value (in Delphi, it's called a modal result) of 100, I would have expected there to be an API call I could call to find out what the button's "return value" would be. I haven't yet found any such call.
I don't want to rely on the button text..
Here's what I have so far.
function EnumWindowsProcToFindDlgControls(hWindow: HWND; _param:LPARAM): BOOL; stdcall;
var
sClassName:string;
hBMP:THandle;
i:integer;
begin
SetLength(sClassName, MAX_PATH);
GetClassName(hWindow, PChar(sClassName), MAX_PATH);
SetLength(sClassName, StrLen(PChar(sClassName)));
if sClassName='Button' then
begin
// always 0...
i:=GetDlgCtrlID(hWindow);
if (i=100) or (i=102) then
begin
hBmp := LoadImage(HInstance, 'DISA', IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE or LR_LOADTRANSPARENT );
SendMessage(hWindow, BM_SETIMAGE, WPARAM(IMAGE_BITMAP), LPARAM(hBmp));
end;
end;
// keep looking
Result:=true;
end;
procedure TForm2.TaskDialog1DialogConstructed(Sender: TObject);
begin
EnumChildWindows(TaskDialog1.Handle, #EnumWindowsProcToFindDlgControls, 0);
end;
I suspect it's not entirely "respectable" to do things like this with a dialog.
This is a Delphi 10 Win32 application using Delphi's VCL TTaskDialog component which is a wrapper around Windows task dialog feature. before it's shown, the OnConstructed event fires, executing this code.
Thank you for your help!
Win32 buttons do not have "return values", which is why there is no API to retrieve such a value from them. What you are thinking of is strictly a VCL feature.
In Win32 API terms, a button can have a control ID, and in the case of MessageBox(), for example, standard ID values like IDOK, IDCANCEL, etc are assigned to the dialog buttons. When a button is clicked and the dialog is closed, the button's control ID is used as the function return value.
But task dialogs do not use control IDs, which is why you do not see any assigned to the dialog buttons.
To identify a particular task dialog button, I can think of two ways:
during child enumeration, retrieve each button's caption text (GetWindowText()), and compare that to captions you are interested in. Just know that the standard buttons (from the TTaskDialog.CommonButtons property) use localized text, which does not make this a well-suited option for locating standard buttons unless you have control over the app's locale settings.
send the dialog a TDM_ENABLE_BUTTON message to temporarily disable the desired button that has a given ID, then enumerate the dialog's controls until you find a disabled child window (using IsWindowEnabled()), and then re-enable the control. You can then manipulate the found window as needed.
For Task Dialog messages and Task Dialog Notifications that operate on buttons (like TDN_BUTTON_CLICKED, which triggers the TTaskDialog.OnButtonClicked event), the standard buttons use IDs like IDOK, IDCANCEL, etc while custom buttons (from the TTaskDialog.Buttons property) use their ModalResult property as their ID.
You can send TDM_ENABLE_BUTTON directly via SendMessage() for standard buttons, or via the TTaskDialogBaseButtonItem.Enabled property for custom buttons.
For #2, this works when I try it:
uses
Winapi.CommCtrl;
function FindDisabledDlgControl(hWindow: HWND; _param: LPARAM): BOOL; stdcall;
type
PHWND = ^HWND;
begin
if not IsWindowEnabled(hWindow) then
begin
PHWND(_param)^ := hWindow;
Result := False;
end else
Result := True;
end;
procedure TForm2.TaskDialog1DialogConstructed(Sender: TObject);
var
hButton: HWND;
begin
// common tcbOk button
SendMessage(TaskDialog1.Handle, TDM_ENABLE_BUTTON, IDOK, 0);
hButton := 0;
EnumChildWindows(TaskDialog1.Handle, #FindDisabledDlgControl, LPARAM(#hButton));
SendMessage(TaskDialog1.Handle, TDM_ENABLE_BUTTON, IDOK, 1);
if hButton <> 0 then
begin
// use hButton as needed...
end;
// custom button
TaskDialog1.Buttons[0].Enabled := False;
hButton := 0;
EnumChildWindows(TaskDialog1.Handle, #FindDisabledDlgControl, LPARAM(#hButton));
TaskDialog1.Buttons[0].Enabled := True;
if hButton <> 0 then
begin
// use hButton as needed...
end;
end;

how can I detect if a button was pressed on a simple webpage using embeddedwb

Am using embeddedwb on my application and I have a simple webpage with a button
<input name=mpi type=submit value=" Continue ">
I was trying with but it was no good
if E.outerHTML = '<input name=mpi type=submit value=" Continue ">' then
begin
if rLoginGate.IsConnectedToLoginGate then
begin
ToggleChatBtn;
end;
end;
now what I want to do is when I press the button I need my application to pick it up and run a simple command like a messagebox anyone got any ideas??
thanks
A way to do this is using the HTML DOM Object model interfaces in MSHTML.PAS.
My earlier answer, here: Detect when the active element in a TWebBrowser document changes shows how to access this via that Document object of a TWebBrowser. TEmbeddedWB provides access it via its Document object, too.
That answer and the comments to it shows how to catch events related to a specific node in the document, and also specific event(s).
Of course, if the HTML is under your control, you can make things easier for yourself by giving the HTML node(s) you're interested in an ID or attribute that's easy to find via the DOM model.
The following shows how to modify the code example in my linked answer
to attach an OnClick handler to a specific element node:
procedure TForm1.btnLoadClick(Sender: TObject);
var
V : OleVariant;
Doc1 : IHtmlDocument;
Doc2 : IHtmlDocument2;
E : IHtmlElement;
begin
// First, navigate to About:Blank to ensure that the WebBrowser's document is not null
WebBrowser1.Navigate('about:blank');
// Pick up the Document's IHTMLDocument2 interface, which we need for writing to the Document
Doc2 := WebBrowser1.Document as IHTMLDocument2;
// Pick up the Document's IHTMLDocument3 interface, which we need for finding e DOM
// Element by ID
Doc2.QueryInterface(IHTMLDocument3, Doc);
Assert(Doc <> Nil);
// Load the WebBrowser with the HTML contents of a TMemo
V := VarArrayCreate([0, 0], varVariant);
V[0] := Memo1.Lines.Text;
try
Doc2.Write(PSafeArray(TVarData(v).VArray));
finally
Doc2.Close;
end;
// Find the ElementNode whose OnClick we want to handle
V := Doc.getElementById('input1');
E := IDispatch(V) as IHtmlElement;
Assert(E <> Nil);
// Create an EventObject as per the linked answer
DocEvent := TEventObject.Create(Self.AnEvent, False) as IDispatch;
// Finally, assign the input1 Node's OnClick handler
E.onclick := DocEvent;
end;
PS: It's a while since I used TEmbeddedWB and it may turn out that there's a more direct way of doing this stuff, because it was subject to a lot of changes after I stopped using it (in the D5 era). Even so, you won't be wasting your time studying this stuff, because COM events are applicable to all sorts of things, not just the HTML DOM model.

Shortcut triggers TAction on first created form instead of form with focus

I found (in Delphi 2010) that shortcuts always end up on first form (as owned by main form) that has that action, but not the currently focused form. My TMainFrm owns several TViewFrm. Each has a TActionManager with the same TActons.
I see some ways out, but wonder whats the best fix.. (and not a bad hack)
The forms are navigated using a tabset which calls their Hide() and Show(). I'd did not expect hidden forms to receive keypresses. Am i doing something wrong?
It seems that action shortcuts are always start at the main form, and using TCustomForm.IsShortCut() get distributed to owned forms. I see no logic there to respect hidden windows, should i override it and have it trigger the focused form first?
Disabling all TActions in TViewFrm.Hide() .. ?
Moving the TActionToolBar to TMainFrm but that is a pit of snakes and last resort.
I have found a workaround thats good enough for me; my main form now overrides TCustomForm.IsShortcut() and first checks visible windows from my list of editor tabs.
A list which i conveniently already have, so this might not work for everyone.
// Override TCustomForm and make it check the currently focused tab/window first.
function TFormMain.IsShortCut(var Message: TWMKey): Boolean;
function DispatchShortCut(const Owner: TComponent) : Boolean; // copied function unchanged
var
I: Integer;
Component: TComponent;
begin
Result := False;
{ Dispatch to all children }
for I := 0 to Owner.ComponentCount - 1 do
begin
Component := Owner.Components[I];
if Component is TCustomActionList then
begin
if TCustomActionList(Component).IsShortCut(Message) then
begin
Result := True;
Exit;
end
end
else
begin
Result := DispatchShortCut(Component);
if Result then
Break;
end
end;
end;
var
form : TForm;
begin
Result := False;
// Check my menu
Result := Result or (Menu <> nil) and (Menu.WindowHandle <> 0) and
Menu.IsShortCut(Message);
// Check currently focused form <------------------- (the fix)
for form in FEditorTabs do
if form.Visible then
begin
Result := DispatchShortCut(form);
if Result then Break;
end;
// ^ wont work using GetActiveWindow() because it always returns Self.
// Check all owned components/forms (the normal behaviour)
if not Result then
Result := inherited IsShortCut(Message);
end;
Another solution would be to change DispatchShortCut() to check for components being visible and/or enabled, but that might impact more than i'd like. I wonder whether the original code architects had a reason not to -- by design. Best would be have it called twice: first to give priority to visible+enabled components, and second call as fallback to normal behavior.

How do I detect when TWebBrowser finishes downloading a page?

How to know if the TWebBrowser already finished to download the page?
My problem is: I can't know when my page was completely downloaded so it can be shown.
I request one page to my webbrowser and I want to show the response only when the page was completely downloaded.
You could try handling the OnDocumentComplete event.
If the site uses scripting to trigger downloading of additional data, you may have to employ more sophisticated methods since the event will fire before the page finishes running all its scripts. In general, the task begins to look like the halting problem. You might wish to refine your definition of "completely downloaded" to exclude certain difficult-to-detect cases.
source: http://www.delphifaq.com/faq/delphi/network/f264.shtml
Indeed, in case of multiple frames, OnDocumentComplete gets fired multiple times. Not every frame fires this event, but each frame that fires a DownloadBegin event will fire a corresponding DocumentComplete event.
How can the 'real completion' be recognized?
The OnDocumentComplete event sends parameter pDisp: IDispatch, which is the IDispatch of the frame (shdocvw) for which DocumentComplete is fired. The top-level frame fires the DocumentComplete in the end.
So, to check if a page is done downloading, you need to check if pDisp is same as the IDispatch of the WebBrowser control.
That's what the code below demonstrates:
procedure IForm1.WebBrowser1Documentccmplete(Sender: Iobject:
const pDisp: Inispatch; var URL: OLEvariant):
var
Curwebrowser : IWebBrowser:
IopWebBrowser: IWebBrowser:
Document : OLEvariant;
WindowName : string:
begin { TForm1.WebBrowser1DocumentComplete }
Curwebrowser := pDisp as IWebBrowser:
TopWebBrowser := (Sender as IWebBrowser).DefaultInterface;
if CurWebrowser=TopWebBrowser then
begin
ShowMessage('Document is complete.')
end
else
begin
Document := CurWebrowser.Document;
WindowName := Document.ParentWindow.Name:
ShowMessage('Frame ' + WindowName + ' is loaded.')
end:
end;

Flash ShowMessage(Pos) or any other predefined window in Dialogs unit in Delphi for Post-WInXP OS

This is approach I found for Tray ... :
http://www.programmersheaven.com/mb/delphikylix/257563/257563/how-can-i-make-a-system-tray-flash/
Does the same technique works for Dialogs ( as they are forms with addition params, in fact )?
Or I can do it with way faster methods like getting handle / address / interface and overload or overdrive the function with FlashWindow(Ex) method?
I mean - can I make, for example ShowMessage(), window / dialog flash using FlashWindowEx() method and if I can, can it be done using the example in link given above?
Please, point to best direction or clarify my doubts ..
Thanks.
Sorry for bad formulation of question.
The same technique applies to any top-level window, including dialog boxes. If you can get the window's handle, you can pass it to FlashWindowEx.
There are many ways a modal form or dialog (both VCL or native from the system) can be shown from a Delphi program, so you need to somehow hook into message processing and catch messages that are sent when a modal form or dialog is shown.
For that a message hook can be set using the SetWindowsHookEx() API function. Since you need this only while the application is inactive you could set it in the handler of the OnDeactivate application event, and reset it in the handler for the OnActivate application event:
var
gNextHook: HHOOK;
procedure TForm1.AppActivate(Sender: TObject);
begin
if gNextHook <> 0 then
UnhookWindowsHookEx(gNextHook);
gNextHook := 0;
end;
procedure TForm1.AppDeactivate(Sender: TObject);
begin
gNextHook := SetWindowsHookEx(WH_CALLWNDPROC, #WndProcHook, 0,
GetCurrentThreadId);
end;
The hook function would watch for messages that are sent when a modal dialog or form is shown, and call FlashWindowEx() with the correct parameters:
function WndProcHook(nCode: integer; AWParam: WPARAM; ALParam: LPARAM): LRESULT; stdcall;
var
DataPtr: PCWPStruct;
Fwi: TFlashWInfo;
begin
DataPtr := PCWPStruct(ALParam);
if (DataPtr^.message = WM_INITDIALOG)
or ((DataPtr^.message = CM_ACTIVATE) and (DataPtr^.lParam = 0) and (DataPtr^.wParam = 0))
then begin
Fwi.cbSize := SizeOf(TFlashWInfo);
// flash caption of new modal window
Fwi.hwnd := DataPtr^.hwnd;
Fwi.dwFlags := FLASHW_ALL or FLASHW_TIMERNOFG;
Fwi.uCount := 0;
Fwi.dwTimeout := 0;
FlashWindowEx(Fwi);
// uncomment this to flash task bar button as well
(*
Fwi.hwnd := Application.MainForm.Handle;
Fwi.dwFlags := FLASHW_TRAY or FLASHW_TIMERNOFG;
FlashWindowEx(Fwi);
*)
end;
Result := CallNextHookEx(gNextHook, nCode, AWParam, ALParam);
end;
I chose WM_INITDIALOG which is sent for native dialogs like the open or save dialogs, and CM_ACTIVATE which is sent when a VCL form is shown modally. There may be more such messages that need to be caught. Above code works for the MessageDlg() function, the Application.MessageBox() function and TOpenDialog at least.
Since these dialogs don't have their own taskbar button I added (commented out) code to flash the taskbar button of the main form as well. This isn't optimal, as they flash out of sync.
Tested with Delphi 2009 on Windows XP, all error handling omitted, use it as a starting point only.

Resources