20201117のC#に関する記事は8件です。

Uno Platform で湯婆婆を Windows アプリと WebAssembly 対応してみた(無駄に MVVM)

最近流行していて、さらにはアドベントカレンダーまで登場した湯婆婆ですが、Flutterで湯婆婆を実装してみる に触発されて湯婆婆の画面付きを作ってみました。

こんな感じです。左が Windows アプリで右側が WebAssembly になります。

yubaba.gif

実際に動くサイトは以下にデプロイしています。

湯婆婆 on Azure

使ってるもの

Windows, Android, iOS, WebAssembly, macOS, Linux に対応している Uno Platform を使って作りました。今回はスマホ対応は単純にレスポンシブに作ることがメンドクサカッタのと、macOS と Linux は環境を用意するのがメンドクサカッタので Windows と WebAssembly を対象に作りました。

Uno Platform については以前 Qiita で紹介しました。

Uno Platform 入門
image.png

公式サイトはこちらになります。

Uno Platform
image.png

無駄に頑張った所

今回の湯婆婆を作るうえで頑張ったところを列挙していきたいと思います。

?田さん対応

サロゲートペアに対応しています!!C# での対応方法は先日自分で書いたのでその通りにやりました。

C# で湯婆婆を実装してみる(?田さんにも対応)

無駄に MVVM

湯婆婆ごときにはオーバースペックなのでお勧めしませんが、なんとなく先日 Uno Platform に対応した Prism (MVVM フレームワーク) と ReactiveProperty を使って開発しています。

なのでソリューションエクスプローラーはこんなに沢山プロジェクトがあります。オーバースペック!

image.png

単体テスト書いた

湯婆婆のコアロジック(ちゃんとしたセリフになってるか)を単体テストしました。ランダムに 1 文字選ぶために乱数を生成している部分は乱数生成部分をモックに差し替え可能なように作って単体テストではモックを差し込んでいます。

テスト結果が緑だと気持ちいいですね!

image.png

?田さん対応のテストケースも書いています。あと大事な仕様の名前が未入力だと湯婆婆が死ぬことも確認しています。

[Fact]
public async Task SurrogatePairName()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Target.SubmitContractAsync(new ContractPaper("?田"));
    Assert.Collection(Target.Messages,
        act => Assert.Equal("フン。", act.Value),
        act => Assert.Equal("?田というのかい。", act.Value),
        act => Assert.Equal("贅沢な名だねぇ。", act.Value),
        act => Assert.Equal("今からお前の名前は?だ。", act.Value),
        act => Assert.Equal("いいかい、?だよ。", act.Value),
        act => Assert.Equal("分かったら返事をするんだ、?!!", act.Value));
    MockRandom.VerifyAll();
}

[Fact]
public async Task KillYubabaUsingNull()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Assert.ThrowsAsync<ArgumentNullException>(
        async () => await Target.SubmitContractAsync(new ContractPaper(null)));
}

[Fact]
public async Task KillYubabaUsingEmpty()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
        async () => await Target.SubmitContractAsync(new ContractPaper("")));
}

まとめ

ついかっとなって画面を作ってしまいました。ちょうど Uno Platform 版の Prism を試してみたかったので個人的には満足です。

ソースコードは以下のリポジトリで公開しています。

https://github.com/runceel/Yubaba.Uno

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# で湯婆婆を Windows アプリと WebAssembly 対応してみた(無駄に MVVM)

最近流行していて、さらにはアドベントカレンダーまで登場した湯婆婆ですが、Flutterで湯婆婆を実装してみる に触発されて湯婆婆の画面付きを作ってみました。

こんな感じです。左が Windows アプリで右側が WebAssembly になります。

yubaba.gif

実際に動くサイトは以下にデプロイしています。

湯婆婆 on Azure

使ってるもの

Windows, Android, iOS, WebAssembly, macOS, Linux に対応している Uno Platform を使って作りました。今回はスマホ対応は単純にレスポンシブに作ることがメンドクサカッタのと、macOS と Linux は環境を用意するのがメンドクサカッタので Windows と WebAssembly を対象に作りました。

Uno Platform については以前 Qiita で紹介しました。

Uno Platform 入門
image.png

公式サイトはこちらになります。

Uno Platform
image.png

無駄に頑張った所

今回の湯婆婆を作るうえで頑張ったところを列挙していきたいと思います。

?田さん対応

サロゲートペアに対応しています!!C# での対応方法は先日自分で書いたのでその通りにやりました。

C# で湯婆婆を実装してみる(?田さんにも対応)

無駄に MVVM

湯婆婆ごときにはオーバースペックなのでお勧めしませんが、なんとなく先日 Uno Platform に対応した Prism (MVVM フレームワーク) と ReactiveProperty を使って開発しています。

なのでソリューションエクスプローラーはこんなに沢山プロジェクトがあります。オーバースペック!

image.png

単体テスト書いた

湯婆婆のコアロジック(ちゃんとしたセリフになってるか)を単体テストしました。ランダムに 1 文字選ぶために乱数を生成している部分は乱数生成部分をモックに差し替え可能なように作って単体テストではモックを差し込んでいます。

テスト結果が緑だと気持ちいいですね!

image.png

?田さん対応のテストケースも書いています。あと大事な仕様の名前が未入力だと湯婆婆が死ぬことも確認しています。

[Fact]
public async Task SurrogatePairName()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Target.SubmitContractAsync(new ContractPaper("?田"));
    Assert.Collection(Target.Messages,
        act => Assert.Equal("フン。", act.Value),
        act => Assert.Equal("?田というのかい。", act.Value),
        act => Assert.Equal("贅沢な名だねぇ。", act.Value),
        act => Assert.Equal("今からお前の名前は?だ。", act.Value),
        act => Assert.Equal("いいかい、?だよ。", act.Value),
        act => Assert.Equal("分かったら返事をするんだ、?!!", act.Value));
    MockRandom.VerifyAll();
}

[Fact]
public async Task KillYubabaUsingNull()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Assert.ThrowsAsync<ArgumentNullException>(
        async () => await Target.SubmitContractAsync(new ContractPaper(null)));
}

[Fact]
public async Task KillYubabaUsingEmpty()
{
    MockRandom.Setup(x => x.Next(2)).Returns(0).Verifiable();
    await Target.RequestIntroductionAsync();
    await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
        async () => await Target.SubmitContractAsync(new ContractPaper("")));
}

まとめ

ついかっとなって画面を作ってしまいました。ちょうど Uno Platform 版の Prism を試してみたかったので個人的には満足です。

ソースコードは以下のリポジトリで公開しています。

https://github.com/runceel/Yubaba.Uno

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】マルチディスプレイにおける座標の扱い

はじめに

Unityでは、スタンドアロンモードのみマルチディスプレイに対応しています。(対応OS:Windows、Mac、Linux)

Windowsでマルチディスプレイ対応のアプリを作成していたところ、
座標の扱いで何度か混乱してしまったため、書き留めておきたいと思います。

環境

  • Windows 10
  • Unity 2019.2.19f1
  • Visual Studio 2019

スクリーン座標とワールド座標

Unityをさわったことがある方ならご存知だと思いますが、Unityの座標にはスクリーン座標系とワールド座標系が存在します。
スクリーン座標系は画面上の座標で、左下隅が (0, 0)となります。
ワールド座標系はUnityのゲーム空間上の座標で、Scene直下にあるオブジェクトであればInspectorのTransform > Position の値となります。
ワールド座標・スクリーン座標.PNG
マウス位置(Input.mousePosition)は画面上のどこかを表すものなので、当然スクリーン座標系となります。

マルチディスプレイの場合のマウス位置は?

マルチディスプレイの場合、Input.mousePositionで取得できる座標は、メインディスプレイ基準の座標となります。
例えば、1920 x 1080のディスプレイ2台を以下のように配置していた場合
ディスプレイ配置.PNG
座標は以下のようになります。
ディスプレイ配置2.PNG

ワールド座標に変換したい

取得したマウス位置の座標をワールド座標に変換するには、
① 画面ごとのスクリーン座標に変換
② 該当する画面のカメラでワールド座標に変換
の2段階の変換を行う必要があります。

    // cameras という変数に、ディスプレイ番号順のカメラが保持されている想定です

    private Vector2? ConvertScreenPointToWorld(Vector2 position)
    {
        // 1. 画面ごとのスクリーン座標に変換
        // ※ 各画面の左下隅を(0, 0)とする座標に変換されます
        var relativePosition = Display.RelativeMouseAt(position);
        // ※ 気持ち悪いですが、z座標にディスプレイ番号がセットされます...
        var displayIndex = (int)relativePosition.z;
        if (displayIndex >= cameras.Count) return null;

        // 2. 該当する画面のカメラでワールド座標に変換
        var camera = cameras[displayIndex];
        return camera.ScreenToWorldPoint(relativePosition);
    }

ちなみに、Unityエディター上では Display.RelativeMouseAt() は動作しないのでご注意ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

unityのオブジェクトをC#で生成、子要素に移動する方法

※Unity実践リファレンスの内容を参考にしています。
<エンジニアのためのUnity実践リファレンス ~ ゲーム開発にすぐに役立つスクリプト入門>
https://www.amazon.co.jp/dp/B00WHEJI8W/ref=cm_sw_em_r_mt_dp_K82SFb5NXTQ27

動的にC#でゲームオブジェクトを生成し、それを特定のゲームオブジェクトの子要素として移動させるためのコードになります

スクリーンショット 2020-11-17 15.25.13.png

    // test.cs

    // Start is called before the first frame update
    void Start()
    {
        //C#で使用する変数名にparentAという名前で作り、
        //ヒエラルキー上のオブジェクト名をParentGO_Aとして生成

        var parentA = new GameObject("ParentGO_A");
        var parentB = new GameObject("ParentGO_B");
        var childA = new GameObject("childrenGO_A");
        var childB = new GameObject("childrenGO_B");
        var childBchildA = new GameObject("childrenBchildrenGO_A");

        //childAをparentAの子要素にトランスフォーム
        //これでchildAのゲームオブジェクトを指定したゲームオブジェクトの子要素に変更
        childA.transform.parent = parentA.transform;
        childB.transform.parent = parentB.transform;

        //特定の要素の子要素にする場合に、2階層下の要素としても1回で変更できる
        //この場合は
        childBchildA.transform.parent = childB.transform;
    }

<実行前>
スクリーンショット 2020-11-17 15.24.52.png

<実行後>
スクリーンショット 2020-11-17 15.25.03.png

ちゃんと階層構造を持ったままゲームオブジェクトが生成されました。

この機能を使って特定の条件下で生成したゲームオブジェクトがあれば、それをFind関数を利用して見つけたのちに、子要素へ移動させるということもできそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CefSharpでカスタムWebブラウザを作る

1. 目的

CefSharpでカスタムWebブラウザを作成します。
CefSharpはChromiumをコアとするOpenSourceのWebViewです。
CefはChromiumをコアとしているので、ほぼChromeと同等のWebブラウザを作成することができます。

https://github.com/cefsharp/CefSharp

.NET FrameworkのWPFとWinformsで利用可能なコントロールとして提供されていますが、今回はWinforms版でカスタムWebブラウザを作ります。言語はC#を使います。

2. 最初の最初

以下の流れでWinformプロジェクトを作成して、一番シンプルなカスタムWebブラウザを作成します。

  1. 新規プロジェクト作成する
  2. Nugetパッケージの取り込む
  3. WebViewを貼り付ける

まだVisual Studioをインストールしていない場合は、以下のサイトからVisual Studio Express(無料)のインストーラをダウンロードして、インストールしましょう。
Visual Studio Express

2.1 新規プロジェクトの作成する

C#のWinformsのプロジェクトを新規作成します。
以下は、Visual Studio 2019 Expressでのやり方になります。

  1. 「新しいプロジェクトの作成」をクリックします。
  2. 「Windowsフォームアプリケーションの作成(.NET Framework)」を選択して「次へ」をクリックします。
  3. プロジェクト名と場所を指定して、「作成」をクリックします。
    ここでは、「SimpleBrowser」としました。
  4. 作成されるプロジェクトのフォームの名称がデフォルトで「Form1.cs」とカッコ悪いので、「ソリューション エクスプローラ」で「Form1.cs」を選択して、コンテキストメニューから「名前の変更」を選択して、適当な名前に変更します。
    ここでは、「SimpleBrowserFrame.cs」としました。
    ソリューションエクスプローラ.png

  5. CefSharpは「AnyCPU」でビルドできないので、構成マネージャで「AnyCPU」を削除して「x64」と「x86」を追加します。
    ※「x64」と「x86」のいずれか1つでももちろんOKです。
    ソリューションエクスプローラでソリューションを選択して、コンテキストメニューから「バッチビルド」を選択すると、以下のような構成に見えるはずです。
    構成.png

2.2 Nugetパッケージの取り込む

CefSharpのNugetパッケージを取り込みます。

  1. ソリューションエクスプローラで「SimpleBrowser」を選択して、コンテキストメニューから「NuGetパッケージの管理」を選択します。
  2. NuGetパッケージマネージャで、「参照」タブを選択します。
  3. 検索ボックスに「CefSharp」と入力して検索を実行すると、CefSharp関連のNugetパッケージが表示されます。
    Nuget.png

  4. 以下のNugetパッケージをインストールします。
    ※CefSharp.WinFormsをインストールすると、芋ずる式にすべてインストールされます。

    • CefSharp.WinForms
    • CefSharp.Common
    • cef.redist.x64
    • cef.redist.x86

表示されているバージョン「v85.3.130」は、chromiumのバージョンとほぼ一致しています。
もし特定のバージョンのchromiumを使いたい場合は、バージョンを指定してNugetを取り込みます。

2.3 WebViewを貼り付ける

CefSharpのWebViewであるChromiumWebBrowserコントロールをフォームに貼り付けます。
ChromiumWebBrowserはツールボックスにも表示されますが、ツールボックスからはコントロールを貼り付けできないので、フォームのコード(SimpleBrowserFrame.cs)に直接以下を記述します。

SimpleBrowserFrame.cs
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.WinForms; // <- 追加

namespace SimpleBrowser
{
    public partial class SimpleBrowserFrame : Form
    {
        /// <summary>
        /// CefSharpのWebViewのインスタンス
        /// </summary>
        private ChromiumWebBrowser WebBrowser;

        public SimpleBrowserFrame()
        {
            InitializeComponent();

            // CefSharpのWebViewを作成する。
            // 初期ページはgoogleにする。
            WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

            // コントロールを追加する。
            this.Controls.Add(WebBrowser);
            WebBrowser.Dock = DockStyle.Fill;
        }
    }
}

2.4 ビルドして実行する

何も考えずに実行すると、以下の様な非常にシンプルなWebブラウザが表示されます。
browser01.png

2.5 バージョンを確認する

初期URLを以下のようにchrome://versionに変更して実行してみると、バージョンのページが表示されます。

このようにchromeのカスタムスキーマの一部も表示させることができます。

chromeのバージョン確認
//WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");
WebBrowser = new ChromiumWebBrowser("chrome://version");    // 変更する。

browser02.png

3. 最低限の設定

ChromiumWebBrowserをnewして貼り付けるだけでも、Webコンテンツを表示することができますが、幾つかの最低限の設定をしておきます。
具体的には、以下の2つの方法で設定します。

  • Cef.Initializeメソッド
    ChromiumWebBrowserをnewする前に、一度だけ実行します。
    ChromiumWebBrowserを作成した後は、呼び出して設定を変更することはできません。
  • ChromiumWebBrowser.BrowserSettingsプロパティ
    ChromiumWebBrowserのインスタンスを作成した後に、インスタンス毎に設定します。

3.1 Cef.Initializeメソッドによる初期化

Cef.Initializeメソッドでは、以下の設定を行います。

キャッシュ用ディレクトリのパス

キャッシュ用ディレクトリは、ブラウザコンテンツのキャッシュのほかに以下の情報が格納されます。

  • history
  • cookie
  • indexedDB
  • localStorage
  • sessionStorage
  • application cache

キャッシュ用ディレクトリを設定するには、CefSettingsBase.CachePathプロパティに絶対パスでキャッシュ用ディレクトリのパスを指定します。
nullを指定した場合は、キャッシュが作成されないインメモリモード(incognito mode)になります。インメモリモードの場合、indexedDB、localStorageおよびapplication cacheは、もちろん保存されなくなります。

キャッシュ用ディレクトリの設定方法
CefSettings settings = new CefSettings();

// キャッシュディレクトリの設定
setting.settings = @"c:\temp\cache";

UserAgent文字列設定

CefSettingsBase.UserAgentで、UserAgent文字列を自由に設定することができます。
しかし、デフォルトのUserAgent文字列を取得することはできません。
デフォルトのUserAgent文字列を確認するには、開発者ツールでHTTPリクエストヘッダを確認する必要があります。
ちなみに、デフォルトのUserAgent文字列はx86とx64で異なっています。

UserAgentの設定方法
CefSettings settings = new CefSettings();

// 独自のUserAgent文字列を指定する
settings.UserAgent = @"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 SimpleBroser/1.0.0.0";

言語の設定

CefSettingsBase.AcceptLanguageListに、受け入れる言語をカンマ区切りのリストで指定します
ここで指定した言語リストは、HTTPリクエストヘッダのAccept-Languageに指定されます。
chromeのリクエストヘッダを見ると、ja,en-US;q=0.9,en;q=0.8と設定されており、日本語とアメリカ英語と英語が重み付けを含めて指定されていることが確認できます。
ここではchromeと同じ設定にしてみます。

言語の設定方法
CefSettings settings = new CefSettings();

// 受け入れる言語リストを指定する
settings.AcceptLanguageList = "ja,en-US;q=0.9,en;q=0.8";

ログレベルの設定

デフォルトでChromiumのデバッグログ(debug.log)が出力されます。
CefSettingsBase.LogSeverityでログのレベルを変更することで、ログ出力の内容を増やしたり、ログを出力しないようにすることができます。

ログの設定方法
CefSettings settings = new CefSettings();

