You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

598 lines
24 KiB

  1. // Copyright © 2010 The CefSharp Authors. All rights reserved.
  2. //
  3. // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Runtime.InteropServices;
  8. using System.Threading.Tasks;
  9. using System.Windows.Forms;
  10. using CefSharp.Example;
  11. using CefSharp.Example.Handlers;
  12. using CefSharp.Example.JavascriptBinding;
  13. using CefSharp.WinForms.Example.Handlers;
  14. using CefSharp.WinForms.Experimental;
  15. using CefSharp.WinForms.Handler;
  16. using CefSharp.WinForms.Host;
  17. namespace CefSharp.WinForms.Example
  18. {
  19. public partial class BrowserTabUserControl : UserControl
  20. {
  21. public IWinFormsChromiumWebBrowser Browser { get; private set; }
  22. private ChromiumWidgetNativeWindow messageInterceptor;
  23. private bool multiThreadedMessageLoopEnabled;
  24. public BrowserTabUserControl(ChromiumHostControl chromiumHostControl)
  25. {
  26. InitializeComponent();
  27. Browser = chromiumHostControl;
  28. browserPanel.Controls.Add(chromiumHostControl);
  29. chromiumHostControl.LoadingStateChanged += OnBrowserLoadingStateChanged;
  30. chromiumHostControl.ConsoleMessage += OnBrowserConsoleMessage;
  31. chromiumHostControl.TitleChanged += OnBrowserTitleChanged;
  32. chromiumHostControl.AddressChanged += OnBrowserAddressChanged;
  33. chromiumHostControl.StatusMessage += OnBrowserStatusMessage;
  34. chromiumHostControl.IsBrowserInitializedChanged += OnIsBrowserInitializedChanged;
  35. chromiumHostControl.LoadError += OnLoadError;
  36. }
  37. public BrowserTabUserControl(Action<string, int?> openNewTab, string url, bool multiThreadedMessageLoopEnabled)
  38. {
  39. InitializeComponent();
  40. var browser = new ChromiumWebBrowser(url)
  41. {
  42. Dock = DockStyle.Fill
  43. };
  44. browserPanel.Controls.Add(browser);
  45. Browser = browser;
  46. browser.MenuHandler = new MenuHandler();
  47. browser.RequestHandler = new WinFormsRequestHandler(openNewTab);
  48. browser.JsDialogHandler = new JsDialogHandler();
  49. browser.DownloadHandler = Fluent.DownloadHandler.AskUser();
  50. browser.AudioHandler = new CefSharp.Handler.AudioHandler();
  51. browser.FrameHandler = new CefSharp.Handler.FrameHandler();
  52. browser.PermissionHandler = new ExamplePermissionHandler();
  53. if (multiThreadedMessageLoopEnabled)
  54. {
  55. browser.KeyboardHandler = new KeyboardHandler();
  56. }
  57. //The CefSharp.WinForms.Handler.LifeSpanHandler implementation
  58. //allows for Popups to be hosted in Controls/Tabs
  59. //This example also demonstrates docking DevTools in a SplitPanel
  60. browser.LifeSpanHandler = CefSharp.WinForms.Handler.LifeSpanHandler
  61. .Create()
  62. .OnBeforePopupCreated((chromiumWebBrowser, b, frame, targetUrl, targetFrameName, targetDisposition, userGesture, browserSettings) =>
  63. {
  64. //Can cancel opening popup based on Url if required.
  65. if(targetUrl?.StartsWith(CefExample.BaseUrl + "/cancelme.html") == true)
  66. {
  67. return PopupCreation.Cancel;
  68. }
  69. return PopupCreation.Continue;
  70. })
  71. .OnPopupCreated((ctrl, targetUrl) =>
  72. {
  73. //Don't try using ctrl.FindForm() here as
  74. //the control hasn't been attached to a parent yet.
  75. if (FindForm() is BrowserForm owner)
  76. {
  77. owner.AddTab(ctrl, targetUrl);
  78. }
  79. })
  80. .OnPopupDestroyed((ctrl, popupBrowser) =>
  81. {
  82. //If we docked DevTools (hosted it ourselves rather than the default popup)
  83. //Used when the BrowserTabUserControl.ShowDevToolsDocked method is called
  84. if (popupBrowser.MainFrame.Url.Equals("devtools://devtools/devtools_app.html"))
  85. {
  86. //Dispose of the parent control we used to host DevTools, this will release the DevTools window handle
  87. //and the ILifeSpanHandler.OnBeforeClose() will be call after.
  88. ctrl.Dispose();
  89. }
  90. else
  91. {
  92. //If browser is disposed or the handle has been released then we don't
  93. //need to remove the tab in this example. The user likely used the
  94. // File -> Close Tab menu option which also calls BrowserForm.RemoveTab
  95. if (!ctrl.IsDisposed && ctrl.IsHandleCreated)
  96. {
  97. if (ctrl.FindForm() is BrowserForm owner)
  98. {
  99. owner.RemoveTab(ctrl);
  100. }
  101. ctrl.Dispose();
  102. }
  103. }
  104. })
  105. .OnPopupBrowserCreated((ctrl, popupBrowser) =>
  106. {
  107. //The host control maybe null if the popup was hosted in a native Window e.g. Devtools by default
  108. if(ctrl == null)
  109. {
  110. return;
  111. }
  112. //You can access all the core browser functionality via IBrowser
  113. //frames, browwser host, etc.
  114. var isPopup = popupBrowser.IsPopup;
  115. })
  116. .Build();
  117. browser.LoadingStateChanged += OnBrowserLoadingStateChanged;
  118. browser.ConsoleMessage += OnBrowserConsoleMessage;
  119. browser.TitleChanged += OnBrowserTitleChanged;
  120. browser.AddressChanged += OnBrowserAddressChanged;
  121. browser.StatusMessage += OnBrowserStatusMessage;
  122. browser.IsBrowserInitializedChanged += OnIsBrowserInitializedChanged;
  123. browser.LoadError += OnLoadError;
  124. #if NETCOREAPP
  125. browser.JavascriptObjectRepository.Register("boundAsync", new AsyncBoundObject(), options: BindingOptions.DefaultBinder);
  126. #else
  127. browser.JavascriptObjectRepository.Register("bound", new BoundObject(), isAsync: false, options: BindingOptions.DefaultBinder);
  128. browser.JavascriptObjectRepository.Register("boundAsync", new AsyncBoundObject(), isAsync: true, options: BindingOptions.DefaultBinder);
  129. #endif
  130. //If you call CefSharp.BindObjectAsync in javascript and pass in the name of an object which is not yet
  131. //bound, then ResolveObject will be called, you can then register it
  132. browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
  133. {
  134. var repo = e.ObjectRepository;
  135. if (e.ObjectName == "boundAsync2")
  136. {
  137. #if NETCOREAPP
  138. repo.Register("boundAsync2", new AsyncBoundObject(), options: BindingOptions.DefaultBinder);
  139. #else
  140. repo.Register("boundAsync2", new AsyncBoundObject(), isAsync: true, options: BindingOptions.DefaultBinder);
  141. #endif
  142. }
  143. };
  144. browser.RenderProcessMessageHandler = new RenderProcessMessageHandler();
  145. browser.DisplayHandler = new WinFormsDisplayHandler();
  146. //browser.MouseDown += OnBrowserMouseClick;
  147. this.multiThreadedMessageLoopEnabled = multiThreadedMessageLoopEnabled;
  148. var eventObject = new ScriptedMethodsBoundObject();
  149. eventObject.EventArrived += OnJavascriptEventArrived;
  150. // Use the default of camelCaseJavascriptNames
  151. // .Net methods starting with a capitol will be translated to starting with a lower case letter when called from js
  152. #if !NETCOREAPP
  153. browser.JavascriptObjectRepository.Register("boundEvent", eventObject, isAsync: false, options: BindingOptions.DefaultBinder);
  154. #endif
  155. CefExample.RegisterTestResources(browser);
  156. var version = string.Format("Chromium: {0}, CEF: {1}, CefSharp: {2}", Cef.ChromiumVersion, Cef.CefVersion, Cef.CefSharpVersion);
  157. //Set label directly, don't use DisplayOutput as call would be a NOOP (no valid handle yet).
  158. outputLabel.Text = version;
  159. }
  160. /// <summary>
  161. /// Clean up any resources being used.
  162. /// </summary>
  163. /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
  164. protected override void Dispose(bool disposing)
  165. {
  166. if (disposing)
  167. {
  168. if (components != null)
  169. {
  170. components.Dispose();
  171. components = null;
  172. }
  173. if (messageInterceptor != null)
  174. {
  175. messageInterceptor.ReleaseHandle();
  176. messageInterceptor = null;
  177. }
  178. }
  179. base.Dispose(disposing);
  180. }
  181. private void OnBrowserMouseClick(object sender, MouseEventArgs e)
  182. {
  183. MessageBox.Show("Mouse Clicked" + e.X + ";" + e.Y + ";" + e.Button);
  184. }
  185. private void OnLoadError(object sender, LoadErrorEventArgs args)
  186. {
  187. //Aborted is generally safe to ignore
  188. //Actions like starting a download will trigger an Aborted error
  189. //which doesn't require any user action.
  190. if(args.ErrorCode == CefErrorCode.Aborted)
  191. {
  192. return;
  193. }
  194. //Don't display an error for external protocols such as mailto which
  195. //we might want to open in the default viewer
  196. if (args.ErrorCode == CefErrorCode.UnknownUrlScheme && args.Frame.Url.StartsWith("mailto"))
  197. {
  198. return;
  199. }
  200. var errorHtml = string.Format("<html><body><h2>Failed to load URL {0} with error {1} ({2}).</h2></body></html>",
  201. args.FailedUrl, args.ErrorText, args.ErrorCode);
  202. _ = args.Browser.SetMainFrameDocumentContentAsync(errorHtml);
  203. //AddressChanged isn't called for failed Urls so we need to manually update the Url TextBox
  204. this.InvokeOnUiThreadIfRequired(() => urlTextBox.Text = args.FailedUrl);
  205. }
  206. private void OnBrowserConsoleMessage(object sender, ConsoleMessageEventArgs args)
  207. {
  208. DisplayOutput(string.Format("Line: {0}, Source: {1}, Message: {2}", args.Line, args.Source, args.Message));
  209. }
  210. private void OnBrowserStatusMessage(object sender, StatusMessageEventArgs args)
  211. {
  212. this.InvokeOnUiThreadIfRequired(() => statusLabel.Text = args.Value);
  213. }
  214. private void OnBrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs args)
  215. {
  216. SetCanGoBack(args.CanGoBack);
  217. SetCanGoForward(args.CanGoForward);
  218. this.InvokeOnUiThreadIfRequired(() => SetIsLoading(args.IsLoading));
  219. }
  220. private void OnBrowserTitleChanged(object sender, TitleChangedEventArgs args)
  221. {
  222. this.InvokeOnUiThreadIfRequired(() => Parent.Text = args.Title);
  223. }
  224. private void OnBrowserAddressChanged(object sender, AddressChangedEventArgs args)
  225. {
  226. this.InvokeOnUiThreadIfRequired(() => urlTextBox.Text = args.Address);
  227. }
  228. private static void OnJavascriptEventArrived(string eventName, object eventData)
  229. {
  230. switch (eventName)
  231. {
  232. case "click":
  233. {
  234. var message = eventData.ToString();
  235. var dataDictionary = eventData as Dictionary<string, object>;
  236. if (dataDictionary != null)
  237. {
  238. var result = string.Join(", ", dataDictionary.Select(pair => pair.Key + "=" + pair.Value));
  239. message = "event data: " + result;
  240. }
  241. MessageBox.Show(message, "Javascript event arrived", MessageBoxButtons.OK, MessageBoxIcon.Information);
  242. break;
  243. }
  244. }
  245. }
  246. private void SetCanGoBack(bool canGoBack)
  247. {
  248. this.InvokeOnUiThreadIfRequired(() => backButton.Enabled = canGoBack);
  249. }
  250. private void SetCanGoForward(bool canGoForward)
  251. {
  252. this.InvokeOnUiThreadIfRequired(() => forwardButton.Enabled = canGoForward);
  253. }
  254. private void SetIsLoading(bool isLoading)
  255. {
  256. goButton.Text = isLoading ?
  257. "Stop" :
  258. "Go";
  259. goButton.Image = isLoading ?
  260. Properties.Resources.nav_plain_red :
  261. Properties.Resources.nav_plain_green;
  262. HandleToolStripLayout();
  263. }
  264. [return: MarshalAs(UnmanagedType.Bool)]
  265. [DllImport("user32.dll", SetLastError = true)]
  266. private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
  267. private void OnIsBrowserInitializedChanged(object sender, EventArgs e)
  268. {
  269. var requestContext = Browser.GetRequestContext();
  270. string errorMessage;
  271. // Browser must be initialized before getting/setting preferences
  272. var success = requestContext.SetPreference("enable_do_not_track", true, out errorMessage);
  273. if (!success)
  274. {
  275. this.InvokeOnUiThreadIfRequired(() => MessageBox.Show("Unable to set preference enable_do_not_track errorMessage: " + errorMessage));
  276. }
  277. //Example of disable spellchecking
  278. //success = requestContext.SetPreference("browser.enable_spellchecking", false, out errorMessage);
  279. var preferences = requestContext.GetAllPreferences(true);
  280. var doNotTrack = (bool)preferences["enable_do_not_track"];
  281. //Use this to check that settings preferences are working in your code
  282. //success = requestContext.SetPreference("webkit.webprefs.minimum_font_size", 24, out errorMessage);
  283. //If we're using CefSetting.MultiThreadedMessageLoop (the default) then to hook the message pump,
  284. // which running in a different thread we have to use a NativeWindow
  285. if (multiThreadedMessageLoopEnabled)
  286. {
  287. SetupMessageInterceptor();
  288. }
  289. }
  290. /// <summary>
  291. /// The ChromiumWebBrowserControl does not fire MouseEnter/Move/Leave events, because Chromium handles these.
  292. /// This method provides a demo of hooking the Chrome_RenderWidgetHostHWND handle to receive low level messages.
  293. /// You can likely hook other window messages using this technique, drag/drog etc
  294. /// </summary>
  295. private void SetupMessageInterceptor()
  296. {
  297. if (messageInterceptor != null)
  298. {
  299. messageInterceptor.ReleaseHandle();
  300. messageInterceptor = null;
  301. }
  302. Task.Run(async () =>
  303. {
  304. try
  305. {
  306. while (true)
  307. {
  308. IntPtr chromeWidgetHostHandle;
  309. if (ChromiumRenderWidgetHandleFinder.TryFindHandle(Browser.BrowserCore, out chromeWidgetHostHandle))
  310. {
  311. messageInterceptor = new ChromiumWidgetNativeWindow((Control)Browser, chromeWidgetHostHandle);
  312. messageInterceptor.OnWndProc(message =>
  313. {
  314. const int WM_MOUSEACTIVATE = 0x0021;
  315. const int WM_NCLBUTTONDOWN = 0x00A1;
  316. const int WM_DESTROY = 0x0002;
  317. // Render process switch happened, need to find the new handle
  318. if (message.Msg == WM_DESTROY)
  319. {
  320. SetupMessageInterceptor();
  321. return false;
  322. }
  323. if (message.Msg == WM_MOUSEACTIVATE)
  324. {
  325. // The default processing of WM_MOUSEACTIVATE results in MA_NOACTIVATE,
  326. // and the subsequent mouse click is eaten by Chrome.
  327. // This means any .NET ToolStrip or ContextMenuStrip does not get closed.
  328. // By posting a WM_NCLBUTTONDOWN message to a harmless co-ordinate of the
  329. // top-level window, we rely on the ToolStripManager's message handling
  330. // to close any open dropdowns:
  331. // http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/ToolStripManager.cs,1249
  332. var topLevelWindowHandle = message.WParam;
  333. PostMessage(topLevelWindowHandle, WM_NCLBUTTONDOWN, IntPtr.Zero, IntPtr.Zero);
  334. }
  335. //Forward mouse button down message to browser control
  336. //else if(message.Msg == WM_LBUTTONDOWN)
  337. //{
  338. // PostMessage(browserHandle, WM_LBUTTONDOWN, message.WParam, message.LParam);
  339. //}
  340. // The ChromiumWebBrowserControl does not fire MouseEnter/Move/Leave events, because Chromium handles these.
  341. // However we can hook into Chromium's messaging window to receive the events.
  342. //
  343. //const int WM_MOUSEMOVE = 0x0200;
  344. //const int WM_MOUSELEAVE = 0x02A3;
  345. //
  346. //switch (message.Msg) {
  347. // case WM_MOUSEMOVE:
  348. // Console.WriteLine("WM_MOUSEMOVE");
  349. // break;
  350. // case WM_MOUSELEAVE:
  351. // Console.WriteLine("WM_MOUSELEAVE");
  352. // break;
  353. //}
  354. return false;
  355. });
  356. break;
  357. }
  358. else
  359. {
  360. // Chrome hasn't yet set up its message-loop window.
  361. await Task.Delay(10);
  362. }
  363. }
  364. }
  365. catch
  366. {
  367. // Errors are likely to occur if browser is disposed, and no good way to check from another thread
  368. }
  369. });
  370. }
  371. private void DisplayOutput(string output)
  372. {
  373. outputLabel.InvokeOnUiThreadIfRequired(() => outputLabel.Text = output);
  374. }
  375. private void HandleToolStripLayout(object sender, LayoutEventArgs e)
  376. {
  377. HandleToolStripLayout();
  378. }
  379. private void HandleToolStripLayout()
  380. {
  381. var width = toolStrip1.Width;
  382. foreach (ToolStripItem item in toolStrip1.Items)
  383. {
  384. if (item != urlTextBox)
  385. {
  386. width -= item.Width - item.Margin.Horizontal;
  387. }
  388. }
  389. urlTextBox.Width = Math.Max(100, width - urlTextBox.Margin.Horizontal - goButton.Width);
  390. }
  391. private void GoButtonClick(object sender, EventArgs e)
  392. {
  393. LoadUrl(urlTextBox.Text);
  394. }
  395. private void BackButtonClick(object sender, EventArgs e)
  396. {
  397. Browser.Back();
  398. }
  399. private void ForwardButtonClick(object sender, EventArgs e)
  400. {
  401. Browser.Forward();
  402. }
  403. private void UrlTextBoxKeyUp(object sender, KeyEventArgs e)
  404. {
  405. if (e.KeyCode != Keys.Enter)
  406. {
  407. return;
  408. }
  409. LoadUrl(urlTextBox.Text);
  410. }
  411. private void LoadUrl(string url)
  412. {
  413. if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute))
  414. {
  415. Browser.LoadUrl(url);
  416. }
  417. else
  418. {
  419. var searchUrl = "https://www.google.com/search?q=" + Uri.EscapeDataString(url);
  420. Browser.LoadUrl(searchUrl);
  421. }
  422. }
  423. public async Task HideScrollbarsAsync()
  424. {
  425. var devTools = Browser.GetDevToolsClient();
  426. await devTools.Emulation.SetScrollbarsHiddenAsync(true);
  427. }
  428. public async Task CopySourceToClipBoardAsync()
  429. {
  430. var htmlSource = await Browser.GetSourceAsync();
  431. Clipboard.SetText(htmlSource);
  432. DisplayOutput("HTML Source copied to clipboard");
  433. }
  434. private void ToggleBottomToolStrip()
  435. {
  436. if (toolStrip2.Visible)
  437. {
  438. Browser.StopFinding(true);
  439. toolStrip2.Visible = false;
  440. }
  441. else
  442. {
  443. toolStrip2.Visible = true;
  444. findTextBox.Focus();
  445. }
  446. }
  447. private void FindNextButtonClick(object sender, EventArgs e)
  448. {
  449. Find(true);
  450. }
  451. private void FindPreviousButtonClick(object sender, EventArgs e)
  452. {
  453. Find(false);
  454. }
  455. private void Find(bool next)
  456. {
  457. if (!string.IsNullOrEmpty(findTextBox.Text))
  458. {
  459. Browser.Find(findTextBox.Text, next, false, false);
  460. }
  461. }
  462. private void FindTextBoxKeyDown(object sender, KeyEventArgs e)
  463. {
  464. if (e.KeyCode != Keys.Enter)
  465. {
  466. return;
  467. }
  468. Find(true);
  469. }
  470. public void ShowFind()
  471. {
  472. ToggleBottomToolStrip();
  473. }
  474. private void FindCloseButtonClick(object sender, EventArgs e)
  475. {
  476. ToggleBottomToolStrip();
  477. }
  478. //Example of DevTools docked within the existing UserControl,
  479. //in this example it's hosted in a Panel with a SplitContainer
  480. public void ShowDevToolsDocked()
  481. {
  482. if (browserSplitContainer.Panel2Collapsed)
  483. {
  484. browserSplitContainer.Panel2Collapsed = false;
  485. }
  486. //Find devToolsControl in Controls collection
  487. Control devToolsControl = null;
  488. devToolsControl = browserSplitContainer.Panel2.Controls.Find(nameof(devToolsControl), false).FirstOrDefault();
  489. if (devToolsControl == null || devToolsControl.IsDisposed)
  490. {
  491. devToolsControl = Browser.ShowDevToolsDocked(
  492. parentControl: browserSplitContainer.Panel2,
  493. controlName: nameof(devToolsControl));
  494. EventHandler devToolsPanelDisposedHandler = null;
  495. devToolsPanelDisposedHandler = (s, e) =>
  496. {
  497. browserSplitContainer.Panel2.Controls.Remove(devToolsControl);
  498. browserSplitContainer.Panel2Collapsed = true;
  499. devToolsControl.Disposed -= devToolsPanelDisposedHandler;
  500. };
  501. //Subscribe for devToolsPanel dispose event
  502. devToolsControl.Disposed += devToolsPanelDisposedHandler;
  503. }
  504. }
  505. public Task<bool> CheckIfDevToolsIsOpenAsync()
  506. {
  507. return Cef.UIThreadTaskFactory.StartNew(() =>
  508. {
  509. return Browser.GetBrowserHost().HasDevTools;
  510. });
  511. }
  512. }
  513. }