CefSharpでポップアップウィンドウを制御する方法。ILifeSpanHandlerを実装する。

2020年3月18日

CefSharpを使ってリンクを新しいwindowで開いて、そのwindowを制御する方法について調べた。
ただ、まだまだ分からないところがあるので、現状わかったことを書いていく。
後日また新たにわかったことがあれば追記していきたい。

この記事に関するソースコードはこちら

ポップアップウィンドウはCefSharpが制御している

リンクを新しいウィンドウで開くと、 別段何も実装していないのに、ちゃんとCefSharpで実装されたかのようなウィンドウが出る。どうやら、CefSharpが独自にFormを作って制御してくれているようだ。

後ほど実際にソースコードを書いて例示してみるが、自分でブラウザchromiumBrowserをインスタンス化して開くこともできるようだ。
ただし、親子関係が失われるので推奨しないとドキュメントに書いてあった。

この方法を使用して、ポップアップの作成をキャンセルし、選択した新しいChromiumWebBrowserインスタンスでURLを開くことができます。このメソッドを使用すると、親子関係が存在しないことに注意することが重要です。だから一般的にはお勧めしません
You can cancel popup creation and open the URL in a new ChromiumWebBrowser instance of your choosing using this method. It is important to note the parent-child relationship will not exist using this method. So in general it’s not recommended

親子関係というのがしっくり理解できなかったので、実際に動かしてみてどういう部分で不便なのかやっとわかった。端的に言うと、window間のデータ連携に支障が出ることがわかった。
もしこの解釈に誤りがあれば教えて頂けると助かります。

ところで、 CefSharp で生成されるFormを制御したいことがあると思います。
実際にポップアップのアイコンを変更したいというニーズがあったので、実装しました。以下の記事を参考にしてください。

ILifeSpanHandlerを実装してポップアップを開いたタイミングでブレークさせる

まず、ポップアップウィンドウが開かれる瞬間を補足したい場合は、IlifeSpanHandlerインターフェースを実装したクラスを用意する必要がある。
以下ひな形を用意した。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CefSharp;

namespace WinFormsCefSharpSample.JsMessageSample
{
    class LifespanHandler : ILifeSpanHandler
    {
        public bool OnBeforePopup(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl,
            string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures,
            IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser)
        {
            newBrowser = null;
            return false;
        }

        public void OnAfterCreated(IWebBrowser chromiumWebBrowser, IBrowser browser)
        {
        }

        public bool DoClose(IWebBrowser chromiumWebBrowser, IBrowser browser)
        {
            return false;
        }

        public void OnBeforeClose(IWebBrowser chromiumWebBrowser, IBrowser browser)
        {
        }
    }
}

上記ソースを以下のようにして chromeBrowser.LifeSpanHandler に嵌め込む。
chromeBrowser.LifeSpanHandler = new LifespanHandler();

ブレークポイントをOnBeforePopupの中に仕込んでおけば、 新しいリンクをクリックした時に止るようになる。

さて、上記コードはLifespanHandlerを実装しなくても同じ挙動となるコードです。つまり、変化なし。
return true;
にすると、ポップアップが立ち上がってこなくなります。
(実験コードで編集中にポップアップが開かない!と思ってたら、自分で
return true; にしててドン引きした。)

自前でポップアップのFormを用意する前にまず確認

ここからは、 先に書いたwindow間のデータ連携に支障が出るというケースを確認していきたい。
そのために、まずは上記ソースをもとにして、window間の連携を簡単に作る。

例としては非常にシンプルだと思う。
親windowを閉じるとポップアップで開いた子のwindowも閉じるというもの。
このバージョンのソースコードはこちらです。
以下が実際のコードです。

    let popupWindow = null;
    function raisePopUp() {
        // こちらの例だと新しいwindowが開かれる。
        popupWindow = window.open("./popup.html","test",'width=500,height=500');
    }
    window.addEventListener("unload", () => {
        popupWindow.close();
    });

こちらを実行すると、親子両方の画面が閉じます。

自前でポップアップのFormを用意

では、自前でポップアップを作った場合どうなるかというのを確認していきます。 全部終わった後のソースはこちら
以下の作業をやりました。

LifespanHandlerにpopupを呼び出すためのアクションを定義する。
OnBeforePopupのタイミングでアクションを呼び出す。

LifespanHandlerをインスタンス化する側でアクションを登録できるように以下のように修正した。
LifespanHandler.csの変更部分は以下。

        /// <summary>
        /// popupを呼び出すためのアクション
        /// </summary>
        public Action<string> raisePopupForm;
        public bool OnBeforePopup(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl,
            string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures,
            IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser)
        {
            // 設定した
            raisePopupForm?.Invoke(targetUrl);
            newBrowser = null;
            return true;
        }

今回注意すべきところは、return true;にするところ。
これをしないと
Cefが管理しているPopup
自前で用意したPopup
の二つが立ち上がってしまう。

LifespanHandlerをnewしてアクションを登録する。

chromeBrowserを作るところで、以下のようにアクションを登録する。

            var lifespanHandler = new LifespanHandler();
            lifespanHandler.raisePopupForm = str =>
            {
                // OnBeforePopupから呼び出される
                var popupForm = new PopupForm(str);
                popupForm.Show();
            };
            chromeBrowser.LifeSpanHandler = lifespanHandler;

Popupを開くためのFormを追加

以前作ったFormBasic.csを流用して作りました。
変わったところはインスタンス化時にURLを受け取るようにしたところくらい。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;

namespace WinFormsCefSharpSample
{
    public partial class PopupForm : Form
    {
        private readonly string _url;

        public PopupForm(string url)
        {
            _url = url;
            InitializeComponent();
            InitializeChromium();
        }
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            Cef.Shutdown();
        }

        public ChromiumWebBrowser chromeBrowser;
        public void InitializeChromium()
        {
            CefSettings settings = new CefSettings();
            // CefのInitializeは一度だけ
            if (!Cef.IsInitialized)
            {
                // Initialize cef with the provided settings
                Cef.Initialize(settings);
                // これを入れないと黒い余白が発生していまう。
                Cef.EnableHighDPISupport();
            }

            // Create a browser component
            chromeBrowser = new ChromiumWebBrowser(this._url);
            // Add it to the form and fill it to the form window.
            this.Controls.Add(chromeBrowser);
            chromeBrowser.Dock = DockStyle.Fill;
        }
    }
}

動かしてみる

これら変更を反映して実際に実行してみると、自前のFormでポップアップが作られる。
親windowを閉じても、windowオブジェクト間の連携が取れていないため、子のポップアップは閉じない。
これ以外にも、どういった変化があるのかは、ポップアップで開かれる側のwindowオブジェクトを確認してみるとわかりそう。