// ログレベルを指定する。
settings.LogSeverity = LogSeverity.Verbose;

コマンドライン引数設定

CefSettingsBase.CefCommandLineArgsに、Chromeのコマンドライン引数を追加することができます。ただし、Chromiumのコマンドラインオプションはあまりドキュメント化されておらず、また実際に機能するかは、試してみないと分からないです。
ここでは、MediaDevices.getUserMedia()メソッドで、カメラにアクセスできるようにするために、enable-media-streamオプションを設定します。

コマンドライン引数の設定方法
CefSettings settings = new CefSettings();

// カメラにアクセスできるようにする
settings.CefCommandLineArgs.Add("enable-media-stream", "1");

以上の設定をまとめると、以下のようになります。

Cef.Initializeによる初期化の例
CefSettings settings = new CefSettings() {
    AcceptLanguageList = "ja,en-US;q=0.9,en;q=0.8",
    CachePath = @"c:\temp\cache",
    UserAgent = @"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 SimpleBroser/1.0.0.0",
    LogSeverity = LogSeverity.Verbose
}

// カメラにアクセスできるようにする
settings.CefCommandLineArgs.Add("enable-media-stream", "1");

// Application.Runの前に初期化を呼び出す。
Cef.Initialize(settings);

Application.Run(new SimpleBrowserFrame());

Cookie用ディレクトリの設定(おまけ)

バージョン73.1.xまでは、Cookie用のディレクトリはキャッシュ用フォルダと別に指定することができましたが、75.1.x以降はキャッシュ用ディレクトリ配下にCookieが作成されるようになり、cookie用ディレクトリは個別に設定することはできなくなりました。バージョン73.1.x以前を使用する場合は、ICookieManager.SetStoragePathを呼び出して、cookieのパスを指定します。呼び出しのタイミングはIBrowserProcessHandler.OnContextInitializedになります。
参考のために、設定例を以下に示します。※このメソッドは、75.1.x以降は使用できません。

Cookie用ディレクトリの設定方法
namespace SimpleBrowser.Handlers
{
    class BrowserProcessHandler : IBrowserProcessHandler
    {
        void IDisposable.Dispose()
        {
        }

        void IBrowserProcessHandler.OnContextInitialized()
        {
            // グローバールCookieマネージャを取得して、Cookieのパスを設定する。
            ICookieManager cookieManager = Cef.GetGlobalCookieManager();
            cookieManager.SetStoragePath(@"c:\temp\cookie", false, null);
        }

        void IBrowserProcessHandler.OnScheduleMessagePumpWork(long delay)
        {
        }
    }
}

3.2 ChromiumWebBrowser.BrowserSettingsによる初期化

ChromiumWebBrowserインスタンスをnewした後、ChromiumWebBrowser.BrowserSettingsプロパティで主に以下の項目を設定することができます。

  • Application cacheの使用許可
  • フォントファミリー
  • フォントサイズ
  • Javascriptの使用許可
  • Javascriptでdocument.execCommandを使ったcopy&pasteの使用許可
  • LocalStorageの使用許可

これらを設定する例を以下に示します。

BrowserSettingsの設定による初期化方法
public SimpleBrowserFrame()
{
    InitializeComponent();

    // CefSharpのWebViewを作成する。
    WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

    // コントロールを追加する。
    this.Controls.Add(WebBrowser);
    WebBrowser.Dock = DockStyle.Fill;

    // javascriptを有効にする。(書かなくても有効です)
    WebBrowser.BrowserSettings.Javascript = CefState.Enabled;
    // document.execCommandでのcopy&pasteを有効にする。
    WebBrowser.BrowserSettings.JavascriptDomPaste = CefState.Enabled;
    // ApplicationCacheを有効にする。
    WebBrowser.BrowserSettings.ApplicationCache = CefState.Enabled;
    // localStorageを有効にする。
    WebBrowser.BrowserSettings.LocalStorage = CefState.Enabled;
    // フォントサイズを設定する。(レイアウトがずれる場合があるので注意)
    WebBrowser.BrowserSettings.DefaultFontSize = 16; 
}

なお、ドキュメントに書かれていても、実際には機能しない設定もあるので注意が必要です。
具体的には、以下は機能しませんでした。

  • IBrowserSettings.Databases
    indexedDBの使用許可・禁止ができると書かれていますが、indexedDBを使用禁止にすることはできませんでした。
  • IBrowserSettings.JavascriptAccessClipboard
    javascriptからclipboardを操作できるように書かれていますが、有効になるように設定しても、navigator.clipboard.writetext等のclipboard APIはアクセス拒否されます。

4. Proxyの設定

デフォルトでは、システムのインターネットオプションで指定しているProxy設定が使用されますが、システムのインターネットオプションと別に、独自のProxy設定を行うことができます。Proxy設定の方法は、以下のいずれかの方法で行います。

  • (方法1)CefSharpSettings.Proxyで設定
  • (方法2)コマンドラインオプションで指定
  • (方法3)WebViewのリクエストコンテキストで指定
  • (方法4)プログラム全体のリクエストコンテキストで指定

4.1 (方法1)CefSharpSettings.Proxyで設定

CefSharpSettings.Proxyスタティックメソッドを使用します。
IP、Port、UserName、Password、BypassListを設定することで、自動的にProxyサーバの認証処理も行うようになります。
この方法の場合は、ユーザに認証情報の入力を求めることができません。
また、一度設定すると、プロセスが終了するまで変更することができません。

Proxyの設定方法1
// Cef.Initializeの前に呼び出す。
CefSharpSettings.Proxy = new ProxyOptions("127.0.0.1", "8080", "UserName", "Password", "192.168.0.*;*.test.com");
Cef.Initialize(settings);

4.2 (方法2)コマンドラインオプションで指定

以下のコマンドラインオプションを指定します。
これが一番簡単です。

  • proxy-serverオプションでIPとPORTを指定する。
  • proxy-bypass-listオプションでBypassListを指定する。
Proxyの設定方法2
CefSettings settings = new CefSettings();

// プロキシ情報を設定する。
settings.CefCommandLineArgs.Add("proxy-server", "127.0.0.1:8080");
settings.CefCommandLineArgs.Add("proxy-bypass-list", "192.168.0.*;*.test.com");

Cef.Initialize(settings);

この方法の場合、ユーザに認証情報の入力を求める処理を実装することができます。
認証情報が必要になると自動的に認証処理が呼び出されます(詳細は「9. 認証」を参照)。
この方法も一度設定すると、プロセスが終了するまで変更することができません。

4.3 (方法3)WebViewのリクエストコンテキストで指定

WebViewのリクエストコンテキスト(IBrowserHost.RequestContext)に対して、IRequestContext.SetPreferenceメソッドを呼び出して、プロキシの情報を設定します。最も細かくProxyを設定できる方法で、複数のWebViewがある場合でも、それぞれのWebViewで別々のProxyを設定することができます。

この方法の場合も、ユーザに認証情報の入力を求める処理を実装することができます。
認証情報が必要になると自動的に認証処理が呼び出されます(詳細は「9. 認証」を参照)。
また、途中でプロキシ設定を変更することもできます。

第一引数に"proxy"という文字列、第二引数に以下のKey&Valueを持つDictionaryインスタンスを指定して、IRequestContext.SetPreferenceメソッドを呼び出します。※この部分はドキュメントが少ないです。

Key Value
mode "fixed_servers"を指定します
server プロキシサーバとポート番号を指定します
bypass_list プロキシサーバをバイパスするURLのリストを指定します
Proxyの設定方法3
Cef.UIThreadTaskFactory.StartNew(new Action(() =>
{
    // プロキシに設定する内容をDictionaryオブジェクトで設定する
    var v = new Dictionary<string, object>();
    v.Add("mode", "fixed_servers");
    v.Add("server", "localhost:8080");
    v.Add("bypass_list", "*.google.co.jp;*.nikkei.com");

    // proxyを設定する
    String error;
    WebBrowser.GetBrowserHost().RequestContext.SetPreference("proxy", v, out error);
}));

なお、IRequestContext.SetPreferenceメソッドは必ずCef UI Threadのコンテキストで呼び出す必要があるため、Cef.UIThreadTaskFactoryスタティックメソッドを使って、呼び出しのコンテキストを強制する必要があります。

4.4 (方法4)プログラム全体のリクエストコンテキストで指定

(方式3)とほぼ同じですが、WebView単位ではなく、プログラム全体でのリクエストコンテキストに対して、Proxy設定を行う方法になります。プログラム全体のリクエストコンテキストはCef.GetGlobalRequestContextメソッドで取得することができます。取得したRequestContextに対して、(方法3)と同じ処理を行います。

Proxyの設定方法4
Cef.UIThreadTaskFactory.StartNew(new Action(() =>
{
    // プロキシに設定する内容をDictionaryオブジェクトで設定する
    var v = new Dictionary<string, object>();
    v.Add("mode", "fixed_servers");
    v.Add("server", "localhost:8080");
    v.Add("bypass_list", "*.google.co.jp;*.nikkei.com");

    // グローバルなRequestContextに対してproxyを設定する
    String error;
    Cef.GetGlobalRequestContext().SetPreference("proxy", v, out error);
}));

5. 開発者ツール

実験を行うには開発者ツールが便利です。
CefSharpではWebBrowserExtensions.ShowDevToolsメソッドを呼び出すだけで、Chromeでおなじみの開発者ツールを表示することができます。
ここでは、Chromeと同じように、F12キーが押下されると、開発者ツールを表示するようにします。

5.1 IKeyboardHandlerインタフェースを実装するクラスを作成する

キー押下をハンドリングするためには、IKeyboardHandlerインタフェースを実装するクラスを作成する必要があります。Visual Studioのソリューションエクスプローラで、コンテキストメニューから「追加」ー「クラス」を選択します。
ここではクラス名をKeyboardHandlerにします。また、今後ハンドラが増えるので、ハンドラを実装するクラスを「Handlers」フォルダにまとめておきます。

5.2 作成したクラスでIKeyboardHandlerを実装する

以下のように、IKeyboardHandlerを実装させます。

IKeyboardHandlerを実装するクラス
using CefSharp;
namespace SimpleBrowser.Handlers
{
    class KeyboardHandler : IKeyboardHandler
    {
    }
}

次に、IKeyboardHandlerの文字列を選択すると電球マークが出るので、電球マークをクリックして、コンテキストメニューから「すべてのメンバーを明示的に実装する」を選択します。これで、Visual Studioが自動でインタフェースのメソッドのスケルトンを実装してくれます。

5.3 F12キー押下時の処理を追加する。

IKeyboardHandlerインタフェースはOnKeyEventOnPreKeyEventの2つのメソッドを持ちます。

  • OnKeyEvent
    キーが押下されてページの描画処理やページ内のjavascriptがキーを受け取った後に呼び出されるハンドラです。
  • OnPreKeyEvent
    キーが押下されてページの描画処理やページ内のjavascriptにキーが通知される前に呼び出されるハンドラです。
    ここでは、OnPreKeyEventでF12キーの押下をハンドリングして、開発者ツールを表示します。
キー押下処理の追加方法
using CefSharp;
namespace SimpleBrowser.Handlers
{
    class KeyboardHandler : IKeyboardHandler
    {
        bool IKeyboardHandler.OnKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey)
        {
            return false;
        }

        bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut)
        {
            if (type == KeyType.RawKeyDown)
            {
                // VK_F12キー押下
                if (windowsKeyCode == (int)Keys.F12 && modifiers == CefEventFlags.None)
                {
                    // 開発者ツールを表示する
                    browser.ShowDevTools();
                    return true;
                }                    
            }
            return false;
        }
    }
}

5.4 WebViewにハンドラを設定する。

作成したキー押下時のハンドラを、ChromiumWebBrowser.KeyboardHandlerプロパティに設定します。

ChromiumWebBrowserへのIKeyboardHandlerインタフェースの設定
public partial class SimpleBrowserFrame : Form
{
    /// <summary>
    /// CefSharpのWebViewのインスタンス
    /// </summary>
    private ChromiumWebBrowser WebBrowser;

    public SimpleBrowserFrame()
    {
        InitializeComponent();

        // CefSharpのWebViewを作成する。
        WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

        // いろいろと初期化処理
        //     :
        //     :

        // キーボードハンドラを設定する
        WebBrowser.KeyboardHandler = new Handlers.KeyboardHandler();
    }
}

5.5 カスタムWebブラウザを起動して、F12キー押下をしてみる。

うまく行くと、以下の様なおなじみの開発者ツールが起動します。
開発者ツール.png

おー!表示されました。

6. 戻る、進む、更新、ロード

一般的なブラウジング操作として、戻る、進む、更新、ロードを実装します。
まず、最初にショートカットキーの処理を実装して、次にUI操作からの処理を実装します。

6.1 ショートカットキー押下時の処理追加

ショートカットキーの処理を実装するには、「5. 開発者ツール」で説明したOnPreKeyEventに処理を追加します。

  • ALT+[←] 戻る
    IBrowser.GoBackメソッドを呼び出します。
    戻ることが可能な状況かどうかは、IBrowser.CanGoBackプロパティを確認することで判断します。
  • ALT+[→]進む
    IBrowser.GoForwardメソッドを呼び出します。
    進むことが可能な状況かどうかは、IBrowser.CanGoForwardプロパティを確認することで判断します。
  • F5 更新
    IBrowser.Reloadメソッドを呼び出します。
    なお、ロード中に更新を許可させない場合は、IBrowser.IsLoadingを参照して、ロード中かどうかを判断します。

以下は実装例です。

キー押下処理の追加
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut)
{
    if (type == KeyType.RawKeyDown)
    {
        // ALT+VK_LEFT
        if (windowsKeyCode == (int)Keys.Left &&  modifiers == CefEventFlags.AltDown)
        {
            if (browser.CanGoBack)
            {
                // 戻る
                browser.GoBack();
                return true;
            }
        }
        // ALT+VK_RIHGT
        else if (windowsKeyCode == (int)Keys.Right && modifiers == CefEventFlags.AltDown)
        {
            if (browser.CanGoForward)
            {
                // 進む
                browser.GoForward();
                return true;
            }
        }
        // VK_F5
        else if (windowsKeyCode == (int)Keys.F5 && modifiers == CefEventFlags.None)
        {
            // 更新する
            browser.Reload();
            return true;
        }
        // (省略)
    }

    return false;
}

これで、戻る、進む、更新の操作がショートカットキーでできるようになりました。

6.2 ボタンとINPUTの配置

ショートカットと同じ処理を、UI操作からもできるようにします。
まず、ユーザ操作を受け付けるために、「戻る」「進む」「更新」「アドレスバー」を配置します。
GUIの編集ですので、以下で紹介するやり方にとらわれず、自由に作成しても問題ありません。

TableLayoutPanelを配置する。

フォームにツールボックスからTableLayoutPanelを選んでドラッグ&ドロップします。

  • TableLayoutPanelのDockをFillにして、フォーム全体をカバーするようにします。
  • TableLayoutPanelの行と列を適当に配置します。

レイアウト1.png

WebViewのコンテナを配置する。

WebViewを配置する部分に、コンテナ用のPanelを配置します。

  • Panelは(0,1)の位置から、ColumnSpan=4で複数セルをカバーさせています。
  • PanelPanelのDockをFillにして、連結したセル全体をカバーさせます。
  • PanelPanelのNameをwebViewContainerとしておきます。

レイアウト2.png

アドレスバーを配置する。

アドレスバーを配置します。

  • TextBoxを(3,0)の位置に配置します。
  • TextBoxをのDockをFillにして、セル全体をカバーさせます。
  • TextBoxをのMarginやFontを適当の変更して、見た目を調整する。
  • TextBoxのNameをaddressBarとしておきます。

レイアウト3.png

ボタンを配置する。

戻る、進む、更新のボタンを配置します。

  • Buttonを3つ配置します。
  • それぞれのButtonでDockをFillにして、セル全体をカバーさせます。
  • それぞれのButtonのイメージを適当に設定します。
  • それぞれのButtonのNameをbackBtnforwardBtnreloadBtnとしておきます。

レイアウト4.png

WebViewの作成処理を変更する。

ChromiumWebBrowserの配置場所を、先ほど作ったPanelに変更します。

WebViewのコンテナ変更
// CefSharpのWebViewを作成する。
WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

// コントロールをPanelに配置する
this.webViewContainer.Controls.Add(WebBrowser);
WebBrowser.Dock = DockStyle.Fill;

以上で、戻る、進む、更新のボタンとアドレスバーを持つシンプルなWebブラウザのUIが完成しました。

6.3 ボタンクリック時の処理追加

戻る、進む、更新のボタンクリックの処理を追加します。
フォームのデザインビューで、それぞれのボタンをダブルクリックして、click時のハンドラを追加します。

戻る、進む、更新のボタンクリックの処理ハンドリング
private void backBtn_Click(object sender, EventArgs e)
{
    if (WebBrowser.CanGoBack)
    {
        // 戻る
        WebBrowser.Back();
    }
}

private void forwardBtn_Click(object sender, EventArgs e)
{
    if (WebBrowser.CanGoForward)
    {
        // 進む
        WebBrowser.Forward();
    }
}

private void reloadBtn_Click(object sender, EventArgs e)
{
    // 更新する
    WebBrowser.Reload();
}

6.4 アドレスバーでの処理追加

アドレスバーでEnterキーが押下された時に、アドレスバーに入力されているURLをロードするようにします。
アドレスバーのKeyDownイベントのハンドラとして、addressBar_KeyDownメソッドを追加して、以下の処理を追加します。

アドレスバーでの入力ハンドリング
private void addressBar_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.Enter)
    {
        // アドレスバーに入力があれば、ロードする
        if (!String.IsNullOrEmpty(addressBar.Text))
        {
            // アドレスバーに入力されているURLをロードする。
            WebBrowser.Load(addressBar.Text);
        }
    }
}

6.5 実行してみる

ビルドして実行してみます。
正しく実装されていると、以下のようなブラウザが表示されるはずです。
基本UI.png

