Trying to get a basic tabbed browser working..

Post Reply
RaelB
Posts: 8
Joined: Sun Feb 12, 2017 11:22 am

Trying to get a basic tabbed browser working..

Post by RaelB » Mon Jul 08, 2019 4:23 am

Hello,
I am trying to get a basic tabbed browser demo working.
I have a TFrame that contains the Chromium/CEFWindowParent controls or just the ChromiumWindow.
However, when I call Chromium.LoadURL / ChromiumWindow.LoadURL nothing happens.
Can you please tell me what I am missing.
I tried to compare with the SimpleBrowser and TabBrowser demos, but could not see what was wrong.
I have attached a demo project.
Using Delphi 10.2.3 (Tokyo)
Thank you
Rael
You do not have the required permissions to view the files attached to this post.

User avatar
salvadordf
Posts: 1950
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: Trying to get a basic tabbed browser working..

Post by salvadordf » Mon Jul 08, 2019 8:28 am

Hi,

The browsers are not initialized because TChromium.CreateBrowser is called inside the TCEFFrame.Create. At that moment the frame is not fully configured.

Create a custom TCEFFrame.CreateBrowser procedure and call it after TCEFFrame is fully configured and visible.

TCEFFrame.CreateBrowser would call TChromium.CreateBrowser like this :

Code: Select all

procedure TCEFFrame.CreateBrowser;
begin
  Chromium1.CreateBrowser(CEFWindowParent1, '');
end;
Modify TForm1.btnAddClick to call TCEFFrame.CreateBrowser after the frame is fully configured like this :

Code: Select all

procedure TForm1.btnAddClick(Sender: TObject);
var
  CEFFrame: TCEFFrame;
  TabSheet: TTabSheet;
begin
  TabSheet := TTabSheet.Create(Self);
  TabSheet.PageControl := PageControl1;
  TabSheet.Caption := 'TabSheet ' + PageControl1.PageCount.ToString;

  CEFFrame := TCEFFrame.Create(Self);
  CEFFrame.Name := 'CEFFrame'+ PageControl1.PageCount.ToString;
  CEFFrame.Parent := TabSheet;
  CEFFrame.Align := alClient;
  CEFFrame.Visible := True;

  PageControl1.ActivePageIndex := PageControl1.PageCount - 1;

  CEFFrame.CreateBrowser; // <---- THIS IS THE NEW PROCEDURE TO CREATE THE BROWSER
end;
That demo also needs this :
  • GlobalCEFApp.GlobalContextInitialized must be TRUE before creating the first browser.
  • Each CEFFrame must wait until the browser is initialized before loading the first URL. Use TChromium.OnAfterCreated
  • All browsers must be closed correctly before closing the main form.
All of those things can be copied from the TabbedBrowser demo.

RaelB
Posts: 8
Joined: Sun Feb 12, 2017 11:22 am

Re: Trying to get a basic tabbed browser working..

Post by RaelB » Mon Jul 08, 2019 5:15 pm

Thanks for the help. It is working correctly now.

I see from the demos, and your comment "All browsers must be closed correctly before closing the main form".

Without adding any further code (to handle closing tabs etc..), I did a simple test with a few tabs and each tab has loaded a different url. Then I close the app and there are no errors.

So, is there a set of steps that I can try to elicit an error when closing the app, if I have not added the specific closing code.
IOW, a way to test that my closing code is correct?

Thanks
Rael

User avatar
salvadordf
Posts: 1950
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: Trying to get a basic tabbed browser working..

Post by salvadordf » Tue Jul 09, 2019 8:51 am

The destruction sequence for each tab in the TabbedBrowser demo is this :
  • RemoveTabBtnClick calls TChromium.CloseBrowser of the selected tab which triggers a TChromium.OnClose event.
  • TChromium.OnClose sends a CEFBROWSER_DESTROYWNDPARENT message to destroy TCEFWindowParent in the main thread which triggers a TChromium.OnBeforeClose event.
  • TChromium.OnBeforeClose sends a CEFBROWSER_DESTROYTAB message to destroy the tab in the main thread.
The code comments with this information is here :
https://github.com/salvadordf/CEF4Delphi/blob/5d36c422ebbae916f69ab82e7852181d82f274f3/demos/Delphi_VCL/TabbedBrowser/uMainForm.pas#L139

Each tab must follow those steps and the main form should only be closed when all the tabs have completed the previous sequence.

It's a complicated sequence because TChromium events are executed in a different thread and the VCL doesn't like to create and destroy controls in different threads. By using messages we ensure that TCEFWindowParent is destroyed in the main application thread.

