From fbce2d61de8b769326e94ede96ac97363ef9ea1c Mon Sep 17 00:00:00 2001 From: amaitland Date: Tue, 30 Jan 2018 15:01:38 +1000 Subject: [PATCH] WinForms Example - Update LifeSpanHandler example to host popup in Tab (notify popup of move and resize) --- CefSharp.WinForms.Example/BrowserForm.cs | 25 +- .../BrowserTabUserControl.cs | 2 +- .../CefSharp.WinForms.Example.csproj | 1 + .../Handlers/LifeSpanHandler.cs | 83 +++-- .../Helper/PopupAsChildHelper.cs | 311 ++++++++++++++++++ 5 files changed, 375 insertions(+), 47 deletions(-) create mode 100644 CefSharp.WinForms.Example/Helper/PopupAsChildHelper.cs diff --git a/CefSharp.WinForms.Example/BrowserForm.cs b/CefSharp.WinForms.Example/BrowserForm.cs index f155ca48ab..ffbffe1f52 100644 --- a/CefSharp.WinForms.Example/BrowserForm.cs +++ b/CefSharp.WinForms.Example/BrowserForm.cs @@ -41,6 +41,29 @@ private void BrowserFormLoad(object sender, EventArgs e) AddTab(CefExample.DefaultUrl); } + /// + /// Used to add a Popup browser as a Tab + /// + /// + public void AddTab(Control browserHostControl, string url) + { + browserTabControl.SuspendLayout(); + + var tabPage = new TabPage(url) + { + Dock = DockStyle.Fill + }; + + tabPage.Controls.Add(browserHostControl); + + browserTabControl.TabPages.Add(tabPage); + + //Make newly created tab active + browserTabControl.SelectedTab = tabPage; + + browserTabControl.ResumeLayout(true); + } + private void AddTab(string url, int? insertIndex = null) { browserTabControl.SuspendLayout(); @@ -117,7 +140,7 @@ private BrowserTabUserControl GetCurrentTabControl() } var tabPage = browserTabControl.Controls[browserTabControl.SelectedIndex]; - var control = (BrowserTabUserControl)tabPage.Controls[0]; + var control = tabPage.Controls[0] as BrowserTabUserControl; return control; } diff --git a/CefSharp.WinForms.Example/BrowserTabUserControl.cs b/CefSharp.WinForms.Example/BrowserTabUserControl.cs index 89f7064d6a..b01e4993db 100644 --- a/CefSharp.WinForms.Example/BrowserTabUserControl.cs +++ b/CefSharp.WinForms.Example/BrowserTabUserControl.cs @@ -44,7 +44,7 @@ public BrowserTabUserControl(Action openNewTab, string url, bool m { browser.KeyboardHandler = new KeyboardHandler(); } - browser.LifeSpanHandler = new LifeSpanHandler(); + //browser.LifeSpanHandler = new LifeSpanHandler(); browser.LoadingStateChanged += OnBrowserLoadingStateChanged; browser.ConsoleMessage += OnBrowserConsoleMessage; browser.TitleChanged += OnBrowserTitleChanged; diff --git a/CefSharp.WinForms.Example/CefSharp.WinForms.Example.csproj b/CefSharp.WinForms.Example/CefSharp.WinForms.Example.csproj index 7b5b5756c6..ecd64acda3 100644 --- a/CefSharp.WinForms.Example/CefSharp.WinForms.Example.csproj +++ b/CefSharp.WinForms.Example/CefSharp.WinForms.Example.csproj @@ -90,6 +90,7 @@ + Form diff --git a/CefSharp.WinForms.Example/Handlers/LifeSpanHandler.cs b/CefSharp.WinForms.Example/Handlers/LifeSpanHandler.cs index 9d323c4749..2ebbbb1851 100644 --- a/CefSharp.WinForms.Example/Handlers/LifeSpanHandler.cs +++ b/CefSharp.WinForms.Example/Handlers/LifeSpanHandler.cs @@ -3,77 +3,61 @@ // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. using System; +using System.Collections.Generic; using System.Windows.Forms; +using CefSharp.WinForms.Example.Helper; namespace CefSharp.WinForms.Example.Handlers { public class LifeSpanHandler : ILifeSpanHandler { + private Dictionary popupasChildHelpers = new Dictionary(); + bool ILifeSpanHandler.OnBeforePopup(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) { - // Set newBrowser to null unless your attempting to host the popup in a new instance of ChromiumWebBrowser - // This should only be used in WPF/OffScreen + //Set newBrowser to null unless your attempting to host the popup in a new instance of ChromiumWebBrowser newBrowser = null; - return false; //Return true to cancel the popup creation - - // Hosting the popup in your own control/window - // Use IWindowInfo.SetAsChild to specify the parent handle - // NOTE: Window resize not yet handled - you need to get the - // IBrowserHost from the newly created IBrowser instance that represents the popup - // Then subscribe to window resize notifications and call NotifyMoveOrResizeStarted(). - // Also any chances in width/height you need to call SetWindowPos on the browsers HWND - // Use NativeMethodWrapper.SetWindowPosition to achieve this - you can get the HWND using - // IBrowserHost method - - //var chromiumWebBrowser = (ChromiumWebBrowser)browserControl; - - //var windowX = windowInfo.X; - //var windowY = windowInfo.Y; - //var windowWidth = (windowInfo.Width == int.MinValue) ? 600 : windowInfo.Width; - //var windowHeight = (windowInfo.Height == int.MinValue) ? 800 : windowInfo.Height; - - //chromiumWebBrowser.Invoke(new Action(() => - //{ - // var owner = chromiumWebBrowser.FindForm(); - - // var popup = new Form - // { - // Left = windowX, - // Top = windowY, - // Width = windowWidth, - // Height = windowHeight, - // Text = targetFrameName - // }; + //Use IWindowInfo.SetAsChild to specify the parent handle + //NOTE: user PopupAsChildHelper to handle with Form move and Control resize + var chromiumWebBrowser = (ChromiumWebBrowser)browserControl; - // popup.CreateControl(); - - // owner.AddOwnedForm(popup); + chromiumWebBrowser.Invoke(new Action(() => + { + var owner = chromiumWebBrowser.FindForm() as BrowserForm; - // var control = new Control(); - // control.Dock = DockStyle.Fill; - // control.CreateControl(); + if(owner != null) + { + var control = new Control(); + control.Dock = DockStyle.Fill; + control.CreateControl(); - // popup.Controls.Add(control); + owner.AddTab(control, targetUrl); - // popup.Show(); + var rect = control.ClientRectangle; - // var rect = control.ClientRectangle; + windowInfo.SetAsChild(control.Handle, rect.Left, rect.Top, rect.Right, rect.Bottom); + } + })); - // windowInfo.SetAsChild(control.Handle, rect.Left, rect.Top, rect.Right, rect.Bottom); - //})); + return false; } void ILifeSpanHandler.OnAfterCreated(IWebBrowser browserControl, IBrowser browser) { + if (browser.IsPopup) + { + var interceptor = new PopupAsChildHelper(browser); + popupasChildHelpers.Add(browser.Identifier, interceptor); + } } bool ILifeSpanHandler.DoClose(IWebBrowser browserControl, IBrowser browser) { //We need to allow popups to close //If the browser has been disposed then we'll just let the default behaviour take place - if(browser.IsDisposed || browser.IsPopup) + if (browser.IsDisposed || browser.IsPopup) { return false; } @@ -84,9 +68,18 @@ bool ILifeSpanHandler.DoClose(IWebBrowser browserControl, IBrowser browser) return true; } - public void OnBeforeClose(IWebBrowser browserControl, IBrowser browser) + void ILifeSpanHandler.OnBeforeClose(IWebBrowser browserControl, IBrowser browser) { + if (!browser.IsDisposed && browser.IsPopup) + { + var interceptor = popupasChildHelpers[browser.Identifier]; + if (interceptor != null) + { + popupasChildHelpers[browser.Identifier] = null; + interceptor.Dispose(); + } + } } } } diff --git a/CefSharp.WinForms.Example/Helper/PopupAsChildHelper.cs b/CefSharp.WinForms.Example/Helper/PopupAsChildHelper.cs new file mode 100644 index 0000000000..eeda9e7774 --- /dev/null +++ b/CefSharp.WinForms.Example/Helper/PopupAsChildHelper.cs @@ -0,0 +1,311 @@ +// Copyright © 2010-2017 The CefSharp Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using CefSharp.Internals; +using CefSharp.WinForms.Internals; + +namespace CefSharp.WinForms.Example.Helper +{ + /// + /// When using ILifeSpanHandler.OnBeforePopup and calling IWindowInfo.SetAsChild + /// it's important to track form movement and control size change. This NativeWindow + /// does that for you. With newer version it's possible this is only required when using + /// MultiThreadedMessageLoop = true. + /// TODO: Currently there's a 1:1 mapping between helpers and browsers, it should be possible + /// to have one helper per form that notifies all browser instances + /// + public class PopupAsChildHelper : NativeWindow, IDisposable + { + /// + /// Keep track of whether a move is in progress. + /// + private bool isMoving; + + /// + /// Gets or sets the parent control used to host the popup child handle + /// + private Control parentControl; + + /// + /// Gets or sets the parent form. + /// + /// The parent form. + private Form parentForm; + + /// + /// The IBrowser that references the Popup + /// + private IBrowser browser; + + /// + /// The browsers window handle(hwnd) + /// + private IntPtr browserHandle; + + /// + /// Initializes a new instance of the class. + /// + /// The browser. + public PopupAsChildHelper(IBrowser browser) + { + if (browser == null) + { + throw new ArgumentNullException("browser"); + } + + this.browser = browser; + + //From the browser we grab the window handle (hwnd) + this.browserHandle = browser.GetHost().GetWindowHandle(); + + //WinForms will kindly lookup the child control from it's handle + this.parentControl = Control.FromChildHandle(browserHandle); + + if(this.parentControl == null) + { + throw new Exception("Unable to locate parentControl from the browser handle."); + } + + // Get notified if our control window parent changes: + this.parentControl.ParentChanged += ParentFormChanged; + //Get notified of size changes + this.parentControl.SizeChanged += ParentControlSizeChanged; + + // Find the browser form to subclass to monitor WM_MOVE/WM_MOVING + RefindParentForm(); + } + + /// + /// Call to force refinding of the parent Form. + /// (i.e. top level window that owns the ChromiumWebBrowserControl) + /// + public void RefindParentForm() + { + parentControl.InvokeOnUiThreadIfRequired(() => + { + ParentFormChanged(parentControl, null); + }); + } + + /// + /// Adjust the form to listen to if the ChromiumWebBrowserControl's parent changes. + /// + /// The ChromiumWebBrowser whose parent has changed. + /// The instance containing the event data. + private void ParentFormChanged(object sender, EventArgs e) + { + var control = (Control)sender; + var oldForm = parentForm; + var newForm = control.FindForm(); + + if (oldForm == null || newForm == null || oldForm.Handle != newForm.Handle) + { + if (Handle != IntPtr.Zero) + { + ReleaseHandle(); + } + if (oldForm != null) + { + oldForm.HandleCreated -= OnHandleCreated; + oldForm.HandleDestroyed -= OnHandleDestroyed; + } + parentForm = newForm; + if (newForm != null) + { + newForm.HandleCreated += OnHandleCreated; + newForm.HandleDestroyed += OnHandleDestroyed; + // If newForm's Handle has been created already, + // our event listener won't be called, so call it now. + if (newForm.IsHandleCreated) + { + OnHandleCreated(newForm, null); + } + } + } + } + + private void ParentControlSizeChanged(object sender, EventArgs e) + { + var bounds = parentControl.Bounds; + if(browserHandle != IntPtr.Zero) + { + NativeMethodWrapper.SetWindowPosition(browserHandle, bounds.X, bounds.Y, bounds.Width, bounds.Height); + } + } + + /// + /// Handles the event. + /// + /// The sender. + /// The instance containing the event data. + private void OnHandleCreated(object sender, EventArgs e) + { + AssignHandle(((Form)sender).Handle); + } + + /// + /// Handles the event. + /// + /// The sender. + /// The instance containing the event data. + private void OnHandleDestroyed(object sender, EventArgs e) + { + ReleaseHandle(); + } + + /// + /// Invokes the default window procedure associated with this window. + /// + /// A that is associated with the current Windows message. + protected override void WndProc(ref Message m) + { + var isMovingMessage = false; + + // Negative initial values keeps the compiler quiet and to + // ensure we have actual window movement to notify CEF about. + const int invalidMoveCoordinate = -1; + var x = invalidMoveCoordinate; + var y = invalidMoveCoordinate; + + // Listen for operating system messages + switch (m.Msg) + { + case 0x216: //WM_MOVING + { + var movingRectangle = (Rectangle)Marshal.PtrToStructure(m.LParam, typeof(Rectangle)); + x = movingRectangle.Left; + y = movingRectangle.Top; + isMovingMessage = true; + break; + } + case 0x3: //WM_MOVE + { + // Convert IntPtr into 32bit int safely without + // exceptions: + int dwLParam = CastToInt32(m.LParam); + + // Extract coordinates from lo/hi word: + x = dwLParam & 0xffff; + y = (dwLParam >> 16) & 0xffff; + + isMovingMessage = true; + break; + } + } + + // Only notify about movement if: + // * ParentControl Handle Created + // NOTE: This is checked for paranoia. + // This WndProc can't be called unless ParentForm has + // its handle created, but that doesn't necessarily mean + // Browser has had its handle created. + // WinForm controls don't usually get eagerly created Handles + // in their constructors. + // * ParentForm Actually moved + // * Not currently moving (on the UI thread only of course) + // * The current WindowState is Normal. + // This check is to simplify the effort here. + // Other logic already handles the maximize/minimize + // cases just fine. + // You might consider checking Control.Visible and + // not notifying our browser control if the browser control isn't visible. + // However, if you do that, the non-Visible CEF tab will still + // have any SELECT drop downs rendering where they shouldn't. + if (isMovingMessage + && parentControl.IsHandleCreated + && parentForm.WindowState == FormWindowState.Normal + && (parentForm.Left != x || parentForm.Top != y) + && !isMoving) + { + // parentForm.Left & .Right are negative when the window + // is transitioning from maximized to normal. + // If we are transitioning, the form will also receive + // a WM_SIZE which can deal with the move/size combo itself. + if (parentForm.Left >= 0 && parentForm.Right >= 0) + { + OnMoving(); + } + } + + DefWndProc(ref m); + } + + /// + /// Called when the window is moving. + /// + protected virtual void OnMoving() + { + isMoving = true; + + if (!browser.IsDisposed) + { + browser.GetHost().NotifyMoveOrResizeStarted(); + } + + isMoving = false; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (parentForm != null) + { + parentForm.HandleCreated -= OnHandleCreated; + parentForm.HandleDestroyed -= OnHandleDestroyed; + parentForm = null; + } + + // Unmanaged resource, but release here anyway. + // NativeWindow has its own finalization logic + // that should be run if this instance isn't disposed + // properly before arriving at the finalization thread. + // See: http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/NativeWindow.cs,147 + // for the gruesome details. + if (Handle != IntPtr.Zero) + { + ReleaseHandle(); + } + + if (parentControl != null) + { + parentControl.ParentChanged -= ParentFormChanged; + parentControl.SizeChanged -= ParentControlSizeChanged; + parentControl = null; + } + } + } + + /// + /// When overridden in a derived class, manages an unhandled thread exception. + /// + /// An that specifies the unhandled thread exception. + protected override void OnThreadException(Exception e) + { + // TODO: Do something more interesting here, logging, whatever, something. + base.OnThreadException(e); + } + + private static int CastToInt32(IntPtr intPtr) + { + return unchecked((int)intPtr.ToInt64()); + } + } +}