ALT+[→]、ALT+[←]、F5のショートカットキーが有効になっていることも確認できます。
ボタンやアドレスバーも操作可能になっています。

6.5 操作対象についての注意

IKeyboardHandlerハンドラでの戻る、進む、更新の処理は、IWebBrowserではなくIBrowserに対して行っています。
IWebBrowserとIBrowserは同じWebViewを指しているように思われるかもしれませんが、window.openメソッドで開いた子ウィンドウの場合など、明示的にChromiumWebBrowserをnewして作成していないWebViewの場合は、IWebBrowserとIBrowserは同じWebViewを指していないので注意が必要です。

  • ChromiumWebBrowserをnew作成した場合
    以下の様にChromiumWebBrowserIWebBrowserIBrowserは同じWebViewと関連しています。
    IKeyboardHandler等のCefSharpのハンドラのメソッドは、これらの引数にして呼び出されることになります。
    この場合は、IWebBrowserIBrowserのどちらを操作しても、結果は同じになります。

    UML1.png

  • a要素のクリックやwindow.openにより子ウィンドウが自動的に作成された場合
    CefSharpにより自動作成されたWebViewでは、ChromiumWebBrowserIWebBrowserは存在しておらず、参照を取得することができません。
    IKeyboardHandler等のCefSharpのハンドラのメソッドでは、親ウィンドウのIWebBrowserインタフェースが引数として渡されます。
    この場合は、IWebBrowserを操作すると親ウィンドウのWebViewが操作されてしまうことになります。

    UML2.png

まとめると、ChromiumWebBrowserを明示的にnewして作ったWebView以外のWebViewが存在する場合(子ウィンドをサポートする場合)は、WebViewに対する操作は、IWebBrowserに対してではなく、必ずIBrowserに対して行うようにします。

なお、上の「6.3 ボタンクリック時の処理追加」と「6.4 アドレスバーでの処理追加」では、IBrowserではなくChromiumWebBrowserに対して、GoBack等のメソッドを呼び出しています。これらについては、「22. 子ウィンドウオープン」でILifeSpanHandlerを実装した後に、IBrowserインタフェースを操作する様に変更します。

7. 拡大・縮小

WebBrowserExtensions.SetZoomLevelメソッドを呼び出すことで、拡大率を適用します。
WebBrowserExtensions.GetZoomLevelAsyncメソッドを呼び出すことで、現在の拡大率を取得することができます。

7.1 拡大率とZoomLevelの関係

両メソッドともにdouble値のZoomLevelで拡大・縮小のレベルを使用します。
ZoomLevelと拡大率(等倍を1.0とする倍率)には、以下の関係があります。

\begin{align}
1.2^{ZoomLevel} &= 拡大率\\
ZoomLevel &= \log_{1.2} 拡大率 = \frac{\log 拡大率}{\log 1.2}
\end{align}

Chromの拡大率は以下のようになっています。
- 拡大は、110%、125%、150%、175%、200%、250%、300%、400%、500%
- 縮小は、90%、80%、75%、67%、50%、33%、25%

これらの拡大率をZoomLevelに直すと以下のようになります。

拡大率 計算式 ZoomLevel
500% $\log_{1.2} 5 = \frac{\log 5}{\log 1.2}$ 8.827
400% $\log_{1.2} 4 = \frac{\log 4}{\log 1.2}$ 7.604
300% $\log_{1.2} 3 = \frac{\log 3}{\log 1.2}$ 6.026
250% $\log_{1.2} 2.5 = \frac{\log 2.5}{\log 1.2}$ 5.026
200% $\log_{1.2} 2 = \frac{\log 2}{\log 1.2}$ 3.802
175% $\log_{1.2} 1.75 = \frac{\log 1.75}{\log 1.2}$ 3.069
125% $\log_{1.2} 1.25 = \frac{\log 1.25}{\log 1.2}$ 1.224
110% $\log_{1.2} 1.1 = \frac{\log 1.1}{\log 1.2}$ 0.523
90% $\log_{1.2} 0.9 = \frac{\log 0.9}{\log 1.2}$ -0.578
80% $\log_{1.2} 0.8 = \frac{\log 0.8}{\log 1.2}$ -1.224
75% $\log_{1.2} 0.75 = \frac{\log 0.75}{\log 1.2}$ -1.578
67% $\log_{1.2} 0.67 = \frac{\log 0.67}{\log 1.2}$ -2.197
50% $\log_{1.2} 0.5 = \frac{\log 0.5}{\log 1.2}$ -3.802
33% $\log_{1.2} 0.33 = \frac{\log 0.33}{\log 1.2}$ -6.081
25% $\log_{1.2} 0.25 = \frac{\log 0.25}{\log 1.2}$ -7.604

7.2 ショートカットキーで拡大・縮小・等倍の処理を追加する

ショートカットキーをハンドリングするために、IKeyboardHandler.OnPreKeyEventに処理を追加します。

  • Ctrl + [+]
    現在の拡大率を取得して、現在の拡大率より一つ大きい拡大率を適用します。
  • Ctrl + [-]
    現在の拡大率を取得して、現在の拡大率より一つ小さい拡大率を適用します。
  • Ctrl + [0]
    等倍にします。

以下の様なコードになります。
なお、[+][-][0]のキーはキーボード本体とテンキーの2つにあり、それぞれキーコードが区別されているで、複数の条件を指定する必要があります。

拡大縮小のショートカットキーのハンドリング方法
class KeyboardHandler : IKeyboardHandler
{
    // ZoomLevelの定義
    private static readonly List<double> ZOOM_LEVLES = new List<double>{ -7.604, -6.081, -3.802, -2.197, -1.578, -1.224, -0.578, 0, 0.523, 1.224, 3.069, 3.802, 5.026, 6.026, 7.604, 8.827 };

    // (省略)

    bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut)
    {
        if (type == KeyType.RawKeyDown)
        {
            // Ctrl + NumberPad_Add
            // Ctrl + Add
            if ((windowsKeyCode == (int)Keys.Add && (modifiers == (CefEventFlags.ControlDown | CefEventFlags.IsKeyPad))) ||
                (windowsKeyCode == (int)Keys.Oemplus && (modifiers == (CefEventFlags.ControlDown | CefEventFlags.ShiftDown))))
            {
                // 現在のZoomLevelを取得する
                browser.GetZoomLevelAsync().ContinueWith((t) =>
                {
                    // 現在のZoomLevelの次に大きなZoomLevelを求めて設定する
                    int index = ZOOM_LEVLES.IndexOf(t.Result);
                    index = Math.Min(++index, ZOOM_LEVLES.Count - 1);
                    browser.SetZoomLevel(ZOOM_LEVLES[index]);
                });
                return true;
            }
            // Ctrl + NumberPad_Minus
            // Ctrl + Minus
            else if ((windowsKeyCode == (int)Keys.Subtract && (modifiers == (CefEventFlags.ControlDown | CefEventFlags.IsKeyPad))) ||
                (windowsKeyCode == (int)Keys.OemMinus && (modifiers == (CefEventFlags.ControlDown | CefEventFlags.ShiftDown))))
            {
                // 現在のZoomLevelを取得する
                browser.GetZoomLevelAsync().ContinueWith((t) =>
                {
                    // 現在のZoomLevelの次に小さいZoomLevelを求めて設定する
                    int index = ZOOM_LEVLES.IndexOf(t.Result);
                    index = Math.Max(--index, 0);
                    browser.SetZoomLevel(ZOOM_LEVLES[index]);
                });
                return true;
            }
            // Ctrl + NumberPad_0
            // Ctrl + 0
            else if ((windowsKeyCode == (int)Keys.D0 && modifiers == CefEventFlags.ControlDown) ||
                (windowsKeyCode == (int)Keys.NumPad0 && modifiers == (CefEventFlags.ControlDown | CefEventFlags.IsKeyPad)))
            {
                // 100%の倍率に戻す
                browser.SetZoomLevel(0);
                return true;
            }
        }

        return false;
    }
}

以上で、拡大、縮小、等倍の操作がショートカットキーからできるようになりました。

8. 印刷

WebBrowserExtensions.Printメソッドを呼び出すことで、簡単にWebViewの印刷ができます。
ここでは、Ctrl+[P]ショートカットキーに、印刷処理を割りあてます。

8.1 ショートカットキーの追加

ショートカットキーをハンドリングするために、IKeyboardHandler.OnPreKeyEventに処理を追加します。

印刷のショートカットキーのハンドリング
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser chromiumWebBrowser, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut)
{
    if (type == KeyType.RawKeyDown)
    {
        // CTRL+[P]
        if (windowsKeyCode == (int)Keys.P && modifiers == CefEventFlags.ControlDown)
        {
            // 印刷する
            browser.Print();
            return true;
        }
        // ほかのショートカットキーの処理
        //         :
    }
    return false;
}

8.2 印刷の確認

ビルドして実行してみます。
Ctrl+[P]を押下すると、印刷ダイアログが表示されます。
なお、PDFに印刷したい場合は、WebBrowserExtensions.PrintToPdfAsyncメソッドを呼び出すことで、PDFファイルに保存することができます。

9. 認証

Basic認証やDigest認証が求められる場合に、認証情報を入力するための仕組みを実装することができます。Proxyサーバで認証が必要な場合にも、この仕組みで認証情報の入力をユーザに求めることができます。
認証情報が必要になると、IRequestHandler.GetAuthCredentialsメソッドがCefSharpから呼び出されます。
なお、HTMLの仕様では一度Basic認証やDigest認証で認証に成功しても、それ以降同じホストに対するHTTPリクエストの度に認証が要求されます。一般的なブラウザはこの認証要求を、入力済みの認証情報を使って自動的に認証処理を行っています。
CefSharpでも同様に一度認証に成功すると、それ以降同じホストからの認証要求は自動的に処理されるため、2回目以降はこのハンドラは呼び出されなくなります。

9.1 IRequestHandlerを実装するクラスの作成

IRequestHandlerインタフェースは、CefSharpを使うアプリケーションで最も重要なインタフェースです。
ここでは、このインタフェースを実装するクラスとして、RequestHandlerクラスを追加します。
5. 開発者ツール」でのやり方と同様に、ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にRequestHandlerを指定します。そして、以下のようにIRequestHandlerを実装させます。

IRequestHandlerを実装するクラス
namespace SimpleBrowser.Handlers
{
    class RequestHandler: IRequestHandler
    {
    }
}

その後、IRequestHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。

9.2 IRequestHandlerインタフェースのデフォルト処理の実装

Visual Studioで自動実装したメンバーは例外をスローするようになっているので、以下のデフォルト処理を実装します。

  • IRequestHandler.GetAuthCredentials
    サーバが認証を要求した時に呼び出されるハンドラです。
    一般的な実装としては、ダイアログボックスを表示して、ユーザにユーザ名とパスワードを入力させる処理を行います。
    この部分は「9.4 GetAuthCredentialsメソッドの実装」で実装していきます。
  • IRequestHandler.GetResourceRequestHandler
    リソースのリクエストを行う前に呼び出されるハンドラで、主に以下の処理を行うためのインタフェースを返却します。
    • Cookieの送信や受け入れの許可と禁止
    • HTTPレスポンスの内容を変更するためのフィルタの返却
    • リソースのロード許可と禁止
    • HTTPレスポンス受信、HTTPのリダイレクト、ロード完了のハンドリング
      ここでは、デフォルトの動作をさせるために、nullを返却しておきます。
  • IRequestHandler.OnBeforeBrowse
    ナビゲーションを許可するかどうかを判断するハンドラです。
    ここでは、trueを返却して、ナビゲーションを許可します。
  • IRequestHandler.OnCertificateError
    SSLの証明書でエラーが発生した場合(オレオレ証明書など)に、処理を継続するかどうかを判断するためのハンドラです。
    一般的な実装としては、証明書エラーのページを一度表示して、ユーザに表示を継続するかどうかを問い合わせて、その結果に応じて処理を行うことになります。
    ここでは、常に処理を継続するようにするため、引数で指定されているcallbackのIRequestCallback.Continueメソッドをtrueを指定して呼び出してtrueを返却します。
  • IRequestHandler.OnDocumentAvailableInMainFrame
    window.documentオブジェクトが作成されたときに呼び出されるハンドラです。
    ここでは特に何もしません。
  • IRequestHandler.OnOpenUrlFromTab
    Ctrl+左クリックでアンカーをクリックした際に、新しいタブでナビゲーションを開始するかを判断するためのハンドラです。 ここでは、このタイプのナビゲーションを許可しないようにfalseを返却するようにします。
  • IRequestHandler.OnPluginCrashed
    Pluginがクラッシュしたときに呼び出されるハンドラですが、特に何もしません。
  • IRequestHandler.OnQuotaRequest
    webkitStorageInfo.requestQuotaメソッドでクオータの要求があった場合のハンドラです。
    一般的な実装としては、ダイアログボックスを表示してユーザに許可するかどうかを問い合わせて、その結果で許可または禁止するように実装します。
    ここでは常に許可する動作として、引数で指定されているcallbackのIRequestCallback.Continueメソッドをtrueを指定して呼び出してtrueを返却します。
  • IRequestHandler.OnRenderProcessTerminated
    予期せずレンダープロセスが終了した場合のハンドラです。
    一般的な実装の場合、ユーザに故障発生のダイアログを出したり、ブラウザを再起動する処理を行うことになります。
    ここでは特に何もしません。
  • IRequestHandler.OnRenderViewReady
    レンダーのViewが準備できた時のハンドラです。
    ここでは特に何もしません。
  • IRequestHandler.OnSelectClientCertificate
    SSLでサーバがクライアント証明書を要求する時に、クライアント証明書を選択するために呼び出されるハンドラです。
    一般的な実装では、クライアント証明書の一覧を表示して、ユーザに選択させる処理になります。
    ここではfalseを返却して、クライアント証明書リストの先頭の証明書を使用するデフォルトの動作をさせます。

9.3 認証用ダイアログボックスの実装

ソリューションエクスプローラでコンテキストメニューから「追加」-「フォーム(Windows フォーム)」を選択して、新しいフォームを追加します。フォームの名前はここではAuthDialogとしました。
そして、以下のようにユーザ名とパスワードのTextInputとOKとCANCELボタンを持つダイアログボックスをデザインします。
※GetAuthCredentialsメソッドでは、認証を要求しているhostの情報、ポート番号、サーバが返却したrealm文字列等の情報も受け取るので、これらの情報も表示するようにすることもできます。

ログオン.png

また、AuthDialogクラスに以下を実装します。

  • 入力されたユーザ名を保持するUserNameプロパティを追加します。
  • 入力されたパスワードを保持するPasswordプロパティを追加します。
  • OKボタンがクイックされたときに、UserNameとPasswordプロパティに入力値を設定します。
  • OKボタンが選択されたときにDialogResult.OKが返却されるように、フォームのAcceptButtonに割り当てます。
  • CANCELボタンが選択されたときにDialogResult.Cancelが返却されるように、フォームのCancelButtonに割り当てます。
  • Topmostで表示するようにします。

以下のようなコードになります。

認証情報入力ダイアログ
using System;
using System.Windows.Forms;

namespace SimpleBrowser
{
    public partial class AuthDialog : Form
    {
        /// <summary>
        /// ダイアログに入力されたユーザ名
        /// </summary>
        public String UserName { get; private set; }

        /// <summary>
        /// ダイアログに入力されたパスワード
        /// </summary>
        public String Password { get; private set; }

        public AuthDialog()
        {
            InitializeComponent();
        }

        private void ConfirmBtn_Click(object sender, EventArgs e)
        {
            // 確定したユーザ名とパスワードをプロパティに設定する。
            UserName = userNameTxt.Text;
            Password = passwordTxt.Text;
        }
    }
}

9.4 GetAuthCredentialsメソッドの実装

いよいよGetAuthCredentialsメソッドを実装します。
このメソッドはアプリケーションのUIスレッドとは異なるCEF IO threadと呼ばれるスレッドから呼び出されていますが、このスレッドを絶対にブロックしてはいけません。
そこで、WebViewの親コントロールのコンテキス(UIスレッドのコンテキスト)で、ダイアログを表示してユーザにユーザ名とパスワードの入力を促すようにして、このメソッド自体はすぐにtrueを返却するようにします。そして、ユーザが入力を確定した時点で、引数で指定されたIAuthCallbackインタフェースを使用して、入力されたユーザ名とパスワードをCefSharpに通知するようにします。

なお、「5. 操作対象についての注意」で説明したように、IWebBrowserではなくIBrowserを常に操作対象と考える必要があります。少し汚くなりますがPInvokeでWin32のGetParentを使って、IBrowserに関連したWebViewの親ウィンドウを取得します。この処理は今後も使うことになるため、以下のようなユーティリティ関数を作成します。

具体的には、以下のようになります。

IBrowserからTopLevelのフォームの取り出しメソッド
using System.Runtime.InteropServices;
namespace SimpleBrowser
{
    public partial class SimpleBrowserFrame : Form
    {
        // (省略)

        /// <summary>
        /// IBrowserからメインフレームを取得する
        /// </summary>
        /// <param name="browser"></param>
        /// <returns></returns>
        public static SimpleBrowserFrame getMainFrame(IBrowser browser)
        {
            // browserのウィンドウハンドル
            IntPtr hWnd = browser.GetHost().GetWindowHandle();

            // browserの親コントロール(webViewContainer)
            Control container = Control.FromHandle(GetParent(hWnd));

            if (container != null && container.TopLevelControl is SimpleBrowserFrame)
            {
                // コントロールのトップレベルのコントロールを取得して返却する
                return (SimpleBrowserFrame)container.TopLevelControl;
            }
            else
            {
                return null;
            }
        }