The "CloseBrowser -> OnClose -> OnBeforeClose" sequence is defined in CEF and it's necessary to destroy all the browsers correctly before GlobalCEFApp is destroyed.

You can test this by creating several browser tabs with complex and large pages. Open a YouTube video, several newspapers pages with plenty of animated ads and then try to close the application.

Notice that the TabbedBrowser has this code when GlobalCEFApp is created :

Code: Select all

GlobalCEFApp.DisableFeatures := 'NetworkService';


Add that code to your browser too or you might get access violation errors.

RaelB
Posts: 8
Joined: Sun Feb 12, 2017 11:22 am

Re: Trying to get a basic tabbed browser working..

Post by RaelB » Wed Jul 10, 2019 2:35 am

Thanks for the further explanation.

I was spending some time looking at the TabbedBrowser demo and also my demo, and then I realised/it appears that the Form OnClose event is triggered simply by freeing TCEFWindowParent (or perhaps soon after). That seems pretty strange to me. Why does that happen? Is it part of CEF or part of CEF4Delphi?

User avatar
salvadordf
Posts: 1950
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: Trying to get a basic tabbed browser working..

Post by salvadordf » Wed Jul 10, 2019 1:15 pm

Chromium is very complex and sometimes it has some weird ways to do accomplish what TWebBrowser does in a simple function.
However, TWebBrowser has a much worse performance and the users often complain about that.

The destruction sequence is one of those weird cases because Chromium needs to close multiple threads and processes.

This is what the CEF sources say about the steps we need to follow to close a browser :
https://bitbucket.org/chromiumembedded/cef/src/45329d05a5e0d96047374decaa6170cdeac9e0d7/include/capi/cef_life_span_handler_capi.h#lines-110

Code: Select all

  ///
  // Called when a browser has recieved a request to close. This may result
  // directly from a call to cef_browser_host_t::*close_browser() or indirectly
  // if the browser is parented to a top-level window created by CEF and the
  // user attempts to close that window (by clicking the 'X', for example). The
  // do_close() function will be called after the JavaScript 'onunload' event
  // has been fired.
  //
  // An application should handle top-level owner window close notifications by
  // calling cef_browser_host_t::try_close_browser() or
  // cef_browser_host_t::CloseBrowser(false (0)) instead of allowing the window
  // to close immediately (see the examples below). This gives CEF an
  // opportunity to process the 'onbeforeunload' event and optionally cancel the
  // close before do_close() is called.
  //
  // When windowed rendering is enabled CEF will internally create a window or
  // view to host the browser. In that case returning false (0) from do_close()
  // will send the standard close notification to the browser's top-level owner
  // window (e.g. WM_CLOSE on Windows, performClose: on OS X, "delete_event" on
  // Linux or cef_window_delegate_t::can_close() callback from Views). If the
  // browser's host window/view has already been destroyed (via view hierarchy
  // tear-down, for example) then do_close() will not be called for that browser
  // since is no longer possible to cancel the close.
  //
  // When windowed rendering is disabled returning false (0) from do_close()
  // will cause the browser object to be destroyed immediately.
  //
  // If the browser's top-level owner window requires a non-standard close
  // notification then send that notification from do_close() and return true
  // (1).
  //
  // The cef_life_span_handler_t::on_before_close() function will be called
  // after do_close() (if do_close() is called) and immediately before the
  // browser object is destroyed. The application should only exit after
  // on_before_close() has been called for all existing browsers.
  //
  // The below examples describe what should happen during window close when the
  // browser is parented to an application-provided top-level window.
  //
  // Example 1: Using cef_browser_host_t::try_close_browser(). This is
  // recommended for clients using standard close handling and windows created
  // on the browser process UI thread. 
  // 1.  User clicks the window close button which sends a close notification to
  //     the application's top-level window.
  // 2.  Application's top-level window receives the close notification and
  //     calls TryCloseBrowser() (which internally calls CloseBrowser(false)).
  //     TryCloseBrowser() returns false so the client cancels the window close.
  // 3.  JavaScript 'onbeforeunload' handler executes and shows the close
  //     confirmation dialog (which can be overridden via
  //     CefJSDialogHandler::OnBeforeUnloadDialog()).
  // 4.  User approves the close. 
  // 5.  JavaScript 'onunload' handler executes. 
  // 6. CEF sends a close notification to the application's top-level window
  //     (because DoClose() returned false by default).
  // 7.  Application's top-level window receives the close notification and
  //     calls TryCloseBrowser(). TryCloseBrowser() returns true so the client
  //     allows the window close.
  // 8.  Application's top-level window is destroyed. 
  // 9.  Application's on_before_close() handler is called and the browser object
  //     is destroyed.
  // 10. Application exits by calling cef_quit_message_loop() if no other
  //     browsers exist.
  //
  // Example 2: Using cef_browser_host_t::CloseBrowser(false (0)) and
  // implementing the do_close() callback. This is recommended for clients using
  // non-standard close handling or windows that were not created on the browser
  // process UI thread. 
  // 1.  User clicks the window close button which sends a close notification to
  //     the application's top-level window.
  // 2.  Application's top-level window receives the close notification and:
  //     A. Calls CefBrowserHost::CloseBrowser(false).
  //     B. Cancels the window close.
  // 3.  JavaScript 'onbeforeunload' handler executes and shows the close
  //     confirmation dialog (which can be overridden via
  //     CefJSDialogHandler::OnBeforeUnloadDialog()).
  // 4.  User approves the close. 
  // 5.  JavaScript 'onunload' handler executes. 
  // 6.  Application's do_close() handler is called. Application will:
  //     A. Set a flag to indicate that the next close attempt will be allowed.
  //     B. Return false.
  // 7.  CEF sends an close notification to the application's top-level window.
  // 8.  Application's top-level window receives the close notification and
  //     allows the window to close based on the flag from #6B.
  // 9.  Application's top-level window is destroyed. 
  // 10. Application's on_before_close() handler is called and the browser object
  //     is destroyed.
  // 11. Application exits by calling cef_quit_message_loop() if no other
  //       browsers exist.
  ///
