How to attach context menu to TChromium browser - delphi

I have a TChromium broser from Delphi Chromium Embedded (http://code.google.com/p/delphichromiumembedded). I would like to attach a context menu to it. How I can achieve that?

You need to handle the OnBeforeMenu event. In that event handler is enough to set the output parameter Result to True what will suppress the default context menus to popup. After that you can display your own menu on the positions obtained from the menuInfo structure.
Here's the code sample with a custom popup menu:
uses
ceflib, cefvcl;
procedure TForm1.FormCreate(Sender: TObject);
begin
Chromium1.Load('www.example.com');
end;
procedure TForm1.Chromium1BeforeMenu(Sender: TObject;
const browser: ICefBrowser; const menuInfo: PCefHandlerMenuInfo;
out Result: Boolean);
begin
Result := True;
PopupMenu1.Popup(menuInfo.x, menuInfo.y);
end;
procedure TForm1.PopupMenuItemClick(Sender: TObject);
begin
ShowMessage('You''ve clicked on a custom popup item :)');
end;
Update:
For dynamically created instance you have to assign the event handler manually. Try the following code.
uses
ceflib, cefvcl;
type
TForm1 = class(TForm)
Panel1: TPanel;
Button1: TButton;
PopupMenu1: TPopupMenu;
procedure Button1Click(Sender: TObject);
private
procedure ChromiumOnBeforeMenu(Sender: TObject;
const browser: ICefBrowser; const menuInfo: PCefHandlerMenuInfo;
out Result: Boolean);
public
{ Public declarations }
end;
implementation
procedure Form1.ChromiumOnBeforeMenu(Sender: TObject; const browser: ICefBrowser;
const menuInfo: PCefHandlerMenuInfo; out Result: Boolean);
begin
Result := True;
PopupMenu1.Popup(menuInfo.x, menuInfo.y);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Chromium: TChromium;
begin
// owner is responsible for destroying the component
// in this case you are telling to Panel1 to destroy
// the Chromium instance before he destroys itself,
// it doesn't affect the event handling
Chromium := TChromium.Create(Panel1);
Chromium.Parent := Panel1;
Chromium.Left := 10;
Chromium.Top := 10;
Chromium.Width := Panel1.Width - 20;
Chromium.Height := Panel1.Height - 20;
// this line is important, you are assigning the event
// handler for OnBeforeMenu event, so in fact you tell
// to the Chromium; hey if the OnBeforeMenu fires, run
// the code I'm pointing at, in this case will execute
// the ChromiumOnBeforeMenu procedure
Chromium.OnBeforeMenu := ChromiumOnBeforeMenu;
Chromium.Load('www.example.com');
end;

actually you dont need popupmenu and you dont have to have add vcl.menus unit into your application if you have already can build chromium's context menu. also chromium's own menu is more modern and clear look like and faster draw perfrmance rather than a vcl which uses vintage win32 api library.
cef3 has its menu totally configurable like this.
procedure Tfmmain.Chromium1BeforeContextMenu(Sender: TObject;
const browser: ICefBrowser; const frame: ICefFrame;
const params: ICefContextMenuParams; const model: ICefMenuModel);
begin
model.Clear;
model.AddItem(1, 'Your Command 1');
model.AddItem(2, 'Your Command 2');
model.AddSeparator;
model.AddItem(3, 'Your Command 3');
model.AddItem(4, 'your Command 4');
model.AddSeparator;
model.AddItem(999, 'Quit');
model.SetAccelerator(1, VK_RIGHT, false, false, false);
model.SetAccelerator(2, VK_LEFT, false, false, false);
model.SetAccelerator(3, VK_DOWN, false, false, false);
model.SetAccelerator(4, VK_UP, false, false, false);
model.SetAccelerator(999, VK_ESCAPE, false, false, false);
end;
procedure Tfmmain.Chromium1ContextMenuCommand(Sender: TObject;
const browser: ICefBrowser; const frame: ICefFrame;
const params: ICefContextMenuParams; commandId: Integer;
eventFlags: TCefEventFlags; out Result: Boolean);
begin
case commandId of
1:
begin
DoIt1;
Result := true;
end;
2:
begin
DoIt2;
Result := true;
end;
3:
begin
DoIt3;
Result := true;
end;
4:
DoIt4;
Result := true;
end;
999:
begin
Application.MainForm.Close;
Result := true;
end;
end;
end;
note:SetAccelerator shortcuts only functional if popup appears.so you may need onPreKeyEvent

Related

Detecting changes in an editable TWebBrowser

I'm loading an HTML local file into TWebBrowser as follows:
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser1.Navigate('file:///C:\Tmp\input.html');
end;
In the TWebBrowser.OnDocumentComplete event handler I'm making it editable:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
(WebBrowser1.Document as IHTMLDocument2).designMode := 'on';
end;
I need to be notified as soon as the user applies any changes through the TWebBrowser (i.e: he writes something...) but I can't see any OnChanged or similar event handler.
I've tried capturing WM_PASTE and WM_KEYDOWN but my code is never executed:
TMyWebBrowser = class(TWebBrowser)
public
procedure WM_Paste(var Message: TWMPaste); message WM_PASTE;
procedure WM_KeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
end;
...
procedure TMyWebBrowser.WM_Paste(var Message: TWMPaste);
begin
inherited;
ShowMessage('Paste');
end;
procedure TMyWebBrowser.WM_KEYDOWN(var Message: TWMKeyDown);
begin
inherited;
ShowMessage('KeyDown');
end;
I've also tried setting the WindowProc property but without any success.
To capture changes to the document in design mode you should use its IMarkupContainer2 interface to register an IHTMLChangeSink via RegisterForDirtyRange method. The process is pretty simple - implement IHTMLChangeSink, obtain IMarkupContainer2 from WebBrowser1.Document and call its RegisterForDirtyRange method, but there's a catch.
When you change the designMode of IHTMLDocument2, TWebBrowser control reloads the current document and it loses all registered change sinks. Therefore you should register it after putting the document in design mode. After that you receive change notifications via IHTMLChangeSink.Notify method.
But there's another catch. Since entering the design mode causes reloading of the document and that in turn causes changing the readyState property of the document to 'loading' and then consecutively to 'complete'. Your change sink will receive those readyState change notifications. Note that TWebBrowser.OnDocumentComplete is not invoked after entering design mode. That's why you should ignore any notifications until the document is fully reloaded in design mode.
Another minor complication is that RegisterForDirtyRange creates a cookie that you need to maintain in order to unregister the change sink. Since you need a class to implement IHTMLChangeSink anyway, it could also encapsulate the design mode state and change registration.
uses
System.SysUtils, SHDocVw, MSHTML;
const
DesignMode: array[Boolean] of string = ('off', 'on');
type
TWebBrowserDesign = class(TInterfacedObject, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TProc;
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
public
constructor Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
destructor Destroy; override;
end;
constructor TWebBrowserDesign.Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
begin
inherited Create;
if not Assigned(WebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(WebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
FHTMLDocument2.designMode := DesignMode[True];
if Supports(WebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0
else
_Release;
end;
FOnChange := AOnChange;
end;
destructor TWebBrowserDesign.Destroy;
begin
if Assigned(FMarkupContainer2) and (FDirtyRangeCookie <> 0) then
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
if Assigned(FHTMLDocument2) then
FHTMLDocument2.designMode := DesignMode[False];
inherited;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange();
end;
Note the call to _Release after registering the change sink. This is to "prevent" markup container from holding strong reference to TWebBrowserDesign instance. That allows you to control design mode using the lifetime of TWebBrowserDesign instance:
type
TForm1 = class(TForm)
{ ... }
private
FWebBrowserDesign: IInterface;
{ ... }
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
FWebBrowserDesign := TWebBrowserDesign.Create(WebBrowser1, procedure
begin
ButtonSave.Enabled := True;
end);
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
FWebBrowserDesign := nil;
ButtonSave.Enabled := False;
end;
Alternatively you can implement change sink as a component.
type
TWebBrowserDesign = class(TComponent, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TNotifyEvent;
FWebBrowser: TWebBrowser;
procedure EnterDesignMode;
procedure ExitDesignMode;
function GetActive: Boolean;
procedure SetActive(const Value: Boolean);
procedure SetWebBrowser(const Value: TWebBrowser);
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
destructor Destroy; override;
published
property Active: Boolean read GetActive write SetActive;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property WebBrowser: TWebBrowser read FWebBrowser write SetWebBrowser;
end;
destructor TWebBrowserDesign.Destroy;
begin
ExitDesignMode;
inherited;
end;
procedure TWebBrowserDesign.EnterDesignMode;
begin
if not Assigned(FWebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(FWebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
try
FHTMLDocument2.designMode := DesignMode[True];
if Supports(FWebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0;
end;
except
ExitDesignMode;
raise;
end;
end;
procedure TWebBrowserDesign.ExitDesignMode;
begin
if Assigned(FMarkupContainer2) then
begin
if FDirtyRangeCookie <> 0 then
begin
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
FDirtyRangeCookie := 0;
end;
FMarkupContainer2 := nil;
end;
if Assigned(FHTMLDocument2) then
begin
FHTMLDocument2.designMode := DesignMode[False];
if not (csDestroying in ComponentState) then
FHTMLDocument2 := nil; { causes AV when its hosting TWebBrowser component is destroying; I didn't dig into details }
end;
FDocumentComplete := False;
end;
function TWebBrowserDesign.GetActive: Boolean;
begin
Result := Assigned(FHTMLDocument2);
end;
procedure TWebBrowserDesign.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FWebBrowser) then
WebBrowser := nil;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TWebBrowserDesign.SetActive(const Value: Boolean);
begin
if Active <> Value then
begin
if Value then
EnterDesignMode
else
ExitDesignMode;
end;
end;
procedure TWebBrowserDesign.SetWebBrowser(const Value: TWebBrowser);
begin
if Assigned(FWebBrowser) then
begin
ExitDesignMode;
FWebBrowser.RemoveFreeNotification(Self);
end;
FWebBrowser := Value;
if Assigned(FWebBrowser) then
FWebBrowser.FreeNotification(Self);
end;
If you put such a component in a design-time package and register it within the IDE, then you'll be able to link this component with TWebBrowser and assign OnChange event handler in the form designer. Use Active property in code to enter/exit the design mode.
type
TForm1 = class(TForm)
{ ... }
WebBrowserDesign1: TWebBrowserDesign;
{ ... }
end;
procedure WebBrowserDesign1Change(Sender: TObject);
begin
ButtonSave.Enabled := True;
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
WebBrowserDesign1.Active := True;
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
WebBrowserDesign1.Active := False;
ButtonSave.Enabled := False;
end;
NB: Similar question has been asked regarding C#/WinForms - How do I detect when the content of a WebBrowser control has changed (in design mode)?
Final note: I'm not convinced that enabling save button after a change is the best UX design. If you think that the code above is worth to achieve your goal then go ahead. This is just a proof of concept and the code hasn't been thoroughly tested. Use it at your own risk.

TChromium : how to save a specific image to file?

I'm using TChromium and I need to navigate to a specific page and save a specific image of this page to a file.
I know how to navigate and extract the HTML source to get the image's address, but I don't know how to save the image to my local filesystem.
How can I do it using some TChromium method ?
I don't want to use another component (such TIdHTTP) to do it because the site requires login and the image relies on the active session.
Thanks in advance !
From CEF forums:
"CEF does not currently support the extraction of cached resources.
You can identify the request that originally returned the content by
overriding CefRequestHandler::OnBeforeResourceLoad() and then execute
the request yourself using CefWebURLRequest the retrieve and save the
contents."
Another approach is to add a context menu as asked here - TChromium how to add "Save Picture" item in Context Menu? and where TLama has made a code snippet:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtDlgs, IdHTTP, cefvcl, ceflib;
const
MENU_ID_SAVE_IMAGE_AS = Ord(MENU_ID_USER_FIRST) + 1;
type
TDownloader = class(TThread)
private
FURL: string;
FFileName: string;
protected
procedure Execute; override;
public
constructor Create(const URL, FileName: string); reintroduce;
end;
type
TForm1 = class(TForm)
Button1: TButton;
Chromium1: TChromium;
SavePictureDialog1: TSavePictureDialog;
procedure FormCreate(Sender: TObject);
procedure Chromium1BeforeContextMenu(Sender: TObject; const browser: ICefBrowser;
const frame: ICefFrame; const params: ICefContextMenuParams; const model: ICefMenuModel);
procedure Chromium1ContextMenuCommand(Sender: TObject; const browser: ICefBrowser;
const frame: ICefFrame; const params: ICefContextMenuParams; commandId: Integer;
eventFlags: TCefEventFlags; out Result: Boolean);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TDownloader }
constructor TDownloader.Create(const URL, FileName: string);
begin
inherited Create(False);
FreeOnTerminate := True;
FURL := URL;
FFileName := FileName;
end;
procedure TDownloader.Execute;
var
HTTPClient: TIdHTTP;
FileStream: TFileStream;
begin
try
HTTPClient := TIdHTTP.Create;
try
FileStream := TFileStream.Create(FFileName, fmCreate);
try
HTTPClient.Get(FURL, FileStream);
finally
FileStream.Free;
end;
finally
HTTPClient.Free;
end;
except
// error handling ignored for this example
end;
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Chromium1.Load('http://www.google.com/');
end;
procedure TForm1.Chromium1BeforeContextMenu(Sender: TObject; const browser: ICefBrowser;
const frame: ICefFrame; const params: ICefContextMenuParams; const model: ICefMenuModel);
begin
if (CM_TYPEFLAG_MEDIA in params.TypeFlags) and (params.MediaType = CM_MEDIATYPE_IMAGE) then
model.AddItem(MENU_ID_SAVE_IMAGE_AS, 'Save image as...');
end;
procedure TForm1.Chromium1ContextMenuCommand(Sender: TObject; const browser: ICefBrowser;
const frame: ICefFrame; const params: ICefContextMenuParams; commandId: Integer;
eventFlags: TCefEventFlags; out Result: Boolean);
var
SaveDialog: TSavePictureDialog;
begin
if (commandId = MENU_ID_SAVE_IMAGE_AS) then
begin
SaveDialog := TSavePictureDialog.Create(nil);
try
// SaveDialog.FileName := <here you can extract file name from params.SourceUrl>;
// SaveDialog.DefaultExt := <here you can extract file ext from params.SourceUrl>;
if SaveDialog.Execute then
TDownloader.Create(params.SourceUrl, SaveDialog.FileName);
finally
SaveDialog.Free;
end;
end;
end;
end.
Another approach is to identify all the images from the page (take a look at How can I use Javascript to get a list of all picture URLs available on a site?) and download the image link by using CefBrowserHost.StartDownload.
Add to Form1:
public
var file_download_finished : boolean;
procedure TForm1.Chromium1BeforeDownload(Sender: TObject;
const browser: ICefBrowser; const downloadItem: ICefDownloadItem;
const suggestedName: ustring; const callback: ICefBeforeDownloadCallback);
begin
callback.Cont('FileName.jpg', false);
end;
procedure TForm1.Chromium1DownloadUpdated(Sender: TObject;
const browser: ICefBrowser; const downloadItem: ICefDownloadItem;
const callback: ICefDownloadItemCallback);
var i : nativeint;
begin
if downloadItem.IsComplete then
file_download_finished := true
else
i := downloadItem.PercentComplete;
end;
procedure TForm1.Button1Click(Sender: TObject);
var M: TMsg;
begin
file_download_finished := false;
Chromium1.Browser.Host.StartDownload('https://www.gravatar.com/avatar/7ffbdc105c382c0070cdd29d073725b5?s=48&d=identicon&r=PG&f=1');
repeat // wait until download is finished
while PeekMessage(M, 0, 0, 0, pm_Remove) do
begin
TranslateMessage(M);
DispatchMessage(M);
end;
until file_download_finished;
end;

Enumerating DOM nodes in TChromium

I am trying to enumerate DOM nodes using the following code (under XE2).
I have borrowed most of this from answers given here in SO, but for some reason it's not doing anything.
IOW, ProcessDOM() is not ever getting called.
And, I am at my wits end.
Could someone show me what I am doing wrong here.
Thanks in advance.
procedure ProcessNode(ANode: ICefDomNode);
var
Node1: ICefDomNode;
begin
if Assigned(ANode) then begin
Node1 := ANode.FirstChild;
while Assigned(Node1) do begin
{Do stuff with node}
ProcessNode(Node1);
Node1 := Node1.NextSibling;
end;
end;
end;
procedure ProcessDOM(const ADocument: ICefDomDocument);
begin
ProcessNode(ADocument.Body);
end;
procedure TMainForm.Chrome1LoadEnd(Sender: TObject; const ABrowser: ICefABrowser; const AFrame: ICefAFrame; AStatus: Integer);
begin
if Assigned(AFrame) then AFrame.VisitDomProc(ProcessDOM);
end;
I had the same problem and I used the demo guiclient it comes with dcef3. With the following it works.
type TCustomRenderProcessHandler = class(TCefRenderProcessHandlerOwn)
protected
function OnProcessMessageReceived(const browser: ICefBrowser; sourceProcess: TCefProcessId; const message: ICefProcessMessage): Boolean; override;
end;
Chromium1.browser.SendProcessMessage(PID_RENDERER, TCefProcessMessageRef.New('visitdom'));
function TCustomRenderProcessHandler.OnProcessMessageReceived(browser: ICefBrowser; sourceProcess: TCefProcessId; message: ICefProcessMessage): Boolean;
begin
if (message.Name = 'visitdom') then begin
browser.MainFrame.VisitDomProc(
procedure(const doc: ICefDomDocument)
begin
ProcessNode(Doc.Body);
end);
Result := True;
end;
end;
initialization
CefRenderProcessHandler := TCustomRenderProcessHandler.Create;
You need to add a procedure to the handler.
procedure ProcessNode(ANode: ICefDomNode);
Read this: 1
As this blog point out, The main difficulty when accessing a rendered page's DOM is that you can only do so in the same process as the associated renderer for that page.
You can't access dom from browser thread, you have to do it in renderer thread.
First, Forward a message (like visitdom) from browser process to rendering process
procedure TMainForm.crmLoadEnd(Sender: TObject; const browser: ICefBrowser;
const frame: ICefFrame; httpStatusCode: Integer);
var
msg : ICefProcessMessage;
begin
if IsMain(browser, frame) then
FLoading := False;
msg := TCefProcessMessageRef.New('visitdom');
browser.SendProcessMessage(PID_RENDERER, msg);
end;
Second, create a TCustomRenderProcessHandler to handle the message, send the result back to the browser processs.
function TCustomRenderProcessHandler.OnProcessMessageReceived(
const browser: ICefBrowser; sourceProcess: TCefProcessId;
const message: ICefProcessMessage): Boolean;
begin
Result := False;
if (message.Name = 'visitdom') then
begin
browser.MainFrame.VisitDomProc(
procedure(const doc: ICefDomDocument)
function ProcessNode(ANode: ICefDomNode) : String;
var
Node: ICefDomNode;
begin
Result := 'Not Found';
if Assigned(ANode) then
begin
Node := ANode.FirstChild;
while Assigned(Node) do
begin
if Node.ElementTagName='DIV' then
begin
if Node.GetElementAttribute('class')='tv-panels' then
begin
Result := 'Found';
Exit;
end;
end;
ProcessNode(Node);
Node := Node.NextSibling;
end;
end;
end;
var msg : ICefProcessMessage;
begin
msg := TCefProcessMessageRef.New('visitdom');
msg.ArgumentList.SetString(0, processNode(doc.Body));
browser.SendProcessMessage(PID_BROWSER, msg);
end);
Result := True;
end;
end;
Third, On browser process, create an handler to process the messenage sent back from render process.
procedure TMainForm.crmProcessMessageReceived(Sender: TObject;
const browser: ICefBrowser; sourceProcess: TCefProcessId;
const message: ICefProcessMessage; out Result: Boolean);
begin
Result := False;
if (message.Name = 'visitdom') then
begin
StatusBar.SimpleText := message.ArgumentList.GetString(0);
Result := True;
end;
end;
Be careful, while debuging, placing a breakpoint in rendering process never work. It will never reached there.

TWebBrowser: Zoom + "one window mode" incompatible

What I'm trying:
I need a TWebBrowser which is always zoomed in (~140%) AND keeps all links in the same webbrowser (ie. _BLANK links should be opened in the same browser control).
How I'm doing that:
I have set the FEATURE_BROWSER_EMULATION in registry to 9999, so the webpages are rendered with IE9. I have confirmed that this is working. Furthermore, I'm running the compiled program on a fresh install of Windows 7 with IE9, fully updated through Windows Update.
Zoom:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
ZoomFac: OLEVariant;
begin
ZoomFac := 140;
WebBrowser1.ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, ZoomFac);
end;
This works perfectly.
Open new windows in the same browser control:
By default, TWebBrowser opens a new IE, when it encounters a link set to be opened in a new window. I need it to stay in my program/webbrowser.
I have tried many things here. This works for me:
procedure TFormWeb.WebBrowser1NewWindow3(ASender: TObject;
var ppDisp: IDispatch; var Cancel: WordBool; dwFlags: Cardinal;
const bstrUrlContext, bstrUrl: WideString);
begin
Cancel := True;
WebBrowser1.Navigate(bstrUrl);
end;
I cancel the new window, and instead just navigate to the same URL.
Other sources on various pages on the Internet suggests that I don't cancel and instead set ppDisp to various things such as WebBrowser1.DefaultDispath or WebBrowser1.Application and variations of them. This does not work for me. When I click a _BLANK link, nothing happens. This is tested on two computers (both Win7 and IE9). I don't know why it doesn't work, because this seems to be working for other people on the Internet. Maybe this will solve the problem?
Now the problem:
When I combine these 2 pieces of code, it breaks!
procedure TForm1.Button1Click(Sender: TObject);
begin
WebBrowser1.Navigate('http://wbm.dk/test.htm');
// This is a test page, that I created. It just contains a normal link to google.com
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
ZoomFac: OLEVariant;
begin
ZoomFac := 140;
WebBrowser1.ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, ZoomFac);
end;
procedure TForm1.WebBrowser1NewWindow3(ASender: TObject; var ppDisp: IDispatch;
var Cancel: WordBool; dwFlags: Cardinal; const bstrUrlContext,
bstrUrl: WideString);
begin
Cancel := True;
WebBrowser1.Navigate(bstrUrl);
end;
When clicking a link (no matter if it is normal or _BLANK) in the webbrowser at runtime, it produces this error:
First chance exception at $75F1B9BC. Exception class EOleException with message 'Unspecified error'. Process Project1.exe (3288)
If I remove either part of the code, it works (without the removed code, obviously).
Can anybody help me get both things working at the same time?
Thanks for your time!
Update:
This is now a matter of correctly trapping the new window and keep it in the same browser control. The zooming code in OnDocumentComplete has, as far as I can tell, nothing to do with it. It's the zoom in general. If the WebBrowser control has been zoomed (once is enough), the code in NewWindow3 will fail with "Unspecified error". Resetting the zoom level to 100% doesn't help.
By using the zoom code (ExecWB) something changes "forever" in the WebBrowser, which makes it incompatible with the code in NewWindow3.
Can anybody figure it out?
New code:
procedure TForm1.Button1Click(Sender: TObject);
var
ZoomFac: OLEVariant;
begin
ZoomFac := 140;
WebBrowser1.ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, ZoomFac);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser1.Navigate('http://www.wbm.dk/test.htm');
end;
procedure TForm1.WebBrowser1NewWindow3(ASender: TObject; var ppDisp: IDispatch;
var Cancel: WordBool; dwFlags: Cardinal; const bstrUrlContext,
bstrUrl: WideString);
begin
Cancel := True;
WebBrowser1.Navigate(bstrUrl);
end;
Try clicking the link both before and after clicking Button1. After zooming it fails.
You can set ppDisp to a new instance of IWebBrowser2 in the OnNewWindow2 event e.g:
procedure TForm1.Button1Click(Sender: TObject);
begin
WebBrowser1.Navigate('http://wbm.dk/test.htm');
end;
procedure TForm1.WebBrowser1DocumentComplete(Sender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
ZoomFac: OleVariant;
begin
// the top-level browser
if pDisp = TWebBrowser(Sender).ControlInterface then
begin
ZoomFac := 140;
TWebBrowser(Sender).ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, ZoomFac);
end;
end;
procedure TForm1.WebBrowser1NewWindow2(Sender: TObject;
var ppDisp: IDispatch; var Cancel: WordBool);
var
NewWindow: TForm1;
begin
// ppDisp is nil; this will create a new instance of TForm1:
NewWindow := TForm1.Create(self);
NewWindow.Show;
ppDisp := NewWindow.Webbrowser1.DefaultDispatch;
end;
It is also suggested by Microsoft to set RegisterAsBrowser to true.
You could change this code to open a TWebBrowser in a new tab inside a Page control.
We can not set ppDisp to the current instance of the TWebBrowser - so using this simple code:
ppDisp := WebBrowser1.DefaultDispatch; dose not work.
We need to "recreate" the current/active TWebBrowser, if we want to maintain the UI flow - note that in the following example the TWebBrowser is created on the fly e.g.:
const
CM_WB_DESTROY = WM_USER + 1;
OLECMDID_OPTICAL_ZOOM = 63;
type
TForm1 = class(TForm)
Button1: TButton;
Panel1: TPanel;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
function CreateWebBrowser: TWebBrowser;
procedure WebBrowserDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant);
procedure WebBrowserNewWindow2(Sender: TObject; var ppDisp: IDispatch; var Cancel: WordBool);
procedure CMWebBrowserDestroy(var Message: TMessage); message CM_WB_DESTROY;
public
WebBrowser: TWebBrowser;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser := CreateWebBrowser;
end;
function TForm1.CreateWebBrowser: TWebBrowser;
begin
Result := TWebBrowser.Create(Self);
TWinControl(Result).Parent := Panel1;
Result.Align := alClient;
Result.OnDocumentComplete := WebBrowserDocumentComplete;
Result.OnNewWindow2 := WebBrowserNewWindow2;
Result.RegisterAsBrowser := True;
end;
procedure TForm1.WebBrowserDocumentComplete(Sender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
ZoomFac: OleVariant;
begin
// the top-level browser
if pDisp = TWebBrowser(Sender).ControlInterface then
begin
ZoomFac := 140;
TWebBrowser(Sender).ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, ZoomFac);
end;
end;
procedure TForm1.WebBrowserNewWindow2(Sender: TObject; var ppDisp: IDispatch; var Cancel: WordBool);
var
NewWB: TWebBrowser;
begin
NewWB := CreateWebBrowser;
ppDisp := NewWB.DefaultDispatch;
WebBrowser := NewWB;
// just in case...
TWebBrowser(Sender).Stop;
TWebBrowser(Sender).OnDocumentComplete := nil;
TWebBrowser(Sender).OnNewWindow2 := nil;
// post a delayed message to destory the current TWebBrowser
PostMessage(Self.Handle, CM_WB_DESTROY, Integer(TWebBrowser(Sender)), 0);
end;
procedure TForm1.CMWebBrowserDestroy(var Message: TMessage);
var
Sender: TObject;
begin
Sender := TObject(Message.WParam);
if Assigned(Sender) and (Sender is TWebBrowser) then
TWebBrowser(Sender).Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
WebBrowser.Navigate('http://wbm.dk/test.htm');
end;
I think the problem is that sometimes OnDocumentComplete can fire multiple times on document load (pages with frames).
Here is the way to implement it properly.

How to disable view source option in Chromium Embedded?

Is it possible to disable view source option in Delphi Chromium Embedded ?
I haven't found anything suitable in properties/methods list.
There are no direct settings or events allowing to hide Chromium popup menu items. However you have at least few options how to continue, you can for instance:
1. Tell user that the View source option is forbidden and decline the action
You can decide what action will you allow or decline in the OnMenuAction event handler, where if you assign True to the Result parameter the action is declined. The following code checks that you have performed the view source action and if so, decline the action and show the information message:
type
TCefMenuId = TCefHandlerMenuId;
procedure TForm1.Chromium1MenuAction(Sender: TObject;
const browser: ICefBrowser; menuId: TCefMenuId; out Result: Boolean);
begin
if menuId = MENU_ID_VIEWSOURCE then
begin
Result := True;
ShowMessage('View page source is not allowed!');
end;
end;
2. Fake the menu item to something custom by changing menu item's caption with its action
You can take advantage of the menu item for something else by changing the menu item's caption and executing some custom action. The following sample code shows how to change the view source menu item into the about box menu item:
type
TCefMenuId = TCefHandlerMenuId;
procedure TForm1.Chromium1GetMenuLabel(Sender: TObject;
const browser: ICefBrowser; menuId: TCefMenuId; var caption: ustring;
out Result: Boolean);
begin
if menuId = MENU_ID_VIEWSOURCE then
caption := 'About my application...';
end;
procedure TForm1.Chromium1MenuAction(Sender: TObject;
const browser: ICefBrowser; menuId: TCefMenuId; out Result: Boolean);
begin
if menuId = MENU_ID_VIEWSOURCE then
begin
Result := True;
ShowMessage('About box...!');
end;
end;
3. Create you own custom page (frame) popup menu
You can create your own popup menu, but you need to consider that this menu is quite hardcoded, so you will need to maintain it if you'll need to have it the same with each new version of Delphi Chromium wrapper. Here is the code how to create the page menu without view source menu item:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Menus, cefvcl, ceflib;
type
PCefMenuInfo = PCefHandlerMenuInfo;
type
TForm1 = class(TForm)
Chromium1: TChromium;
procedure FormCreate(Sender: TObject);
procedure Chromium1BeforeMenu(Sender: TObject; const browser: ICefBrowser;
const menuInfo: PCefMenuInfo; out Result: Boolean);
private
PageMenu: TPopupMenu;
procedure OnNavigateBackMenuItemClick(Sender: TObject);
procedure OnNavigateForwardMenuItemClick(Sender: TObject);
procedure OnPrintMenuItemClick(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.OnNavigateBackMenuItemClick(Sender: TObject);
begin
Chromium1.Browser.GoBack;
end;
procedure TForm1.OnNavigateForwardMenuItemClick(Sender: TObject);
begin
Chromium1.Browser.GoForward;
end;
procedure TForm1.OnPrintMenuItemClick(Sender: TObject);
begin
Chromium1.Browser.GetFocusedFrame.Print;
end;
procedure TForm1.Chromium1BeforeMenu(Sender: TObject;
const browser: ICefBrowser; const menuInfo: PCefMenuInfo;
out Result: Boolean);
begin
if menuInfo.typeFlags = MENUTYPE_PAGE then
begin
Result := True;
PageMenu.Items[0].Enabled := browser.CanGoBack;
PageMenu.Items[1].Enabled := browser.CanGoForward;
PageMenu.Popup(menuInfo^.x, menuInfo^.y);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
MenuItem: TMenuItem;
begin
PageMenu := TPopupMenu.Create(Self);
MenuItem := TMenuItem.Create(PageMenu);
MenuItem.Caption := 'Back';
MenuItem.OnClick := OnNavigateBackMenuItemClick;
PageMenu.Items.Add(MenuItem);
MenuItem := TMenuItem.Create(PageMenu);
MenuItem.Caption := 'Forward';
MenuItem.OnClick := OnNavigateForwardMenuItemClick;
PageMenu.Items.Add(MenuItem);
MenuItem := TMenuItem.Create(PageMenu);
MenuItem.Caption := '-';
PageMenu.Items.Add(MenuItem);
MenuItem := TMenuItem.Create(PageMenu);
MenuItem.Caption := 'Print';
MenuItem.OnClick := OnPrintMenuItemClick;
PageMenu.Items.Add(MenuItem);
Chromium1.Load('www.stackoverflow.com');
end;
end.
Footnote
The type definitions used in all code samples are there because I've noticed that some version of Delphi Chromium has wrong event handler definitions.
Probably things changed over years, today a direct method exists:
uses
ceflib;
[..]
implementation
procedure TForm1.Chromium1BeforeContextMenu(Sender: TObject;
const browser: ICefBrowser; const frame: ICefFrame;
const params: ICefContextMenuParams; const model: ICefMenuModel);
begin
//model.Clear;
model.Remove(Integer(MENU_ID_VIEW_SOURCE));
end;
You can use model.Clear if you want to completely get rid of popup menu.

Resources