        // Win32のGetParent
        [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern IntPtr GetParent(IntPtr hWnd);
    }
}

このユーティリティメソッドを使用して、GetAuthCredentialsメソッドを実装します。

GetAuthCredentialsメソッドの実装
using System;
using System.Security.Cryptography.X509Certificates;
using System.Windows.Forms;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class RequestHandler : IRequestHandler
    {
        public bool GetAuthCredentials(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback)
        {
            // コントロールのトップレベルのコントロールを取得(SimpleBrowserFrame)
            SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(browser);

            if (mainFrame != null)
            {
                // 親コントロールのコンテキストで非同期にダイアログを表示する。
                mainFrame.BeginInvoke(new Action(() =>
                {
                    // ログオンダイアログを表示する
                    AuthDialog dlg = new AuthDialog();
                    DialogResult ret = dlg.ShowDialog();

                    if (ret == DialogResult.OK)
                    {
                        // 入力されたユーザ名とパスワードで認証を継続する。
                        callback.Continue(dlg.UserName, dlg.Password);
                    }
                    else
                    {
                        // 認証処理をキャンセルする。
                        callback.Cancel();
                    }
                }));
            }
            else
            {
                // 認証処理をキャンセルする。
                callback.Cancel();
            }

            return true;
        }

        public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
        {
            return null;
        }

        public bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect)
        {
            // ナビゲーションを許可する
            return false;
        }

        public bool OnCertificateError(IWebBrowser chromiumWebBrowser, IBrowser browser, CefErrorCode errorCode, string requestUrl, ISslInfo sslInfo, IRequestCallback callback)
        {
            // 証明書エラーでも継続する
            callback.Continue(true);
            return true;
        }

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

        public bool OnOpenUrlFromTab(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture)
        {
            // Ctrl+左クリックでの別Tab表示は許可しない。
            return false;
        }

        public void OnPluginCrashed(IWebBrowser chromiumWebBrowser, IBrowser browser, string pluginPath)
        {
        }

        public bool OnQuotaRequest(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, long newSize, IRequestCallback callback)
        {
            // クオータの要求は常に許可する。
            callback.Continue(true);
            return true;
        }

        public void OnRenderProcessTerminated(IWebBrowser chromiumWebBrowser, IBrowser browser, CefTerminationStatus status)
        {
        }

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

        public bool OnSelectClientCertificate(IWebBrowser chromiumWebBrowser, IBrowser browser, bool isProxy, string host, int port, X509Certificate2Collection certificates, ISelectClientCertificateCallback callback)
        {
            // デフォルトのクライアント証明書の選択方法とする。
            return false;
        }
    }
}

9.5 IRequestHandlerをChromiumWebBrowserに設定する

作成したIRequestHandlerインタフェースをChromiumWebBrowser.RequestHandlerプロパティに設定します。

ChromiumWebBrowserのIRequestHandlerを設定
public partial class SimpleBrowserFrame : Form
{
    // (省略)
    public SimpleBrowserFrame()
    {
        // いろいろと初期化処理
        //     :
        //     :

        // キーボードハンドラを設定する
        WebBrowser.KeyboardHandler = new Handlers.KeyboardHandler();

        // リクエストハンドラを設定する
        WebBrowser.RequestHandler= new Handlers.RequestHandler();
    }
}

9.6 認証の確認

認証が正しく実装できているか確認してみましょう。
BASIC認証を行う簡単なサーバを用意する必要があります。
ここでは、node.jsで動作するhttp-serverを使ってみます。

よく使うので、グローバルにインストールしておきます。

http-serverのインストール
npm install -g http-server

適当なディレクトリに移動して、以下のコマンドを実行すれば、BASIC認証を行うHTTPサーバが起動されます。
ポート番号を8081、ユーザ名をtest、パスワードをp@sswordとしています。

テスト用サーバの起動
cd c:\temp
http-server -p 8081 --user test --password p@ssword

これで、ポート8081でHTTPサーバが起動するので、さっそく確認してみましょう。

カスタムWebブラウザのアドレスバーにhttp://localhost:8081を入力してENTERを押下すると、作成した認証用ダイアログボックスが表示されます。正しいユーザ名とパスワードを入力するとページが表示されます。

その後、F5キーでページを更新すると、認証ダイアログボックスは表示されないことが確認できます。これは一度認証に使ったユーザ名とパスワードの情報をCefSharpの内部で保持していて、二回目以降は自動で認証処理を行ってくれるからです。

10. タイトルの表示

Webコンテンツが指定するタイトル文字列を表示します。
これを行うためには、IDisplayHandlerインターフェースを実装する必要があります。タイトル文字列が変更すると、CefSharpによってこのインタフェースのIDisplayHandler.OnTitleChangedメソッドが呼び出されます。

10.1 IDisplayHandlerを実装するクラスを追加

まず、IDisplayHandlerインタフェースを実装するクラスを新規に作成します。
リューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にDisplayHandlerを指定します。
そして、以下のようにIDisplayHandlerを実装させます。

IDisplayHandlerインタフェースを実装するクラス
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class DisplayHandler:IDisplayHandler
    {
    }
}

10.2 IDisplayHandlerのメソッドを仮実装

その後、IDisplayHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。
自動実装されたメンバーは例外をスローするようになっているので、以下の様にデフォルト実装します。

  • OnAddressChanged
    WebコンテンツのURLが変わったタイミングで呼び出されます。
    URLをロードすると何度かリダイレクトが発生する場合がありますが、通常はそのたびにアドレスバーに表示するURL文字列を更新することになります。これを実現するには、このメソッドが呼び出されるタイミングで、アドレスバーを更新します。
    11. アドレスバーの更新」でこの部分を実装しますので、ここでは特にないもしません。
  • OnAutoResize
    コンテンツがリサイズされたときに呼び出されるメソッドですが、falseを返却してブラウザのデフォルトの動作をさせます。
  • OnConsoleMessage
    javascriptでconsole.logconsole.error等のメソッドが呼び出されて、コンソールログ出力が行われるタイミングで呼び出されます。
    ここでは、開発者ツールのコンソールログに出力するデフォルトの動作を行うように、falseを返却します。
  • OnFaviconUrlChange
    FAVIONアイコンが変わるタイミングで呼び出されます。
    一般的な実装としては、引数で渡されたアイコンのURLでアイコンをダウンロードして表示します。
    ここでは、仮実装として特に何もしないようにします。
  • OnFullscreenModeChange
    WebコンテンツでFullscreen APIが呼び出されて、フルスクリーンモードが変わるタイミングで呼び出されます。
    Fullscreen APIをサポートする場合は、このタイミングでキオスクモードにするなど表示を変更することになります。
    ここでは、仮実装として特に何もしません。
  • OnLoadingProgressChange
    Webページロードの進捗率が変わるタイミングで呼び出されます。
    プログレスバーの進捗率を更新するなどのUIを実装することができます。
    ここでは、仮実装として特に何もしません。
  • OnStatusMessage
    Webコンテンツによりwindow.statusプロパティが変更されて、 ステータスバーのメッセージが変更されるタイミングで呼び出されます。ステータスバーを表示している場合は、ステータスバーに表示する文字列を変更することができます。
    ここでは、仮実装として特に何もしません。
  • OnTitleChanged
    Webコンテンツのtitle要素やdocument.titleでタイトル文字列が変更されるタイミングで呼び出されます。
    以下の「10.3 タイトル変更のハンドラを実装」でこのメソッドを実装します。
  • OnTooltipChanged
    ツールチップを表示するタイミングで呼び出されます。
    Winforms版では呼び出されないことになっていますが、ここではfalseを返却してデフォルトの処理を行うようにします。

10.3 タイトル変更のハンドラを実装

OnTitleChangedメソッドを実装します。
このメソッドの引数は、第一引数がIWebBrowserとなっていますが、実際にタイトル文字列が変更されたWebViewは第二引数のTitleChangedEventArgs.Browserプロパティで指定されるWebViewですので注意が必要です。
※「6.5 操作対象についての注意」で説明したように、第一引数のIWebBrowserと第二引数のプロパティで指定されるIBrowserが異なるWebViewを指している可能性があります。

まず、第二引数のプロパティで指定されるIBrowserから、タイトルを変更する対象のウィンドウを特定します。タブ形式のブラウザにしている場合は、IBrowserを表示しているタブの文字列を変更することになるでしょう。
今回はタブ形式のブラウザでないため、メインウィンドウのタイトルを変更対象とします。
これには、「9.4 GetAuthCredentialsメソッドの実装」で実装してユーティリティメソッドを使って、IBrowserからWebViewを表示しているTopLevelのフレームを取得します。

次に、対象ウィンドウのタイトルを変更しますが、IDisplayHandlerの各メソッドは、ブラウザのアプリケーションUIスレッドのコンテキストで呼び出されていないため、必ずメインウィンドウのコンテキストでタイトル文字列を変更されるようにします。

以下は、実装例になります。

IDisplayHandlerインタフェースの実装
using System;
using System.Collections.Generic;
using CefSharp;
using CefSharp.Structs;
using SimpleBrowser;

namespace SimpleBrowser.Handlers
{
    class DisplayHandler : IDisplayHandler
    {
        public void OnAddressChanged(IWebBrowser chromiumWebBrowser, AddressChangedEventArgs addressChangedArgs)
        {
        }

        public bool OnAutoResize(IWebBrowser chromiumWebBrowser, IBrowser browser, Size newSize)
        {
            // ブラウザのデフォルト処理を行う
            return false;
        }

        public bool OnConsoleMessage(IWebBrowser chromiumWebBrowser, ConsoleMessageEventArgs consoleMessageArgs)
        {
            // コンソールにメッセージを出力する
            return false;
        }

        public void OnFaviconUrlChange(IWebBrowser chromiumWebBrowser, IBrowser browser, IList<string> urls)
        {
        }

        public void OnFullscreenModeChange(IWebBrowser chromiumWebBrowser, IBrowser browser, bool fullscreen)
        {
        }

        public void OnLoadingProgressChange(IWebBrowser chromiumWebBrowser, IBrowser browser, double progress)
        {
        }

        public void OnStatusMessage(IWebBrowser chromiumWebBrowser, StatusMessageEventArgs statusMessageArgs)
        {
        }

        public void OnTitleChanged(IWebBrowser chromiumWebBrowser, TitleChangedEventArgs titleChangedArgs)
        {
            // コントロールのトップレベルのコントロールを取得(SimpleBrowserFrame)
            SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(titleChangedArgs.Browser);

            if (mainFrame != null)
            {
                // 親コントロールのコンテキストでタイトル文字列を変更する。
                mainFrame.BeginInvoke(new Action(() =>
                {
                    // タイトル文字列を変更する
                    mainFrame.Text = titleChangedArgs.Title;
                }));
            }
        }

        public bool OnTooltipChanged(IWebBrowser chromiumWebBrowser, ref string text)
        {
            return false;
        }
    }
}

10.4 IDisplayHandlerをChromiumWebBrowserに設定する

作成したIDisplayHandlerインタフェースをChromiumWebBrowser.DisplayHandlerプロパティに設定します。

ChromiumWebBrowserにIDisplayHandlerを設定
public partial class SimpleBrowserFrame : Form
{
    // (省略)
    public SimpleBrowserFrame()
    {
        // いろいろと初期化処理
        //     :

        // ディスプレイハンドラを設定する
        WebBrowser.DisplayHandler = new Handlers.DisplayHandler();
    }
}

以上でタイトル文字列がタイトルバーに表示されるようになりました。

11. アドレスバーの更新

WebコンテンツのURLが変わったタイミングで、OnAddressChangedメソッドがCefSharpから呼び出されるので、ここで受け取ったURL文字列をアドレスバーに表示します。これだけです。

10. タイトルの表示」でIDisplayHandlerインタフェースを実装しているので、OnAddressChangedメソッドの中身を実装していきます。

11.1 アドレスバーの文字列変更のメソッドを追加

まず、メインフレームのクラスSimpleBrowserFrameにアドレスバー変更のメソッドを追加します。

アドレスバー更新用メソッド
public void updateAddressBar(String url)
{
    this.addressBar.Text = url;
}

11.2 OnAddressChangedを実装

以下の様に、アドレスが変更になったIBrowserからSimpleBrowserFrameを取得して、アドレスバーの更新をします。
※今回はタブ表示でないためアドレスバーは1つしかないので、chromiumWebBrowserの親ウィンドウをたどってSimpleBrowserFrameを取得して操作するのと変わりませんが、複数タブを持つことを想定してこの実装にしています。

OnAddressChangedの実装
public void OnAddressChanged(IWebBrowser chromiumWebBrowser, AddressChangedEventArgs addressChangedArgs)
{
    // アドレスバーを持つフォームを取得する。
    SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(addressChangedArgs.Browser);
    if (mainFrame != null)
    {
        // UIスレッドのコンテキストで、アドレスバーの表示内容を更新する。
        mainFrame.BeginInvoke(new Action(() =>
        {
            mainFrame.updateAddressBar(addressChangedArgs.Address);
        }));
    }
}

以上でアドレスバーの文字列が更新されるようになりました。

12. コンソールログ

OnConsoleMessageメソッドは、Webコンテンツからconsole.logメソッドやconsole.errorメソッドでコンソールログを出力する時に呼びされるメソッドです。このメソッドを実装することで、コンソールログを保存するような処理を作ることができます。

12.1 Log4NetのNugetパッケージを追加

ログのライブラリは何を使ってもよいですが、ここでは人気にあるLog4netを使うことにします。

  1. ソリューションエクスプローラで「SimpleBrowser」を選択して、コンテキストメニューから「NuGetパッケージの管理」を選択します。
  2. NuGetパッケージマネージャで、「参照」タブを選択します。
  3. 検索ボックスに「Log4Net」と入力して検索を実行すると、Log4net関連のNugetパッケージが表示されます。 Nuget2.png
  4. Log4netのnugetパッケージをインストールします。

以上で、Log4Netのパッケージ取り込み完了です。

12.2 Log4netのコンフィグの追加

App.configの変更

App.configに以下の定義を追加します。

この定義は以下の意味です。

  • 実行ファイルと同じディレクトリのconsole.logファイルにログが記録される。
  • プログラムが何度再起動しても、同じログファイルに追加書き込みされる。
  • ログのフォーマットは、「日付時刻 スレッド番号 レベル メッセージ」の形式である。
App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net"/>
    </configSections>
    <log4net>
        <appender name="ConsoleLog" type="log4net.Appender.FileAppender">
            <File value=".\\console.log" />
            <AppendToFile value="true" />
            <layout type="log4net.Layout.PatternLayout">
                <ConversionPattern value="%date [%thread] [%-5level] - %message%n" />
            </layout>
        </appender>
        <root>
            <level value="ALL" />
            <appender-ref ref="ConsoleLog" />
        </root>
    </log4net>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
</configuration>

AssemblyInfo.csの変更

AssemblyInfo.csファイルに以下の定義を追加する。

AssemblyInfo.cs
[assembly: log4net.Config.XmlConfigurator(Watch = true)]

12.3 OnConsoleMessageメソッドの実装

OnConsoleMessageメソッドを実装して、Log4Netを使ってjavascriptのconsole.XXXメソッドの内容をログファイルに出力します。

具体的には、以下の様に実装します。

OnConsoleMessageメソッドの実装
using System;
using System.Collections.Generic;
using CefSharp;
using CefSharp.Structs;
using SimpleBrowser;
using log4net;

namespace SimpleBrowser.Handlers
{
    class DisplayHandler : IDisplayHandler
    {
        /// <summary>
        /// Logger
        /// </summary>
        private static ILog logger = LogManager.GetLogger("ConsoleLog");

        // 省略

        public bool OnConsoleMessage(IWebBrowser chromiumWebBrowser, ConsoleMessageEventArgs consoleMessageArgs)
        {
            // ログレベルに合わせてLog4Netを呼び出してログを保存する。
            switch (consoleMessageArgs.Level)
            {
                case LogSeverity.Error:
                    logger.Error(consoleMessageArgs.Message);
                    break;
                case LogSeverity.Fatal:
                    logger.Fatal(consoleMessageArgs.Message);
                    break;
                case LogSeverity.Info:
                    logger.Info(consoleMessageArgs.Message);
                    break;
                case LogSeverity.Warning:
                    logger.Warn(consoleMessageArgs.Message);
                    break;
                case LogSeverity.Verbose:
                    logger.Debug(consoleMessageArgs.Message);
                    break;
            }

            // コンソールにもメッセージを出力する
            return false;
        }
    }
}

OnConsoleMessageメソッドのドキュメントには、trueを返却するとコンソールにログを出力しなくなると書かれていますが、実際にはtrueを返却してもコンソールにログが出力されているようです(CefSharpのバージョンも関係するかもしれません)。

12.4 ログファイルの確認

プログラムを起動するとプログラムと同じディレクトリにconsole.logファイルができます。
中身を見ると、コンソールログが出力されていることが確認できます。

ログファイルの内容
2020-11-05 20:19:35,861 [4] [DEBUG] - Main._createAppUI: 16.732177734375 ms
2020-11-05 20:19:35,873 [4] [DEBUG] - Main._showAppUI: 11.036865234375 ms
2020-11-05 20:19:35,908 [4] [DEBUG] - Main._initializeTarget: 9.383056640625 ms
2020-11-05 20:19:36,051 [4] [DEBUG] - Main._lateInitialization: 0.58984375 ms
2020-11-05 20:19:52,115 [4] [INFO ] - abc

13. ダウンロード

ファイルのダウンロード機能を実装することができます。
download属性を持つa要素をクリックした場合など、ブラウザがダウンロードと判断した場合に、ファイルのダウンロードが行われます。

13.1 IDownloadHandlerを実装するクラスを追加

まず、IDownloadHandlerインタフェースを実装するクラスを新規に作成します。
リューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にDownloadHandlerを指定します。そして、以下のようにIDownloadHandlerを実装させます。

IDownloadHandlerを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class Download:IDownloadHandler
    {
    }
}

その後、IDownloadHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択する。
すると、IDownloadHandlerインタフェースの以下のメソッドが仮実装されます。

  • OnBeforeDownload
  • OnDownloadUpdated

13.2 OnBeforeDownloadの実装

OnBeforeDownloadメソッドは、ダウンロードするファイルの保存先やファイル名を決めるためのメソッドです。
プログラムで保存場所を決めることも、ユーザにファイルの保存先を問い合わせることができます。