TabbedBrowser needs to destroy TCEFWindowParent because CEF triggers TChromium.OnBeforeClose only when the "window" (using the Windows API terms) created by CEF to show the browser contents is destroyed.

RaelB
Posts: 8
Joined: Sun Feb 12, 2017 11:22 am

Re: Trying to get a basic tabbed browser working..

Post by RaelB » Wed Jul 10, 2019 7:57 pm

Ok, thanks for the additional info.

I am attempting to create a Tabbed Browser demo that is easier to work with and also simpler to understand. I am using a TFrame and have tried to encapsulate the destruction code within the frame, as per attached demo. It could also allow me to use Chromium Windows in multiple parts of the application, more easily.

Can you take a look? In my tests it was working ok, but perhaps I overlooked something.

Thanks
Rael
You do not have the required permissions to view the files attached to this post.

User avatar
salvadordf
Posts: 1950
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: Trying to get a basic tabbed browser working..

Post by salvadordf » Thu Jul 11, 2019 8:59 am

Just a couple of suggestions :
  • Don't use Application.ProcessMessages. Call TChromium.CloseBrowser and wait until the TChromium.OnBeforeClose event is triggered. Then send a message to the main form to destroy that tab.
  • It's not necessary to free Chromium1. The owner (frame) will destroy it automatically when it's freed.

RaelB
Posts: 8
Joined: Sun Feb 12, 2017 11:22 am

Re: Trying to get a basic tabbed browser working..

Post by RaelB » Fri Jul 12, 2019 1:25 am

salvadordf wrote:
Thu Jul 11, 2019 8:59 am
  • Don't use Application.ProcessMessages. Call TChromium.CloseBrowser and wait until the TChromium.OnBeforeClose event is triggered. Then send a message to the main form to destroy that tab.
May I ask why? Is what I have done not equivalent? and has the advantage that the Main unit code is less cluttered.

i.e. Application.ProcessMessages means the OnBeforeClose event will be triggered and set the FChromiumClosed flag.
Then in the Main unit the Tab is Freed

Code: Select all

PageControl1.Pages[PageControl1.ActivePageIndex].Free;

User avatar
salvadordf
Posts: 1950
Joined: Thu Feb 02, 2017 12:24 pm
Location: Spain
Contact:

Re: Trying to get a basic tabbed browser working..

Post by salvadordf » Fri Jul 12, 2019 8:51 am

Application.ProcessMessages should only be used in cases when you have an absolute certainty that processing all the available Windows messages at that time won't disrupt other features. Even then I would recommend using threads, messages, etc instead of calling Application.ProcessMessages.

There are many issues related to Application.ProcessMessages when you search in Google because it's relatively easy to find third party components that were not designed to work correctly with that function. Among those components are : Any component that embeds a Chromium browser, TWebBrowser, etc.

Read this for more information about it :
https://stackoverflow.com/questions/25181713/i-do-not-understand-what-application-processmessages-in-delphi-is-doing
https://www.thoughtco.com/dark-side-of-application-processmessages-1058203
https://blog.dummzeuch.de/2018/09/29/calling-application-processmessages-in-a-delphi-program/

Post Reply