CefSharpのドキュメントにはダウンロードが開始される前に呼び出されると書かれています。
しかし、実際にはdownload属性のあるa要素をクリックすると、即座にダウンロードが開始され、最初にOnDownloadUpdatedメソッドが呼び出されます。そして、次にこのメソッドが呼び出されますが、バックグラウンドではダウンロード処理が進行しています。

ここでは、ユーザに保存場所を問い合わせるようにします。
これを行うためには、引数で渡されるIBeforeDownloadCallbackインタフェースのContinueメソッドを呼び出します。

以下の例では、デフォルトのファイル保存ダイアログが表示されます。
※第二引数をtrueにするとCefSharpがファイル保存ダイアログを出してくれます。

OnBeforeDownloadの実装(その1)
void IDownloadHandler.OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
    // ファイル保存ダイアログを表示する。
    // 最初に表示される保存先とファイル名はCefSharpに任せる。
    callback.Continue("", true);
}

もちろん以下の様に書けば、ユーザに問い合わせることなく、自動的に指定したファイルに保存することもできます。

OnBeforeDownloadの実装(その2)
void IDownloadHandler.OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
    // c:\temp配下時自動的に保存する。
    callback.Continue(@"c:\temp\" + downloadItem.SuggestedFileName, false);
}

13.3 OnDownloadUpdatedの実装

OnDownloadUpdatedメソッドはダウンロードが開始すると最初に呼び出され、その後ダウンロードの進捗に合わせて繰り返し呼び出されます。
このメソッドでは、ダウンロードのキャンセル・中断・再開ができます。
また、ダウンロードの進捗率が通知されるので、ユーザに進捗状況を表示する処理も実装可能です。

  • ダウンロードを継続する場合
    特に何もしなければ、ダウンロードが継続されます。
  • ダウンロードをキャンセルする場合
    引数で指定されるIDownloadItemCallbackインタフェースのCancelメソッドを呼び出します。なお、ダウンロード処理はバックグラウンドで継続しているので、一度キャンセルを行っても即座に中断されずに何度かOnDownloadUpdatedメソッドが呼び出される場合があるので注意が必要です。
  • ダウンロードを中断する場合
    引数で指定されるIDownloadItemCallbackインタフェースのPauseメソッドを呼び出します。
  • ダウンロードを再開する場合
    引数で指定されるIDownloadItemCallbackインタフェースのResumeメソッドを呼び出します。

ここでは、キャンセルも中断も行わないため、特に何も実装しません。

13.4 IDownloadHandlerの指定

最後にChromiumWebBrowser.DownloadHandlerプロパティに、実装したIDownloadHandlerインタフェースを設定することで、CefSharpがこのインタフェースを呼び出すようになります。

具体的には、以下の様に指定します。

ChromiumWebBrowserにIDownloadHandlerを設定
public SimpleBrowserFrame()
{
    // (省略)
    WebBrowser.DownloadHandler = new Handlers.DownloadHandler();
}

13.5 ダウンロード処理の確認

例えばhttps://nodejs.org/en/にアクセスして、node.jsのインストーラをダウンロードしてみましょう。
正しく実装できていると「Save File」ダイアログが表示され、保存ボタンをクリックすると、指定した場所にファイルが保存されることが確認できます。

14. faviconの表示

faviconはHTMLで指定されているサイトのアイコンです。
ここでは、faviconの表示方法を説明します。

14.1 faviconの仕様

faviconの仕様は少しゆるい仕様で、faviconの表示を厳密に実装するのは少し大変です。

指定できるイメージの形式は、W3CではPNG、GIF、ICOのいずれかとしていますが、実際にはJPEGやSVGもサポートしているブラウザも多いです。また、type属性でのファイルタイプの指定は強制ではないため、type属性を指定していないサイトも多くあります。また、複数のfaviconを設定することもできます。

以下はHTMLでのfaviconの指定例です。

HTMLでのfaviconの指定例
<!-- イメージの型を指定しない -->
<link rel="shortcut icon" href="https://example.com/myicon.ico">

<!-- イメージの型を指定する -->
<link rel="icon" type="image/gif" href="https://example.com/image.gif">

以上の様にfaviconを扱うのは少し面倒ですが、ここでは以下の方針とします。

  • どのファイルタイプまでサポートするのか?
    PNG、GIF、ICOをサポートする様にします。
  • イメージ形式か分からない場合に、どのように表示するのか?
    イメージの形式を判断するために、サーバが指定するMimeTypeをを使用するようにします。
    なお、type属性が指定されていても、CefSharpではその属性値を受け取ることができません。

14.2 faviconの表示処理を追加

まず、フォームのアイコンをfaviconに変更する処理を追加します。
以下の様にfaviconファイルのパスとMimeTypeを引数に持つメソッドを追加して、指定されたファイルパスのfaviconをフォームのアイコンとして表示します。

favicon更新処理の例
public partial class SimpleBrowserFrame : Form
{
    // (省略)

    public void updateFavicon(String path, String mimeType)
    {
        try
        {
            // MIME-Typeに応じて、Iconオブジェクトを作成する。
            Icon icon = null;
            switch (mimeType.ToLower())
            {
                // pngとgifの場合
                case "image/png":
                case "image/gif":
                    Bitmap bitmap = new Bitmap(Image.FromFile(path));
                    icon = Icon.FromHandle(bitmap.GetHicon());
                    break;
                // iconの場合
                case "image/vnd.microsoft.icon":
                case "image/x-icon":
                    icon = Icon.ExtractAssociatedIcon(path);
                    break;
                default:
                    break;
            }

            // ファビコンを表示する
            if (icon != null)
            {
                this.Icon = icon;
            }

            // FVICONファイルを削除する。
            System.IO.File.Delete(path);
        }
        catch
        {
        }
    }

    // (省略)
}

14.3 faviconのダウンロード

CefSharpではfaviconが変わるタイミングで、IDisplayHandler.OnFaviconUrlChangeメソッドが呼び出されます。
しかし、faviconの取得はアプリ開発者に任されています。
faviconを取得するには、IBrowserHost.StartDownloadメソッドでダウンロードします。
※このメソッドを使うと任意のファイルをダウンロードすることができます。

ダウンロードが行われると、「13. ダウンロード」で説明したように、IDownloadHandlerインタフェースのメソッドが呼び出されることになります。
このインタフェースはユーザがファイルのダウンロードを行う際にも呼び出されるため、ユーザがダウンロードしたファイルなのか、faviconとしてプログラムでダウンロードしたものなのかを判断する必要がでてきます。
このため、ダウンロード中のfaviconを管理するリストをもうけます。

ダウンロード中のfaviconのリストを作成

ダウンロード中のfaviconを管理するリストを以下の様に作成します。

ダウンロード中のfaviconのリスト
public partial class SimpleBrowserFrame : Form
{
    // (省略)

    /// <summary>
    /// ダウンロード中のfaviconのリスト
    /// </summary>
    public List<String> Favicons { get; } = new List<string>();

    // (省略)
}

faviconをダウンロードする

ダウンロードする前にダウンロード中のfaviconリストにfaviconのURLを追加して、その後faviconをダウンロードします。

OnFaviconUrlChangeメソッドの実装
public void OnFaviconUrlChange(IWebBrowser chromiumWebBrowser, IBrowser browser, IList<string> urls)
{
    // コントロールのトップレベルのコントロールを取得(SimpleBrowserFrame)
    SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(browser);
    if (mainFrame != null)
    {
        // ロード中のFaviconを登録する
        mainFrame.Favicons.Add(urls[0]);

        // Faviconをロードする。
        browser.GetHost().StartDownload(urls[0]);
    }
}

14.4 OnBeforeDownloadの変更

13. ダウンロード」で実装したOnBeforeDownloadメソッドを、適当なパスにfaviconを自動保存するように変更します。

以下の様に、ダウンロードしている対象のURLがダウンロード中のfaviconのリストに含まれている場合は、ランダムなファイル名で保存するように変更します。それ以外のURLの場合は、これまで通りファイル保存ダイアログを表示して、保存場所をユーザに問い合わせるようにします。

OnBeforeDownloadの実装
void IDownloadHandler.OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
    // コントロールのトップレベルのコントロールを取得(SimpleBrowserFrame)
    SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(browser);

    // faviconの場合は、ランダムなファイル名で保存
    if (mainFrame != null && mainFrame.Favicons.Contains(downloadItem.Url))
    {
        callback.Continue($"c:\\temp\\{Path.GetRandomFileName()}", false);
    }
    // それ以外の場合は、ファイル保存ダイアログを表示して保存する。
    else
    {
        callback.Continue("", true);
    }
}

14.5 OnDownloadUpdatedの変更

13. ダウンロード」で実装したOnDownloadUpdatedメソッドで、faviconのダウンロードが完了した時にフォームのアイコンを変更するように変更します。

以下の様に、ファイルのダウンロードが完了した場合で、且つダウンロードした対象がfaviconの場合に、フォームのアイコンを更新するようにします。

OnDownloadUpdatedの実装
void IDownloadHandler.OnDownloadUpdated(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback)
{
    // ダウンロードが完了した場合
    if (downloadItem.IsComplete)
    {
        // コントロールのトップレベルのコントロールを取得(SimpleBrowserFrame)
        SimpleBrowserFrame mainFrame = SimpleBrowserFrame.getMainFrame(browser);

        // faviconの場合
        if (mainFrame != null && mainFrame.Favicons.Contains(downloadItem.Url))
        {
            // ロード中のfaviconファイルのリストから取り除く
            mainFrame.Favicons.Remove(downloadItem.Url);

            // フォームのアイコンを更新する。
            mainFrame.BeginInvoke(new Action(() =>
            {
                mainFrame.updateFavicon(downloadItem.FullPath, downloadItem.MimeType);
            }));
        }
    }
}

14.6 faviconの確認

以上でfaviconが表示されるようになったはずです。
favicon.png
おー、faviconが表示されています。

15. pdf表示

特に何もコードを書かなくても、pdfをロードすると、chromeのエクステンションによってPDFが表示されます。

16. 特定サイトのブロック

特定のサイトのナビゲーションをブロックするには、IRequestHandlerインタフェースのOnBeforeBrowseメソッドでtrueを返却するだけです。このメソッドはフレームにURLがロードされることをブロックします。Webサイトの個々のリソース(image、javascript、css、ajaxで取得されるリソース等)のロードを個別にブロックする場合、IResourceRequestHandler.OnBeforeResourceLoadメソッドでブロックすることができます。

ここでは、https://www.yahoo.co.jp/へのナビゲーションを禁止してみます。

16.1 OnBeforeBrowseメソッドの実装

OnBeforeBrowseメソッドを実装します。
実装は非常に簡単です。

  • trueを返却すると、ナビゲーションが中止されます。
  • falseを返却すると、ナビゲーションが継続します。

以下は実装例です。

OnBeforeBrowseメソッドの実装
class RequestHandler : IRequestHandler
{
    // (省略)

    public bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect)
    {
        if (request.Url.StartsWith("https://www.yahoo.co.jp/"))
        {
            // ナビゲーションを中止する
            return true;
        }
        else
        {
            // ナビゲーションを許可する
            return false;
        }
    }
}

16.2 表示がブロックされるか確認

ビルドしてブラウザを起動して、アドレスバーにhttps://www.yahoo.co.jp/を入力してみます。
正しく実装できていると、ページがロードされないはずです。

以上、非常に簡単だったと思います。

17. Cookie

Cookieは保存と送信をそれぞれ許可またはブロックすることができます。
ここでは、Cookieの保存と送信の制御の方法を説明します。

17.1 ICookieAccessFilterインタフェースを実装するクラスを追加

まず、クッキーをコントロールするために、ICookieAccessFilterインタフェースを実装するクラスを新たに追加します。
ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にCookieAccessFilterを指定します。そして、以下のようにICookieAccessFilterを実装させます。

ICookieAccessFilterを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class CookieAccessFilter: ICookieAccessFilter
    {
    }
}

その後、ICookieAccessFilterを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとICookieAccessFilterインタフェースのメソッドがすべて仮実装されます。

17.2 CanSaveCookieメソッドの実装

CanSaveCookieメソッドは、レスポンスヘッダにSet-Cookieが設定されている場合に、cookieを保存するかどうかを判断するためにリソース毎に呼び出されます。
このメソッドではcookieを受け入れるかどうかを返却値で決定します。

  • trueを返却すると、cookieが保存されます。
  • falseを返却すると、cookieが保存されません。

ここでは、すべてのサイトでCookieを保存しないようにしてみます。

CanSaveCookieメソッドの実装
bool ICookieAccessFilter.CanSaveCookie(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, Cookie cookie)
{
    // cookieを保存しないようにする。
    return false;
}

17.3 CanSendCookieメソッドの実装

CanSendCookieメソッドは、リソース毎にリクエストヘッダにCookieを設定するかどうか判断するために呼び出されます。
このメソッドではcookieを送信するかどうかを返却値で決定します。

  • trueを返却すると、cookieが送信されます。
  • falseを返却すると、cookieが送信されません。

ここでは、すべてのサイトでCookieを送信しないようにしてみます。

CanSendCookieメソッドの実装
bool ICookieAccessFilter.CanSendCookie(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, Cookie cookie)
{
    // cookieを送信しないようにする。
    return false;
}

17.3 IResourceRequestHandlerインタフェースを実装するクラスの追加

先ほど作成したICookieAccessFilterインタフェースを実装したクラスを利用するために、IResourceRequestHandlerインタフェースを実装するクラスが必要になります。以下の流れで新しいクラスを追加します。

ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にResourceRequestHandlerを指定します。そして、以下のようにIResourceRequestHandlerを実装させます。

IResourceRequestHandlerを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class ResourceRequestHandler : IResourceRequestHandler
    {
    }
}

その後、IResourceRequestHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとIResourceRequestHandlerインタフェースのメソッドがすべて仮実装されます。

17.4 IResourceRequestHandlerインタフェースのメソッドを仮実装

  • GetCookieAccessFilter
    独自のCookieの制御を行う場合はICookieAccessFilterを返却します。
    今回はCookieを制御するため、先程作成したCookieAccessFilterクラスのインスタンスを返却するようにします。

  • GetResourceHandler
    独自のリソースハンドラを返却します。
    ここでは独自のリソースハンドラは実装しないためnullを返却します。

  • GetResourceResponseFilter
    独自のリソースフィルタを返却します。
    リソースフィルタはサーバからのレスポンスを変更するための機能で非常に便利です。
    ここでは独自のリソースフィルタを実装しないためnullを返却します。

  • OnBeforeResourceLoad
    リソースがロードする前に呼び出されるメソッドです。
    リソースのロードをブロックしたい場合は、このメソッドを実装します。
    ここではリソースのロードをブロックしないため、CefReturnValue.Continueをリターンします。

  • OnProtocolExecution
    不明なプロトコルを持つURLが処理されるときに呼び出されます。
    OSで登録されているプロトコルの呼び出しを試みる場合はtrueを返却します。
    ここでは何もしないようにfalseを返却します。

  • OnResourceLoadComplete
    リソースのロードが完了した時に呼び出されます。
    ここでは特に何もしません。

  • OnResourceRedirect
    リソースがリダイレクトされるときに呼び出されます。
    ここでは特に何もしません。

  • OnResourceResponse
    リソースのレスポンスを受信したときに呼び出されます。
    リソースをリダイレクトさせることができるようですが、この機能は将来廃止されるようです。
    ここでは特に処理を行わないためfalseを返却します。

以下に実装例を示します。

IResourceRequestHandlerインタフェースの実装
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class ResourceRequestHandler : IResourceRequestHandler
    {
        void IDisposable.Dispose()
        {
        }

        ICookieAccessFilter IResourceRequestHandler.GetCookieAccessFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request)
        {
            // 独自のCookieの制御を行います。
            return new CookieAccessFilter();
        }

        IResourceHandler IResourceRequestHandler.GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request)
        {
            return null;
        }

        IResponseFilter IResourceRequestHandler.GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
        {
            return null;
        }

        CefReturnValue IResourceRequestHandler.OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
        {
            return CefReturnValue.Continue;
        }

        bool IResourceRequestHandler.OnProtocolExecution(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request)
        {
            return false;
        }

        void IResourceRequestHandler.OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)
        {
        }

        void IResourceRequestHandler.OnResourceRedirect(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, ref string newUrl)
        {
        }

        bool IResourceRequestHandler.OnResourceResponse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
        {
            return false;
        }
    }
}

17.5 IRequestHandler.GetResourceRequestHandlerメソッドを実装

作成したIResourceRequestHandlerインタフェースを実装したクラスが使われるように、IRequestHandler.GetResourceRequestHandlerメソッドを実装します。

以下の様に、GetResourceRequestHandlerメソッドでIResourceRequestHandlerインタフェースを返却する様にします。

GetResourceRequestHandlerメソッドを実装
class RequestHandler : IRequestHandler
{
    // (省略)

    public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
    {
        // 独自のIResourceRequestHandlerを返却する。
        return new ResourceRequestHandler();
    }
}

17.6 Cookieがブロックされることを確認

以上の実装で全てのCookieがブロックされるようになりました。
確認のために、開発者ツールを起動してみます。

  1. ブラウザを起動する.
  2. F12キーを押下して開発者ツールを立ち上げる。
  3. Applicationを選択して「Clear Site Data」ボタンをクリックしてCookieを削除する。
    Cookie削除.png

  4. F5でページをリロードする。

  5. レスポンスを確認する。
    開発者ツールのnetworkを選択して、リクエストの内容を確認してみるとCookieがないことが確認できます。
    つまり、Cookieは送信されていません。
    response1.png

  6. Cookieを確認する。
    ApplicationのCookieを選択すると、以下の様にCookieが保存されていないことが確認できます。
    つまり、Cookieは保存されていません。
    cookie.png

以上でCookieを保存と送信が制御できることが確認できました。

18. スクリプト実行

表示しているWebページで任意のスクリプトを実行することができます。
例えば、ページのロードが完了した時点で、bodyの背景色を赤色にするスクリプトを実行することができます。
ここでは、まず初めにWebページのロード完了をハンドリングするために、ILoadHandlerインタフェースを実装します。
次に、ロード完了時にスクリプトを実行して、bodyの背景色を設定してみます。

18.1 ILoadHandlerを実装するクラスを追加

まず、ILoadHandlerインタフェースを実装するクラスを新規に作成します。
リューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にLoadHandlerを指定します。そして、以下のようにILoadHandlerインタフェースを実装させます。

ILoadHandlerを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class LoadHandler: ILoadHandler
    {
    }
}

その後、ILoadHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとILoadHandlerインタフェースのメソッドがすべて仮実装されます。

18.2 ILoadHandlerのメソッドを仮実装

ILoadHandlerインタフェースの各メソッドを仮実装します。

18.3 ILoadHandler.OnLoadingStateChangeメソッドの実装

引数で渡されるLoadingStateChangedEventArgsIsLoadingプロパティを参照することでロードが完了したかどうかを判断することができます。そして、ロードが完了していれば、スクリプトを実行することにします。
スクリプトを実行するためには、IFrame.EvaluateScriptAsyncを呼び出します。

以下は実装例です。

OnLoadingStateChangeメソッドでスクリプトを実行する例
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class LoadHandler : ILoadHandler
    {
        void ILoadHandler.OnFrameLoadEnd(IWebBrowser chromiumWebBrowser, FrameLoadEndEventArgs frameLoadEndArgs)
        {
        }

        void ILoadHandler.OnFrameLoadStart(IWebBrowser chromiumWebBrowser, FrameLoadStartEventArgs frameLoadStartArgs)
        {
        }

        void ILoadHandler.OnLoadError(IWebBrowser chromiumWebBrowser, LoadErrorEventArgs loadErrorArgs)
        {
        }

        void ILoadHandler.OnLoadingStateChange(IWebBrowser chromiumWebBrowser, LoadingStateChangedEventArgs loadingStateChangedArgs)
        {
            // ロードが完了した場合
            if (!loadingStateChangedArgs.IsLoading)
            {
                // Javascriptを実行する。
                loadingStateChangedArgs.Browser.MainFrame.EvaluateScriptAsync(@"document.body.setAttribute('style', 'background-color:red;')");
            }
        }
    }
}

18.4 ILoadHandlerの指定

最後にChromiumWebBrowser.LoadHandlerプロパティに、実装したILoadHandlerインタフェースを設定することで、CefSharpがこのインタフェースを呼び出すようになります。

具体的には、以下の様に指定します。

ChromiumWebBrowserにILoadHandlerインタフェースを設定
public SimpleBrowserFrame()
{
    // (省略)
    WebBrowser.LoadHandler= new Handlers.LoadHandler();
}

18.5 スクリプト実行の確認

ビルドして実行してみます。
正しく実装できるていると、以下の様にbodyの背景色が赤色になっているはずです。
スクリプト実行.png

18.6 スクリプトの実行して結果を受け取る

先ほどの例ではbodyの背景色を変更するスクリプトを実行するだけですが、Webページでスクリプトを実行して、その結果を受け取ることもできます。例えば、Webページのタイトル文字列を取得するスクリプトを実行する場合は以下の様に書きます。

スクリプトを実行して結果を受け取る方法
loadingStateChangedArgs.Browser.MainFrame.EvaluateScriptAsync(@"(()=>{return document.title;})();").ContinueWith((reponse) => {
  String title = (String)reponse.Result.Result;
  MessageBox.Show($"document.title = {title}");
});

19. 独自オブジェクトの公開

任意のクラスを作成してjavascriptから呼び出せるようにできます。
ここでは、任意のコマンドを実行するクラスを作成して、javascriptから呼び出せるようにしてみます。

19.1 公開するクラスの作成

任意のクラスを公開することができますが、以下の注意が必要です。

  • 公開されるすべてのメソッドはPromiseを返すメソッドとしてjavascriptに公開されます。
    つまり、javascript側から呼び出されると、即座にPromiseが返却されて、メソッドの実行結果をfullfill時に受け取ることになります。
  • メソッドを実行するスレッドは一つだけ。
    時間のかかる処理を行っても構いませんが、独自オブジェクトのメソッドを実行するスレッドは1つだけなので、javascript側から複数回呼びされると、キューイングされて実行されることになります。この場合でもjavascript側には呼び出すとすぐにPromiseが返却されます。 したがって、時間のかかる処理を行う場合は、明示的に別スレッドで実行して、次に示すコールバックを呼び出す方法をお勧めします。
  • マーシャリングの仕様が明確でない。
    intStringようなprimitve型を引数として渡すのは問題ありません。
    int[]のようなprimitve型の配列を返却する場合も問題なく動作します。
    しかし、int[]を引数で受け受け取ろうとするとうまく受け取れません。
    このため原則的にprimitive型だけを使う様にするべきです。
  • 非完了型の場合はコールバックを利用する。
    C#のクラスのメソッドは、javascript側が指定するコールバックを、IJavascriptCallback型の引数で受け取ることができます。
    時間のかかる非完了処理では、完了時のこのコールバックを呼び出す方法が良いです。

callback無しですぐに完了するメソッドと完了時にcallbackを呼び出すメソッドの例を以下に示します。

javascriptに公開するオブジェクト
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using CefSharp;

namespace SimpleBrowser
{
    class Test
    {
        // callbackを使わない例
        public String exec(String exePath, String arguments = null)
        {
            // プロセスを起動する
            Process ps = Process.Start(exePath, arguments);
            // 文字列を返却する。
            return "Hello!";
        }

        // javascript側のcallbackを呼び出す例
        public void wait(int millisecond, IJavascriptCallback callback)
        {
            // 別スレッドで処理を実行する。
            Task.Delay(millisecond).ContinueWith((o) =>
            {
                // callbackメソッドが呼び出せるか必ず確認する
                if (callback.CanExecute)
                {
                    // コールバックを呼び出す。
                    // c#->javascriptはint配列のマーシャリングがうまく動作する
                    callback.ExecuteAsync(new int[5] { 1, 2, 3, 4, 5 });
                }
            });
        }
    }
}

19.2 作成したクラスのインスタンスを公開する

作成した独自クラスのインスタンスをjavascriptから利用できるように公開します。
これには、IWebBrowser インタフェースのJavascriptObjectRepositoryプロパティで公開されているIJavascriptObjectRepositoryインタフェースのRegisteメソッドを呼び出します。
IWebBrowserに対して操作していますが、ここで登録した独自オブジェクトは、子ウィンドウでも参照可能になります。

  • 第一引数はjavascript側に公開するオブジェクト名前(大文字小文字が区別されるので注意)
  • 第二引数は公開するオブジェト(ChromiumWebBrowser単位で1つのオブジェクトであることに注意)
  • 第三引数は非同期にするかどうかであるが、ブロッキングされないように、非同期型にすることをお勧めします。

以下の例ではTestという名称で、先ほどのTestクラスのオブジェクトを公開しています。

独自オブジェクトの公開
public SimpleBrowserFrame()
{
    InitializeComponent();
    WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

    // (省略)

    // 独自オブジェクトをTestという名称で公開する
    WebBrowser.JavascriptObjectRepository.Register("Test", new SimpleBrowser.Test(), true, null);
}

19.3 公開されたオブジェクトを使用する

javascriptでは、CefSharp.BindObjectAsyncメソッドを呼び出すことで、公開されたオブジェクトが使えるようになります。

このメソッドは非同期型(Promiseを返却する)ですので、完了を待つ必要があります。

公開されたオブジェクトをjavascriptから参照可能にする方法
CefSharp.BindObjectAsync("Test");

19.4 確認してみる

ビルド&実行して、F12キーで開発者ツールを起動します。
次にコンソールを選択して、以下を入力して実行します。
これで、Testオブジェクトが参照可能になります。

公開されたオブジェクトをjavascriptから参照可能にする方法
CefSharp.BindObjectAsync("Test");

続いて、以下をコンソールに入力して実行します。
すると、電卓が起動することが確認できます。

公開されたオブジェクトのメソッドを実行
Test.exec("calc.exe");

続いて、callbackの確認として、以下をコンソールに入力して実行します。
すると、10秒後にコンソールにint型の配列が出力されます。

公開されたオブジェクトのメソッド(callbackあり)を実行
Test.wait(10000, (ret)=>{console.log(ret);});

このようにC#のオブジェクトをjavascriptに公開する機能は、CefSharpの非常に便利な機能です。

20. レスポンス内容の変更

CefSharpではHTTPのレスポンス内容をカスタマイズすることができます。
※レスポンスヘッダは変更することはできません。
この機能を使うことで、任意のスクリプトをレスポンスに埋め込むことなどができます。

CefSharpは1つのリソースを受信する毎にIResourceRequestHandlerインタフェースのGetResourceResponseFilterメソッドを呼び出します。レスポンス内容のカスタマイズが必要な場合には、IResponseFilterインタフェースを返却するようにします。
そして、返却したIResponseFilterでレスポンスの内容を変更します。

20.1 IResponseFilterを実装するクラスを追加

まず、IResponseFilterインタフェースを実装するクラスを新規に作成します。
ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にResponseFilterを指定します。そして、以下のようにIResponseFilterを実装させます。

IResponseFilterを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class ResponseFilter: IResponseFilter
    {
    }
}

その後、IResponseFilterを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとIResponseFilterインタフェースのメソッドがすべて仮実装されます。

20.2 IResponseFilterのメソッドを実装

IResponseFilterインタフェースの各メソッドを実装します。

  • コンストラクタ
    コンストラクタは、ResponseFilterのインスタンスを作成するためにGetResourceResponseFilterメソッドが呼び出すことになります。このメソッドではレスポンスヘッダを参照することができるため、レスポンスの文字コードを取得することができます。
    文字コードは後の処理で使用するために、コンストラクタの引数でを受け取るようにします。

  • Filter
    このメソッドでレスポンス内容を変更しますが、バイト配列を操作する必要がるため、実装が少し複雑になります。
    1つのリソースのレスポンスは分割されて受信する場合があるため、このメソッドは複数回呼び出されることになります。
    基本的には、以下の流れになります。

    1. 第一引数Streamで受けたオリジナルのレスポンス内容を読み取る。
    2. 内容を自由に変更する。
    3. 第三引数のStreamに変更した内容を書き込む。
    4. 変更した内容を書き込む際に第三引数のStreamのサイズが不足する場合は、次回のFilterメソッド呼び出し時に書き込めるように書き込めなかった内容を保存します。
  • InitFilter
    IResponseFilterを初期化するために、一回だけ(1つのリソースの処理で一回だけ)呼びだされます。
    falseを返却するとIResponseFilterが使用されなくなります。
    ここでは常にtrueを返却してIResponseFilterが使われるようにします。

  • Dispose
    解放が必要なリソースは特にありませんので、何も行いません。

以下の例では、最初に現れた<script>タグの前に、以下のスクリプトを埋め込むことにします。

埋め込むスクリプト
<script>console.log("injected");</script>

少し、複雑ですが参考にしてください。

IResponseFilterの実装
using System;
using System.Linq;
using System.IO;
using System.Text;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class ResponseFilter : IResponseFilter
    {
        /// <summary>
        /// 埋め込み位置の文字列
        /// ここでは、最初に現れる"<script"の直前に埋め込む。
        /// </summary>
        private static readonly String SEARCH_TARGET = "<script";

        /// <summary>
        /// 埋め込むコード
        /// </summary>
        private static readonly String INJECTED_CODE = "<script>console.log('injected');</script>";

        /// <summary>
        /// 出力ストリームに未書き込みのデータ
        /// </summary>
        private byte[] remainData = new byte[0];

        /// <summary>
        /// コード埋め込み済みのフラグ
        /// </summary>
        private bool bInjected = false;

        /// <summary>
        /// レスポンスで指定されているエンコーディング
        /// </summary>
        private Encoding encoding = Encoding.UTF8;


        public ResponseFilter(String charset = null)
        {
            if (!String.IsNullOrEmpty(charset))
            {
                try
                {
                    // responseの文字コードからエンコーディングを作成
                    this.encoding = Encoding.GetEncoding(charset);
                }
                catch
                {
                    // 不正な文字コード指定の場合はUTF8とする。
                    this.encoding = Encoding.UTF8;
                }
            }
        }

        void IDisposable.Dispose()
        {
        }

        FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten)
        {
            if (dataIn != null)
            {
                // 書き出すデータ
                byte[] modifiedContent;

                // 入力ストリームのサイズ
                long lDataSize = dataIn.Length;

                // 出力トリームのサイズ
                long lCapacity = dataOut.Length;

                // 入力ストリームから全て読み込む
                byte[] buffer = new byte[lDataSize];
                dataIn.Read(buffer, 0, (int)lDataSize);

                // 読み取ったサイズを返却
                dataInRead = lDataSize;

                // 1つのリソースに一度だけ埋め込むため、埋め込み済みフラグを確認する。
                if (!bInjected) 
                {
                    // 検索する文字列をバイト配列に変換する
                    byte[] pattern = encoding.GetBytes(SEARCH_TARGET);

                    // コードを埋め込む位置を探す
                    int injectionPoint = 0;
                    for (int i = 0; i < lDataSize - pattern.Length; ++i)
                    {
                        // 埋め込む位置のパターンと一致するかチェックする
                        bool bMatch = true;
                        for (int j = 0; j < pattern.Length; ++ j)
                        {
                            if (pattern[j] != buffer[i + j])
                            {
                                bMatch = false;
                                break;
                            }
                        }

                        // 埋め込む位置が特定できた場合
                        if (bMatch)
                        {
                            injectionPoint = i;
                            break;
                        }
                    }

                    // コードを埋め込む位置が見つかった
                    if (injectionPoint != 0)
                    {
                        // 埋め込んだことをマークする。
                        bInjected = true;

                        // 入力データをコード埋め込み位置で2つに分割する。
                        byte[] firstPart = buffer.Take(injectionPoint).ToArray();
                        byte[] secondPart = buffer.Skip(injectionPoint).ToArray();

                        // 出力ストリームに書き込むデータを作成する。
                        // 前回の書き込めなかったデータ+入力データの前半+埋め込むデータ+入力データの後半
                        byte[] code = encoding.GetBytes(INJECTED_CODE);
                        modifiedContent = remainData.Concat(firstPart).Concat(code).Concat(secondPart).ToArray();
                    }
                    // コードを埋め込む位置が見つからなかった
                    else
                    {
                        // 入力データを書き込むデータとする。
                        modifiedContent = buffer;
                    }
                }
                else
                {
                    // 入力データを書き込むデータとする。
                    modifiedContent = buffer;
                }

                // 出力ストリームに編集したコンテンツを書き出す。
                // ※出力ストリームのサイズ以上は書き込めない。
                dataOutWritten = Math.Min(lCapacity, modifiedContent.Length);
                dataOut.Write(modifiedContent, 0, (int)dataOutWritten);

                // 書き込むデータが出力ストリームのサイズよりも場合
                if (modifiedContent.Length > lCapacity)
                {
                    // 書き込むない部分を、次回書き込むデータとして保存しておく
                    remainData = modifiedContent.Skip((int)lCapacity).ToArray();

                    // バッファ不足であることを返却する
                    return FilterStatus.NeedMoreData;
                }
                // 全てのデータを出力ストリームに書き込めた場合
                else
                {
                    // 全て書き込めているので、未書き込みデータは空にする。
                    remainData = new byte[0];
                    // 処理が完了したことを返却する。
                    return FilterStatus.Done;
                }
            }
            else
            {
                // 入力データがないため、何も書き込んでいないが、処理は完了とする。
                dataInRead = 0;
                dataOutWritten = 0;
                return FilterStatus.Done;
            }
        }

        bool IResponseFilter.InitFilter()
        {
            return true;
        }
    }
}

20.3 IResourceRequestHandler.GetResourceResponseFilterメソッドの実装

IResourceRequestHandler.GetResourceResponseFilterメソッドは1つのHTTPレスポンスを受信する度に呼び出されます。HTMLだけでなく、CSS、Javascript、PDF、イメージ等のすべてのリソースのHTTPレスポンスで呼び出されます。また、開発者ツールなどのカスタムスキーマ(devtools://など)の場合でも、このメソッドが呼び出されます。
したがって、レスポンス内容を変更する対象のリソースを絞る必要があります。

例えば、以下の条件のレスポンスだけを編集対象とします。

  • メインフレームかサブフレーム(iframeとframe)にロードされるリソースだけを対象する。
  • http://とhttps://のスキマーだけを対象とする。

ほかにもMimeTypeを見る方法もあります。
以下は実装例です。

GetResourceResponseFilterメソッドの実装
IResponseFilter IResourceRequestHandler.GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{
    // メインフレームとサブフレームのリソースの場合だけリソースを改変する。
    // 注意:pdfや開発者ツールの場合には、フィルターが適用されないように、httpとhttpsプロトコルだけに限定する。
    if ((request.ResourceType == ResourceType.MainFrame || request.ResourceType == ResourceType.SubFrame) &&
        Regex.IsMatch(request.Url, "^(http|https)://.*$", RegexOptions.IgnoreCase))
    {
        // レスポンス内容を変更する。
        return new ResponseFilter(response.Charset);
    }
    // レスポンス内容は変更しない。
    return null;
}

20.4 IResponseFilterの確認

ビルドして実行して開発者ツールを立ち上げてみます。
すると、コンソールにinjectedと出力されていることが確認できます。
また、Elementを確認すると、スクリプト要素が埋め込まれていることが確認できます。

injection.png

なお、上で実装したコードでは、最初のscriptとして追加されるようにしていますが、このように最初のscriptにならない場合があります。
これは、変更したWebコンテンツがブラウザにロードされた後に、内部のスクリプトによって追加のスクリプトがロードされてるためだと考えられます。

また、Content-Security-Policy(CSP)を突破することはできないため、Content-Security-Policyscript-srcの設定によっては、このように直接埋め込んだスクリプトは実行されないことにも注意が必要です。
CSPを突破するためには、IsCSPBypassingでCSPをバイパスする指定のカスタムスキーマ作成してスクリプトをロード必要があります。カスタムスキーマについては次章で説明します。

21. カスタムスキーマ

カスタムスキーマとは、chromeでのchrome://versionのようなURLでのchrome://の部分のことです。http://https://以外に独自のスキーマを提供することができます。
また、カスタムスキーマはContent-Security-Policy(CSP)を突破することができるため、「# 20. レスポンス内容の変更」で説明したレスポンスでscriptsrcにカスタムスキーマのリソースを埋め込むと、任意のコードを実行できるようになります。
他にも通常のスキーマではできない、特殊なことを行うスキーマを提供することができます。

ここでは、ローカルの任意のファイルの中身を取り出すスキーマを実装してみます。

21.1 ISchemeHandlerFactoryインタフェースを実装するクラスの追加

ISchemeHandlerFactoryインタフェースを実装するクラスを新規に追加します。

ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にLocalFileSchemeHandlerFactoryを指定します。そして、以下のようにISchemeHandlerFactoryインタフェースを実装させます。

ISchemeHandlerFactoryを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class LocalFileSchemeHandlerFactory: ISchemeHandlerFactory
    {
    }
}

その後、ISchemeHandlerFactoryを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとISchemeHandlerFactoryインタフェースのメソッドがすべて仮実装されます。

21.2 ISchemeHandlerFactoryのメソッドを実装

ISchemeHandlerFactoryインターフェースにはCreateメソッドしかありません。
このメソッドでは、カスタムスキーマをサポートするために、IResourceHandlerインタフェースを返却する必要があります。

このIResourceHandlerインタフェースを実装するクラスを作成するのもよいですが、以下の便利なスタティックメソッドが用意されていますので、これを使うことで簡単にIResourceHandlerインタフェースを用意することができます。

  • FromFilePathメソッド
    指定されたファイルの内容を返却するIResourceHandlerインタフェースを作成します。
  • FromStringメソッド
    指定された文字列内容を返却するIResourceHandlerインタフェースを作成します。
  • ForErrorMessageメソッド
    指定された文字列内容とStatusコードを返却するIResourceHandlerインタフェースを作成します。
  • FromByteArray メソッド
    指定されたバイト配列の内容を返却するIResourceHandlerインタフェースを作成します。

ここでは、FromFilePathメソッドとFromStringメソッドを使って、カスタムスキーマのメソッドを実装します。

  1. URLからファイルパスを取得する(※URLでファイルパスが参照される仕様とする)。
  2. 指定されたファイルが存在する場合、FromFilePathメソッドでファイルの内容を返却する。
  3. 指定されたファイルが存在しない場合、FromStringメソッドでエラー文言を返却する。
ISchemeHandlerFactoryのメソッドを実装例
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class LocalFileSchemeHandlerFactory : ISchemeHandlerFactory
    {
        /// <summary>
        /// カスタムスキーマ名
        /// </summary>
        private static readonly String SCHEME_NAME = "localfile";

        public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
        {
            // スキーマ名がLocalFileの場合
            if (schemeName == SCHEME_NAME)
            {
                // urlでc:\からのファイルパスが指定されているとして、絶対パスを作成する。
                String path = "c:\\" + request.Url.Replace($"{SCHEME_NAME}://", "").Replace("/", "\\");

                // 指定されたファイルがあれば、その内容を返却する。
                if (System.IO.File.Exists(path))
                {
                    // 指定されたファイルの中身を返却する
                    return ResourceHandler.FromFilePath(path, "text/plain");
                }
                else
                {
                    // エラーメッセージを返却する
                    return ResourceHandler.FromString("Such file dose NOT exist.");
                }
            }

            return null;
        }
    }
}

なお、カスタムスキーマは常に小文字として扱われます。
アドレスバーに大文字のカスタムスキーマ(LOCALFILE://xxx)と入れても、上のCreateメソッドには(localfile://xxx)と渡ってきます。

21.3 カスタムスキーマの登録

カスタムスキーマを利用できるように登録します。

これには、CefSettingsBase.RegisterSchemeメソッドを使用します。
CefSettingsBaseクラスは、「3.1 Cef.Initializeメソッドによる初期化」で説明している初期化のためのCef.Initializeメソッドに指定するオブジェクトのクラスです。
CefSettingsBase.RegisterSchemeでは以下の項目を設定します。

  • SchemeName - スキーマ名
  • IsCSPBypassing - CSPをバイパスするかどうか
  • IsSecure - HTTPSとして扱うかどうか
  • SchemeHandlerFactory - スキーマを実装するSchemeHandlerFactory

以下に実装例を示します。

カスタムスキーマの登録
using SimpleBrowser.Handlers;
using SimpleBrowser.UI;

namespace SimpleBrowser
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            // (省略)

            // カスタムスキーマを登録する
            settings.RegisterScheme(new CefCustomScheme()
            {
                SchemeName = "localfile",   // スキーマ名はlocalfile
                IsCSPBypassing = true,      // CSPはバイパス
                IsSecure = true,            // HTTPSとして扱う
                SchemeHandlerFactory = new Handlers.LocalFileSchemeHandlerFactory()
            });

            // CefSharpを初期化する
            Cef.Initialize(settings, false, new BrowserProcessHandler());

            Application.Run(new SimpleBrowserFrame());
        }
    }
}

21.4 カスタムスキーマの確認

ビルドして実行してみます。

アドレスバーに「localfile://temp/console.log」と入力して、ENTERキーを押下してみます。
※c:\temp\console.logがある前提。
すると以下の様にc:\temp\console.logファイルの内容が表示されるはずです。
カスタムスキーマ.png

アドレスバーに「localfile://aaaaa」と入力して、ENTERキーを押下してみます。
※c:\aaaaaがない前提。
すると以下の様にエラーメッセージが表示されます。
カスタムスキーマ2.png

以上、カスタムスキーマはブラウザ独自の機能を作りこむときに非常に便利です。

22. 子ウィンドウオープン

A要素のクリックやwindow.openメソッドで作成される子ウィンドウの管理はCefSharpで最も難しい部分です。
実はChromiumWebBrowserのインスタンスをフォームに張り付けただけの状態でも、必要に応じて子ウィンドウは自動的に作成されます。これはCefSharp内部の処理で行われていますが、自動作成される子ウィンドウではWebViewのコンテナであるウィンドウも自動作成されてしまいます。

例えば、タブ表示で子ウィンドウを表示したい場合は、この動作は変えないといけないです。また、子ウィンドウが閉じられるタイミングもハンドリングして、すべてのウィンドウが閉じられたときに、アプリケーションを終了するような動作にする必要もあります。

これを行うには、ILifeSpanHandlerインタフェースを作成して、自動作成されるWebViewも含めてインスタンスの管理を行う必要があります。このILifeSpanHandlerインタフェースは最も実装に気を使わなくてはいけないインタフェースです。

22.1 ILifeSpanHandlerインタフェースを実装するクラス

ILifeSpanHandlerインタフェースを実装するクラスを新規に追加します。

ソリューションエクスプローラでコンテキストメニューを出して、「追加」-「クラス」を選択して、クラス名にLifeSpanHandlerを指定します。そして、以下のようにILifeSpanHandlerインタフェースを実装させます。

ILifeSpanHandlerを実装するクラス
using System;
using CefSharp;

namespace SimpleBrowser.Handlers
{
    class LifeSpanHandler: ILifeSpanHandler
    {
    }
}

その後、ILifeSpanHandlerを選択して電球マークのコンテキストメニューから、「すべてのメンバーを明示的に実装する」を選択します。するとILifeSpanHandlerインタフェースのメソッドがすべて仮実装されます。

22.2 ILifeSpanHandlerインタフェースを実装のメソッドを仮実装する

ILifeSpanHandlerインタフェースには、次の4つのメソッドがあります。

  • DoClose
    WebViewが閉じられようとするときに呼び出されます。
    ChromeなどのWebブラウザでは、タブの×ボタンでタブを閉じようとした場合、Webコンテンツでjavascriptのonbeforeunloadイベントハンドラが呼び出されます。そこで、preventDefault()が呼び出されると、このページを離れても良いかを確認するためダイアログを表示されて、ユーザが了承した場合にタブのクローズ処理が実行されます。そして、ユーザがタブを閉じることを了承すると、javascriptのonunloadイベントハンドラが実行されて、最終的にタブが閉じられれることになります。
    これと同じ終了時の動作を実現するためは、WebViewのコンテナウィンドウが閉じられようとするときに、一旦ウィンドウを閉じる動作を拒否して、IBrowser.CloseBrowserメソッドを呼び出す様にします。すると、onbeforeunloadイベントハンドラが呼び出されるようになります。そして、最終的にウィンドウを閉じることが許可された場合に、本メソッドが呼び出されます。その後、本メソッドでfalseを返却すると、再度WebViewのコンテナウィンドウのクローズのために、OSからWM_CLOSEメッセージが通知されるので、フォームで終了処理を行うことになります。
    今回は特に実装しませんが、onbeforeunloadの仕様を実現するためには、必ず実装が必要になります。

  • OnAfterCreated
    WebViewが作成されてIBrowserインタフェースが利用可能になった時点で呼び出されるハンドラです。
    new ChromiumWebBrowser()で明示的にWebViewを作成した場合も、OnBeforePopupが呼び出された後にCefSharpが自動的にWebViewを作成した場合も、その後このメソッドが呼びされることになります。
    ここでは、CefSharpが自動的にWebViewを作成する場合に、そのコンテナであるフォームに新たに作成されたWebViewの情報を設定する様にします。
    この部分はこの後の章で実装していきます。

  • OnBeforeClose
    WebViewが閉じられるタイミングで呼び出されます。
    タブ形式で表示している場合は、ここでWebViewのインスタンス数をデクリメントして、すべてのWebViewのインスタンス数がゼロになった時点でアプリケーションを終了するような実装にすることもできます。
    今回は特に何も実装しません。

  • OnBeforePopup
    A要素のクリックやwindow.openにより子ウィンドウがオープンされる前に呼び出されます。ユーザ操作によるものかどうかを判断することができますので、ここでポップアップブロック機能を実現することもできます。
    ここではCefSharpが自動的に作成するWebViewの親ウィンドウの情報を設定します。
    この部分は以降の章で解説します。

22.3 OnBeforePopupメソッドの実装

基本的にはfalseを返却してCefSharpにWebViewの作成を任せますが、trueを返却してCefSharpにWebViewを作成させない様にすることもできます。CefSharpにWebViewの作成を任せずに、自分でWebViewを作成する場合は、new ChromiumWebBrowser()を呼び出してWebViewを作成した後、trueを返却することになります。

しかし、自動で作成されたWebViewと明示的にnewして作成したWebViewとでは、動作に大きな違いがあるので注意が必要です。

アプリケーションがwindow.open()メソッドで作成したウィンドウの場合、javascriptのpostMessageメソッドを使ってメッセージの送信ができたり、window.openerプロパティでオープンしたウィンドウの情報を子ウィンドウ側で取得するできる必要があります。しかし、明示的にnewして作成したWebViewの場合は、このようなウィンドウの親子関係がないため、これらの操作ができません。

従って、このハンドラが呼び出される場合は、CefSharpにWebViewの作成をお任せすることをお勧めします。

このメソッドでは、アプリケーションで新しいWebViewのコンテナとなるウィンドウを用意して、そのコンテナウィンドウの情報をCefSharpに通知します。もし、タブ形式のブラウザにする場合は、新しいタブを追加して、WebViewのコンテナとなるウィンドウを用意することになるでしょう。
ここでは、親ウィンドウと同じフォームを作成して、それをコンテナとする様にします。

まず、コンテナとなるフォームSimpleBrowserFrameクラスのコンストラクタを変更して、WebView無しでフォームを作れるようにします。

コンストラクタの変更
public SimpleBrowserFrame(bool bCreateWebView = true)
{
    InitializeComponent();

    // WebView作成の指定がある場合は、WebViewを作成する
    if (bCreateWebView)
    {
        InitializeWebBrowser();
    }
}

private void InitializeWebBrowser()
{
    // CefSharpのWebViewを作成する。
    WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp");

    // コントロールを追加する。
    this.webViewContainer.Controls.Add(WebBrowser);
    WebBrowser.Dock = DockStyle.Fill;

    // WebViewの各種オプションを設定する
    WebBrowser.BrowserSettings.Javascript = CefState.Enabled;
    WebBrowser.BrowserSettings.JavascriptDomPaste = CefState.Enabled;
    WebBrowser.BrowserSettings.ApplicationCache = CefState.Enabled;
    WebBrowser.BrowserSettings.LocalStorage = CefState.Enabled;

    // CefSharpの各種ハンドラを設定する
    WebBrowser.KeyboardHandler = new Handlers.KeyboardHandler();
    WebBrowser.RequestHandler = new Handlers.RequestHandler();
    WebBrowser.DisplayHandler = new Handlers.DisplayHandler();
    WebBrowser.DownloadHandler = new Handlers.DownloadHandler();
    WebBrowser.LoadHandler = new Handlers.LoadHandler();

    // 独自オブジェクトを公開する
    WebBrowser.JavascriptObjectRepository.Register("Test", new SimpleBrowser.Util.Test(), true, null);
}

次に、OnBeforePopupメソッドを実装します。

OnBeforePopup
bool ILifeSpanHandler.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)
{
    // 親のbrowserからフォームを取得する
    SimpleBrowserFrame parent = SimpleBrowserFrame.getMainFrame(browser);
    if (parent != null)
    {
        // 親フォームのコンテキストで実行させる
        parent.Invoke(new Action(() =>
        {
            // WebView無しの空のコンテナフォームを作成する。
            SimpleBrowserFrame newWindows = new SimpleBrowserFrame(false);

            // WebViewのコンテナ(親ウィンドウ)の情報を設定する。
            Rectangle rect = newWindows.ClientRectangle;
            windowInfo.SetAsChild(newWindows.WebBrowserContainer.Handle, rect.Left, rect.Top, rect.Right, rect.Bottom);

            // フォームを表示する
            newWindows.Show();
        }));
    }

    // この引数は実験的な引数の用ですがNULLを設定する
    newBrowser = null;

    // CefSharpにWebViewの作成を任せるためにfalseを返却する
    return false;
}

以上で、子ウィンドウが作成されるときに、アプリケーションで用意したコンテナ用のフォームにCefSharpが自動的に作成するWevViewが表示されるようになりました。

22.4 OnAfterCreatedメソッドの実装

このメソッドでは、新しく作成されたWebViewのIBrowserインタフェースをコンテナウィンドウであるフォームに設定します。

OnAfterCreated
void ILifeSpanHandler.OnAfterCreated(IWebBrowser chromiumWebBrowser, IBrowser browser)
{
    // 新しく作成されたbrowserからコンテナのフォームを取得する
    SimpleBrowserFrame parent = SimpleBrowserFrame.getMainFrame(browser);
    if (parent != null)
    {
        // コンテナフォームのコンテキストで実行
        parent.BeginInvoke(new Action(() =>
        {
            // 親フォームにIBrowserインスタンスの情報を設定する
            parent.Browser = browser;
        }));
    }
}

次に、コンテナフォームであるSimpleBrowserFrameIBrowserインスタンスを保持するためのプロパティを追加します。

IBrowserのプロパティ
public partial class SimpleBrowserFrame : Form
{
    // (省略)

    /// <summary>
    /// ホストしているWebViewのIBrowserインタフェース
    /// </summary>
    public IBrowser Browser { get; set; }

    // (省略)
}

次に、コンテナフォームであるSimpleBrowserFrameのすべてのメソッドで、ChromiumWebBrowserではなくIBrowserを使用する様に変更します。
※CefSharpが自動的に作成したWebViewの場合、ChromiumWebBrowserインスタンスが存在しないため、戻るや進むといった操作はIBrowserを使用する様にする必要があります。

SimpleBrowserFrameのメソッド変更
private void backBtn_Click(object sender, EventArgs e)
{
    if (Browser.CanGoBack)
    {
        Browser.GoBack();
    }
}

private void forwardBtn_Click(object sender, EventArgs e)
{
    if (Browser.CanGoForward)
    {
        Browser.GoForward();
    }
}

private void reloadBtn_Click(object sender, EventArgs e)
{
    Browser.Reload();
}

以上で、新しく作成されたWebViewのインスタンスの情報を親フォームが受け取り、その後の各種操作ができるようになりました。

22.5 ChromiumWebBrowser.LifeSpanHandlerにILifeSpanHandlerを設定

ChromiumWebBrowser.LifeSpanHandler

ILifeSapbHandlerの設定
private void InitializeWebBrowser()
{
    // (省略)

    // LifeSpanHandlerをWebViewに設定する
    WebBrowser.LifeSpanHandler = new Handlers.LifeSpanHandler();

    // (省略)
}

以上で、子ウィンドウの作成をコントロールできるようになりました。

22.6 リサイズの対応

これまでの実装で子ウィンドウの作成ができるようになっていますが、現状では子ウィンドウをリサイズした時にWebViewのサイズがそのままになってしまっています。そこで、リサイズに対応させます。
具体的には、SimpleBrowserFrameフォームのResizeイベントのイベントハンドラを追加して、その中でWebViewのサイズを変更することになります。しかし、WebViewのウィンドウはNativeウィンドウですので、Pinvokeを使う必要があります。少し汚いですが、以下の様になります。

Resizeに対応
public partial class SimpleBrowserFrame : Form
{
    // (省略)

    private void SimpleBrowserFrame_Resize(object sender, EventArgs e)
    {
        if (Browser != null)
        {
            // WebViewのウィンドウハンドルを取得する
            IntPtr hWnd = Browser.GetHost().GetWindowHandle();

            // WebViewのサイズを変更する。
            SetWindowPos(hWnd, HWND_TOP, 0, 0, webViewContainer.Width, webViewContainer.Height, SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOZORDER);
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags);

    [Flags]
    public enum SetWindowPosFlags : uint
    {
        SWP_ASYNCWINDOWPOS = 0x4000,
        SWP_DEFERERASE = 0x2000,
        SWP_DRAWFRAME = 0x0020,
        SWP_FRAMECHANGED = 0x0020,
        SWP_HIDEWINDOW = 0x0080,
        SWP_NOACTIVATE = 0x0010,
        SWP_NOCOPYBITS = 0x0100,
        SWP_NOMOVE = 0x0002,
        SWP_NOOWNERZORDER = 0x0200,
        SWP_NOREDRAW = 0x0008,
        SWP_NOREPOSITION = 0x0200,
        SWP_NOSENDCHANGING = 0x0400,
        SWP_NOSIZE = 0x0001,
        SWP_NOZORDER = 0x0004,
        SWP_SHOWWINDOW = 0x0040,
    }

    private static readonly IntPtr HWND_TOP = new IntPtr(0);
    private static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
    private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
    private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
}

22.7 動作確認

ビルドして実行してみます。
ポップアップウィンドウの例として、Windows update catalogを使ってみます。
アドレスバーにhttps://www.catalog.update.microsoft.com/Search.aspx?q=KB4589208を入力してENTERを押下します。そして、適当なアップテートをクリックすると、新しい子ウィンドウが作成されることが確認できます。
その後、新しい子ウィンドウのアドレスバーでhttps://www.google.co.jp/を入力してENTERを押下した後、戻るボタンや進むボタンをクリックすると、子ウィンドウのナビゲートができていることが確認できると思います。

ただし、現在の実装では、最初のウィンドウを閉じると、すべての子ウィンドウが閉じられる動作になっています。本格的なブラウザを作成する場合は、タブを収容するフレームを親ウィンドウにして、子ウィンドウをタブ表示にする方式が良いです。

23. CefSharpの特徴

個人的な感想ですが、良い点と悪い点をまとめます。

23.1 良い点

  • chromiumベースのWebViewアプリが簡単に作れる
    少ないコードで完全なchromiumベースのWebViewを持つアプリケーションが作成できる。
    chromiumベースなので、HTML5、CSS5の最新仕様に準拠している。

  • ブラウジングの様々な契機をハンドリングできる

  • コンテンツの変更やscriptの埋め込みが簡単にできる

  • 独自機能を組み込んで、javascriptから利用させることができる

  • 開発者ツールも起動できる
    F12キーで表示される開発者ツールも、簡単に表示することができる。
    アプリケーションのデバッグもChromeと全く同じようにできる。

  • chrome driverで試験もできる
    seleniumもappiumも使用することができる。
    テストに関してはchromeと同じようにすることができる。

23.2 悪い点

  • バージョンアップにより仕様が変わりすぎる
    chromiumのバージョンに追従しているので、バージョンアップの頻度が非常に高い。
    また、バージョンアップの度に、breaking changesという致命的な変更が行われるために、改造しないとビルドが通らなくなってしまう。

  • ファイルサイズが大きい
    chromiumのライブラリが含まれるため、小さく作っても150MB以上のファイルサイズになってしまう。
    企業で使用する場合、端末へファイルを配布する方法を考える必要がある。

  • 動かないオプションがある
    CefSharpのAPIマニュアルに書かれているオプションでも動作しないものがある。
    実際に動作するかどうかは動かしてみないと分からない。

  • コマンドラインオプションの仕様が明確でない
    非公式なサイトに様々なコマンドラインオプションが紹介されていますが、実際に動作するかどうかは確認してみないと分からない。
    また、バージョンによって、動作したり、動作しなかったりもする。

  • geolocation APIや3rd-party Cookieのブロックなど、Chromeにあるのに利用できない機能がある
    geolocation APIは、独自で作成して公開することで対応することはできる。
    3rd-party Cookieは、自分で判断してブロックすることはできる。

23.3 まとめ

UWP用のEdgeのWebViewもありますが、Windowsで高度なカスタマイズを行えるWebViewとなると、今のところCefSharp一択ではないかと思います。

最後まで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ApplicationExceptionとは

初めに

上司「Qiita(社内で)公開しませんか?」

ApplicationExceptionとは

しません。

勉強会でApplicationExceptionなるものを知りました。
Exceptionクラスの派生クラスですが、参考書もMSDNも「非推奨」としています。以下公式より。

System.ApplicationException をスローしたり、System.ApplicationException から派生したりしないでください。

何故非推奨?

結論から言うと、MicrosoftがExceptionのルールを破ったかららしいです。

その昔

  • 共通言語ランタイム(CLR)がスローする例外→SystemExceptionクラスから派生
  • アプリケーションがスローする例外→ApplicationExceptionクラスから派生

というルールをMicrosoftが推奨していました。
しかし、ある日Microsoftの開発者がそのルールを守らず、CLRがスローするのにApplicationExceptionから派生させてたり、その逆、アプリケーションがスローするのにSystemExceptionから派生させてたりしてしまったとかなんとか...。

当然設計者も使用者も混乱するので、ある日を境に
「全ての例外はExceptionから派生させよう」
とルールを一新し、混乱の元であるSystemApplicationの使用を非推奨としているわけです。

最後に

天下のMicrosoft社の人でも間違えるんですね...。非推奨の理由を知って少し驚いたので、まとめさせていただきました。
もし説明に間違いや御幣がある場合は、教えていただければ幸いです。では。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】PowerPointファイル表示+音声合成でスライドの動画作成を自動化

先日PowerPointファイルを利用して発表動画を作成する機会があったのですが,何度も噛んだり,制限時間に間に合わなかったり撮り直すことになりました.

すごく喉が痛くなったので,機械に読み上げてもらうようにしようと思った次第です.

やろうとしていることは以下のようなものです.

構成図.png

  1. PowerPointファイルの読み込み
  2. スライドを画像に変換し,ノートに書かれている内容を取得
  3. 画像をUnityに読み込み
  4. ノートの文を音声合成APIに投げ,音声ファイルを作成
  5. スライド画像を表示し,音声を再生させる
  6. (アバターを表示し,口パクさせる)

今回は5までのやり方を示そうと思います.
ここまででナレーションありのスライド動画をUnityで表示させることができるようになります.ナレーションはスライドのメモを読み上げる形で実現します.

UnityRecorder等を利用することで動画をエクスポートすることもできます.

今回の記事で実装した内容はGitHubで公開しています.
詳しくはそちらを確認いただけるとありがたいです.

6まで実装すると以下のような動画がPowerPointファイルを投げるだけで作成できるようになりました.
sample.png
YouTubeの怪しい美容広告みたいになりました.

PowerPointファイルの読み込み

Microsoft.Office.Interop.PowerPointライブラリを利用することで,C#で.pptxや.pptといったPowerPointファイルを開いたり,修正,画像の書き出しといったことが可能になります.

以下のコードで,スライドを1枚の画像として書き出し,ノートをstringで取得します.

var SLIDE_PATH = "スライドの場所"
var FILE_PATH = "画像の保存場所"

// ノートと画像保存場所のリスト
var slideList = new List<(string, string)>();

var app = new Microsoft.Office.Interop.PowerPoint.Application();

// スライドを開く
var ppt = app.Presentations.Open(SLIDE_PATH, MsoTriState.msoTrue, MsoTriState.msoFalse,
    MsoTriState.msoFalse);

var width = (int) ppt.PageSetup.SlideWidth;
var height = (int) ppt.PageSetup.SlideHeight;

var slideList = new List<SlideDataRaw>();

for (var i = 1; i <= ppt.Slides.Count; i++)
{
    // 非表示スライドは無視
    if (ppt.Slides[i].SlideShowTransition.Hidden == MsoTriState.msoTrue) continue;

    // ノート
    var note = ppt.Slides[i].NotesPage.Shapes.Placeholders[2].TextFrame.TextRange.Text;
    if (note == "") continue;

    // JPEGとして保存
    var file = FILE_PATH + $"/slide{i:0000}.jpg";
    ppt.Slides[i].Export(file, "jpg", width, height);

    slideList.Add((note, file));
}

ppt.Close();
app.Quit();

UnityでPowerPointファイルを利用するのに一番問題となるのは,画像の保存場所です.
一般的にUnityではデータの保存場所としてApplication.persistentDataPathなどを利用しますが,これらは実行時Unityがアクセス権を持っており,PowerPointSDKからこの場所にデータの保存が行えないという問題がありました.
なのでFILE_PATHにはC:\Users\User\DocumentsなどUnityが触れない場所を設定する必要があります.

スライド画像をUnityに読み込む

一般的な.jpgファイルの読み込み方法と同じです.
以下のスクリプトで.jpgファイルをTexture2Dに変換します.

private static Texture2D LoadImage(string path)
{
    byte[] binary;
    try
    {
        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            var length = (int) fs.Length;
            binary = new byte[length];
            fs.Read(binary, 0, length);
            fs.Close();
        }
    }
    catch(IOException exception)
    {
        Debug.Log(exception);
        return null;
    }

    var texture = new Texture2D(0, 0);
    texture.LoadImage(binary);
    return texture;
}

テキストを音声合成しAudioClipを作成

先ほど取得できたスライドのノートを音声合成APIに送信し,AudioClipを作成します.
音声合成は必要になった場合にリアルタイムで合成結果を受け取るストリーミング音声合成が一般的ですが,今回は発言内容が事前にすべて決まっているため,先にリクエストを全て送信しておき,結果をキャッシュするようにします.

今回は音声合成APIとしてWatson Text To Speech APIを利用しました.

Watson Text To Speechの利用方法

  1. IBM Cloudにログイン(アカウントを持っていない場合は作成します)
  2. カタログからText To Speechを選択します.
    image.png

  3. リージョンを選択し,「作成」を押します.
    image.png

  4. リソースリストから今作成したサービスを選択し,APIキーとURLをメモしておきます
    image.png

これでWatson側の設定は以上です.
WatsonにはIBM Watson SDK for Unityという便利なUnity用SDKが存在し,これを利用することで多くのサンプルやUtilityスクリプトを利用できますが,今回はText To Speechのみを利用するため,自分でWebRequestを実装します.


まず,先ほど取得したAPIキーからアクセストークンを取得するリクエストを記述する必要があります.音声合成リクエストにはAPIキーではなくこのトークンを送る必要があります.

トークンを取得するリクエストは以下のように記述します.

public string GetAccessToken(string apikey)
{
    var form = new WWWForm();
    form.AddField("grant_type", "urn:ibm:params:oauth:grant-type:apikey");
    form.AddField("apikey", apiKey);
    form.AddField("response_type", "cloud_iam");
    using (var request = UnityWebRequest.Post(AUTH_URL, form))
    {
        request.SetRequestHeader("Content-type", "application/x-www-form-urlencoded");
        request.SendWebRequest();
        while(!request.isDone && !_cancelled){}

        if (request.responseCode != 200L)
        {
            Debug.LogError($"[GenerateAudio] Request Failed ({request.responseCode}): {request.error}\nat{request.url}");
            return;
        }

        var json = request.downloadHandler.text;
        return JsonConvert.DeserializeObject<IamTokenResponse>(json).AccessToken;
    }
}

今回はJsonのパースにNewtonsoft Jsonを利用しました.どんなものでもよく,AccessTokenパラメータのValueが目的のアクセストークンです.

一応,Newtonsoft Jsonの場合のパースする対象オブジェクトは以下のような構造になっています.

public class IamTokenResponse
{
    [JsonProperty("access_token", NullValueHandling = NullValueHandling.Ignore)]
    public string AccessToken { get; set; }
    [JsonProperty("refresh_token", NullValueHandling = NullValueHandling.Ignore)]
    public string RefreshToken { get; set; }
    [JsonProperty("token_type", NullValueHandling = NullValueHandling.Ignore)]
    public string TokenType { get; set; }
    [JsonProperty("expires_in", NullValueHandling = NullValueHandling.Ignore)]
    public long? ExpiresIn { get; set; }
    [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)]
    public long? Expiration { get; set; }
}

そしてリクエストを送信し,合成結果を取得するのは以下のように記述します.

public byte[] GetSynthesizeVoice(string text)
{
    var rqStr = JsonConvert.SerializeObject(new JObject {["text"] = text});
    var url = $"{URL}/v1/synthesize?voice=ja-JP_EmiV3Voice";
    using (var request = new UnityWebRequest(url, "POST"))
    {
        request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(rqStr));
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Accept", "audio/wav");
        request.SetRequestHeader("Authorization", $"Bearer {ACCESS_TOKEN}");
        request.SendWebRequest();
        while(!request.isDone && !_cancelled){}

        if (request.responseCode != 200L)
            return null;
        return request.downloadHandler.data;
    }
}

ACCESS_TOKENには先ほど取得したアクセストークンを,URLにはWatsonのページで取得したUrlを記述してください.


これで合成音声のバイナリを取得できたので,UnityのAudioClipに変換する作業を行います.方法は私の前回の記事Google Cloud Text-To-Speechを利用してUnityでキャラクターをフルボイスに!でも使わせていただいた,WAVクラスを利用します.

var wav = new WAV(GetSynthesizeVoice("こんにちは"));
var audioClip = AudioClip.Create("TextToSpeech", wav.SampleCount, 1, wav.Frequency, false);

スライドを表示

最後に,画像表示と音声再生を同時に行い,音声が終了したら次のスライドを表示し音声も再生させる機構を実装します.

実装は簡単で,音声再生に使うAudioSourceをUpdateで監視しておき,isPlaying(再生中)がfalseになったら次の処理を行います.

public class Presenter : MonoBehaviour
{
    [SerializeField] private RawImage image;
    [SerializeField] private AudioSource source;

    private int _currentIndex;
    private SlideData[] _slide;

    public void StartPresentation(SlideData[] data)
    {
        Debug.Log("[Presenter] Start presentation.");
        _slide = data;
        _currentIndex = -1;
    }

    private void Update()
    {
        if (_slide == null || source.isPlaying) return;
        if (_currentIndex >= _slide.Length - 1) return;
        Debug.Log("[Presenter] Next slide.");
        ++_currentIndex;
        image.texture = _slide[_currentIndex].image;
        source.clip = _slide[_currentIndex].clip;
        source.Play();
    }
}

これでスライドの読み込みから音声合成,スライド送りが実装できました.

終わりに

今回はPowerPointファイルをUnityで読み込み,音声合成を行うことでナレーションありのスライド動画を自動で作成する手順を紹介しました.

今回示したコードのサンプルは,実際には「リクエスト可能サイズに合わせたテキストの分割」や「分割された音声ファイルの結合」が必要です.そのような処理も含めた,UniSlideToMovieというサンプルプロジェクトをGitHubで公開していますのでそちらも確認していただけたらと思います.

また,この次の記事として,今回触れられなかった「6. アバターを表示し,口パクさせる」処理について書きたいと思います.そちらもよろしくお願いします.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# 9.0 のイケてるforeach

初めに

C#9.0から拡張メソッドでのGetEnumeratorでforeachが使えるようになりました。
これを使ってC++の様なイケてる列挙をしてみます。

本題

結論からサクッと
ここでC#9.0のトップレベルステートメントを使っています。

using System;
using System.Collections;
using System.Collections.Generic;

foreach (var i in   0.. 10) Console.WriteLine(i); // 0  1  2  3  4  5  6  7  8  9  10
foreach (var i in   0..^10) Console.WriteLine(i); // 0  1  2  3  4  5  6  7  8  9
foreach (var i in  ^0.. 10) Console.WriteLine(i); //    1  2  3  4  5  6  7  8  9  10
foreach (var i in  ^0..^10) Console.WriteLine(i); //    1  2  3  4  5  6  7  8  9
foreach (var i in  10.. 0 ) Console.WriteLine(i); //10  9  8  7  6  5  4  3  2  1  0
foreach (var i in  10..^0 ) Console.WriteLine(i); //10  9  8  7  6  5  4  3  2  1
foreach (var i in ^10.. 0 ) Console.WriteLine(i); //    9  8  7  6  5  4  3  2  1  0
foreach (var i in ^10..^0 ) Console.WriteLine(i); //    9  8  7  6  5  4  3  2  1

static class RangeExtension
{
    public static RangeEnumerator GetEnumerator(this Range range) => new RangeEnumerator(range);
    public struct RangeEnumerator : IEnumerator<int>
    {
        readonly int Max;
        readonly int Step;

        public int Current { get; private set; }
        object IEnumerator.Current => this.Current;

        public bool MoveNext()
        {
            if (this.Current != this.Max)
            {
                this.Current += this.Step;
                return true;
            }
            return false;
        }
        public void Dispose() { }
        public void Reset() => throw new NotSupportedException();

        public RangeEnumerator(Range range)
        {
            var step = range.End.Value < range.Start.Value ? -1 : 1;
            this.Current = range.Start.Value - (range.Start.IsFromEnd ? 0 : step);
            this.Max = range.End.Value - (range.End.IsFromEnd ? step : 0);
            this.Step = step;
        }
    }
}

四種類の区間に、逆順にも対応でなかなかイケてるのではないでしょうか。

負数の列挙には対応していません。これはIndexが負数に対応していない(というよりも符号で先頭または末尾からのインデックスであるかを表している)ためです。

追記

foreach (var i in (0..2, 0..2)) Console.WriteLine(i); 
//(0, 0) (0, 1) (0, 2) (1, 0) (1, 1) (1, 2) (2, 0) (2, 1) (2, 2)


static class RangeExtension
{
    public static IEnumerator<(int, int)> GetEnumerator(this (Range, Range) range)
    {
        foreach (var v1 in range.Item1)
            foreach (var v2 in range.Item2)
                yield return (v1, v2);
    }
    //同様に3項、4項...の実装があると便利かも
}

RangeのタプルにGetEnumerator()を生やしておくと多重ループが平坦にできてさらにイケてるかもしれません。
アロケーションをしないように実装するのは少し骨が折れますが

皆さんもこの機能の面白い使い方をご存じでしたらコメント等に残してくだされば幸いです。
それでは、最後までご覧いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む