- 投稿日:2020-11-17T20:44:45+09:00
Uno Platform で湯婆婆を Windows アプリと WebAssembly 対応してみた(無駄に MVVM)
最近流行していて、さらにはアドベントカレンダーまで登場した湯婆婆ですが、Flutterで湯婆婆を実装してみる に触発されて湯婆婆の画面付きを作ってみました。
こんな感じです。左が Windows アプリで右側が WebAssembly になります。
実際に動くサイトは以下にデプロイしています。
使ってるもの
Windows, Android, iOS, WebAssembly, macOS, Linux に対応している Uno Platform を使って作りました。今回はスマホ対応は単純にレスポンシブに作ることがメンドクサカッタのと、macOS と Linux は環境を用意するのがメンドクサカッタので Windows と WebAssembly を対象に作りました。
Uno Platform については以前 Qiita で紹介しました。
公式サイトはこちらになります。
無駄に頑張った所
今回の湯婆婆を作るうえで頑張ったところを列挙していきたいと思います。
?田さん対応
サロゲートペアに対応しています!!C# での対応方法は先日自分で書いたのでその通りにやりました。
無駄に MVVM
湯婆婆ごときにはオーバースペックなのでお勧めしませんが、なんとなく先日 Uno Platform に対応した Prism (MVVM フレームワーク) と ReactiveProperty を使って開発しています。
なのでソリューションエクスプローラーはこんなに沢山プロジェクトがあります。オーバースペック!
単体テスト書いた
湯婆婆のコアロジック(ちゃんとしたセリフになってるか)を単体テストしました。ランダムに 1 文字選ぶために乱数を生成している部分は乱数生成部分をモックに差し替え可能なように作って単体テストではモックを差し込んでいます。
テスト結果が緑だと気持ちいいですね!
?田さん対応のテストケースも書いています。あと大事な仕様の名前が未入力だと湯婆婆が死ぬことも確認しています。
[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 を試してみたかったので個人的には満足です。
ソースコードは以下のリポジトリで公開しています。
- 投稿日:2020-11-17T20:44:45+09:00
C# で湯婆婆を Windows アプリと WebAssembly 対応してみた(無駄に MVVM)
最近流行していて、さらにはアドベントカレンダーまで登場した湯婆婆ですが、Flutterで湯婆婆を実装してみる に触発されて湯婆婆の画面付きを作ってみました。
こんな感じです。左が Windows アプリで右側が WebAssembly になります。
実際に動くサイトは以下にデプロイしています。
使ってるもの
Windows, Android, iOS, WebAssembly, macOS, Linux に対応している Uno Platform を使って作りました。今回はスマホ対応は単純にレスポンシブに作ることがメンドクサカッタのと、macOS と Linux は環境を用意するのがメンドクサカッタので Windows と WebAssembly を対象に作りました。
Uno Platform については以前 Qiita で紹介しました。
公式サイトはこちらになります。
無駄に頑張った所
今回の湯婆婆を作るうえで頑張ったところを列挙していきたいと思います。
?田さん対応
サロゲートペアに対応しています!!C# での対応方法は先日自分で書いたのでその通りにやりました。
無駄に MVVM
湯婆婆ごときにはオーバースペックなのでお勧めしませんが、なんとなく先日 Uno Platform に対応した Prism (MVVM フレームワーク) と ReactiveProperty を使って開発しています。
なのでソリューションエクスプローラーはこんなに沢山プロジェクトがあります。オーバースペック!
単体テスト書いた
湯婆婆のコアロジック(ちゃんとしたセリフになってるか)を単体テストしました。ランダムに 1 文字選ぶために乱数を生成している部分は乱数生成部分をモックに差し替え可能なように作って単体テストではモックを差し込んでいます。
テスト結果が緑だと気持ちいいですね!
?田さん対応のテストケースも書いています。あと大事な仕様の名前が未入力だと湯婆婆が死ぬことも確認しています。
[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 を試してみたかったので個人的には満足です。
ソースコードは以下のリポジトリで公開しています。
- 投稿日:2020-11-17T19:03:09+09:00
【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 の値となります。
マウス位置(Input.mousePosition)は画面上のどこかを表すものなので、当然スクリーン座標系となります。マルチディスプレイの場合のマウス位置は?
マルチディスプレイの場合、Input.mousePositionで取得できる座標は、メインディスプレイ基準の座標となります。
例えば、1920 x 1080のディスプレイ2台を以下のように配置していた場合
座標は以下のようになります。
ワールド座標に変換したい
取得したマウス位置の座標をワールド座標に変換するには、
① 画面ごとのスクリーン座標に変換
② 該当する画面のカメラでワールド座標に変換
の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()
は動作しないのでご注意ください。
- 投稿日:2020-11-17T15:29:44+09:00
unityのオブジェクトをC#で生成、子要素に移動する方法
※Unity実践リファレンスの内容を参考にしています。
<エンジニアのためのUnity実践リファレンス ~ ゲーム開発にすぐに役立つスクリプト入門>
https://www.amazon.co.jp/dp/B00WHEJI8W/ref=cm_sw_em_r_mt_dp_K82SFb5NXTQ27動的にC#でゲームオブジェクトを生成し、それを特定のゲームオブジェクトの子要素として移動させるためのコードになります
// 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; }ちゃんと階層構造を持ったままゲームオブジェクトが生成されました。
この機能を使って特定の条件下で生成したゲームオブジェクトがあれば、それをFind関数を利用して見つけたのちに、子要素へ移動させるということもできそうです。
- 投稿日:2020-11-17T09:11:21+09:00
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ブラウザを作成します。
- 新規プロジェクト作成する
- Nugetパッケージの取り込む
- WebViewを貼り付ける
まだVisual Studioをインストールしていない場合は、以下のサイトから
Visual Studio Express
(無料)のインストーラをダウンロードして、インストールしましょう。
Visual Studio Express2.1 新規プロジェクトの作成する
C#のWinformsのプロジェクトを新規作成します。
以下は、Visual Studio 2019 Expressでのやり方になります。
- 「新しいプロジェクトの作成」をクリックします。
- 「Windowsフォームアプリケーションの作成(.NET Framework)」を選択して「次へ」をクリックします。
- プロジェクト名と場所を指定して、「作成」をクリックします。
ここでは、「SimpleBrowser」としました。作成されるプロジェクトのフォームの名称がデフォルトで「Form1.cs」とカッコ悪いので、「ソリューション エクスプローラ」で「Form1.cs」を選択して、コンテキストメニューから「名前の変更」を選択して、適当な名前に変更します。
ここでは、「SimpleBrowserFrame.cs」としました。
CefSharpは「AnyCPU」でビルドできないので、構成マネージャで「AnyCPU」を削除して「x64」と「x86」を追加します。
※「x64」と「x86」のいずれか1つでももちろんOKです。
ソリューションエクスプローラでソリューションを選択して、コンテキストメニューから「バッチビルド」を選択すると、以下のような構成に見えるはずです。
2.2 Nugetパッケージの取り込む
CefSharpのNugetパッケージを取り込みます。
- ソリューションエクスプローラで「SimpleBrowser」を選択して、コンテキストメニューから「NuGetパッケージの管理」を選択します。
- NuGetパッケージマネージャで、「参照」タブを選択します。
検索ボックスに「CefSharp」と入力して検索を実行すると、CefSharp関連のNugetパッケージが表示されます。
以下の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.csusing 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ブラウザが表示されます。
2.5 バージョンを確認する
初期URLを以下のように
chrome://version
に変更して実行してみると、バージョンのページが表示されます。
このようにchromeのカスタムスキーマの一部も表示させることができます。chromeのバージョン確認//WebBrowser = new ChromiumWebBrowser("https://www.google.co.jp"); WebBrowser = new ChromiumWebBrowser("chrome://version"); // 変更する。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の設定方法2CefSettings 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の設定方法3Cef.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の設定方法4Cef.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インタフェースはOnKeyEventとOnPreKeyEventの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キー押下をしてみる。
うまく行くと、以下の様なおなじみの開発者ツールが起動します。
おー!表示されました。
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
の行と列を適当に配置します。WebViewのコンテナを配置する。
WebViewを配置する部分に、コンテナ用のPanelを配置します。
- Panelは(0,1)の位置から、ColumnSpan=4で複数セルをカバーさせています。
- PanelPanelのDockをFillにして、連結したセル全体をカバーさせます。
- PanelPanelのNameを
webViewContainer
としておきます。アドレスバーを配置する。
アドレスバーを配置します。
- TextBoxを(3,0)の位置に配置します。
- TextBoxをのDockをFillにして、セル全体をカバーさせます。
- TextBoxをのMarginやFontを適当の変更して、見た目を調整する。
- TextBoxのNameを
addressBar
としておきます。ボタンを配置する。
戻る、進む、更新のボタンを配置します。
- Buttonを3つ配置します。
- それぞれのButtonでDockをFillにして、セル全体をカバーさせます。
- それぞれのButtonのイメージを適当に設定します。
- それぞれのButtonのNameを
backBtn
、forwardBtn
、reloadBtn
としておきます。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 実行してみる
ビルドして実行してみます。
正しく実装されていると、以下のようなブラウザが表示されるはずです。
ALT+[→]、ALT+[←]、F5のショートカットキーが有効になっていることも確認できます。
ボタンやアドレスバーも操作可能になっています。6.5 操作対象についての注意
IKeyboardHandler
ハンドラでの戻る、進む、更新の処理は、IWebBrowserではなくIBrowserに対して行っています。
IWebBrowserとIBrowserは同じWebViewを指しているように思われるかもしれませんが、window.openメソッドで開いた子ウィンドウの場合など、明示的にChromiumWebBrowserをnewして作成していないWebViewの場合は、IWebBrowserとIBrowserは同じWebViewを指していないので注意が必要です。
ChromiumWebBrowserをnew作成した場合
以下の様にChromiumWebBrowser
、IWebBrowser
、IBrowser
は同じWebViewと関連しています。
IKeyboardHandler
等のCefSharpのハンドラのメソッドは、これらの引数にして呼び出されることになります。
この場合は、IWebBrowser
、IBrowser
のどちらを操作しても、結果は同じになります。a要素のクリックやwindow.openにより子ウィンドウが自動的に作成された場合
CefSharpにより自動作成されたWebViewでは、ChromiumWebBrowser
やIWebBrowser
は存在しておらず、参照を取得することができません。
IKeyboardHandler
等のCefSharpのハンドラのメソッドでは、親ウィンドウのIWebBrowser
インタフェースが引数として渡されます。
この場合は、IWebBrowser
を操作すると親ウィンドウのWebViewが操作されてしまうことになります。まとめると、
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文字列等の情報も受け取るので、これらの情報も表示するようにすることもできます。また、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.log
やconsole.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を使うことにします。
- ソリューションエクスプローラで「SimpleBrowser」を選択して、コンテキストメニューから「NuGetパッケージの管理」を選択します。
- NuGetパッケージマネージャで、「参照」タブを選択します。
- 検索ボックスに「Log4Net」と入力して検索を実行すると、Log4net関連のNugetパッケージが表示されます。
- 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 ] - abc13. ダウンロード
ファイルのダウンロード機能を実装することができます。
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が表示されています。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がブロックされるようになりました。
確認のために、開発者ツールを起動してみます。
- ブラウザを起動する.
- F12キーを押下して開発者ツールを立ち上げる。
F5でページをリロードする。
レスポンスを確認する。
開発者ツールのnetworkを選択して、リクエストの内容を確認してみるとCookie
がないことが確認できます。
つまり、Cookie
は送信されていません。
Cookieを確認する。
ApplicationのCookieを選択すると、以下の様にCookieが保存されていないことが確認できます。
つまり、Cookie
は保存されていません。
以上で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インタフェースの各メソッドを仮実装します。
- ILoadHandler.OnFrameLoadEnd
ブラウザがフレーム(iframeやframeのこと)のロードを終了した時点で呼び出されます。
ここでは特に何もしません。ILoadHandler.OnFrameLoadStart
ブラウザがフレーム(iframeやframeのこと)のロードを開始した時点で呼び出されます。
ここでは特に何もしません。ILoadHandler.OnLoadError
ナビゲーションの失敗やキャンセルによりエラーになった時点で呼び出されます。
ここでは特に何もしません。ILoadHandler.OnLoadingStateChange
ロードの状態が変化した時点で呼び出されます。
以下の「18.3 ILoadHandler.OnLoadingStateChangeメソッドの実装」で、ドキュメントのロード完了をハンドリングして、スクリプトを実行するようにしていきます。18.3 ILoadHandler.OnLoadingStateChangeメソッドの実装
引数で渡されるLoadingStateChangedEventArgsのIsLoadingプロパティを参照することでロードが完了したかどうかを判断することができます。そして、ロードが完了していれば、スクリプトを実行することにします。
スクリプトを実行するためには、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
の背景色が赤色になっているはずです。
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が返却されます。 したがって、時間のかかる処理を行う場合は、明示的に別スレッドで実行して、次に示すコールバックを呼び出す方法をお勧めします。- マーシャリングの仕様が明確でない。
int
やString
ような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つのリソースのレスポンスは分割されて受信する場合があるため、このメソッドは複数回呼び出されることになります。
基本的には、以下の流れになります。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を確認すると、スクリプト要素が埋め込まれていることが確認できます。なお、上で実装したコードでは、最初のscriptとして追加されるようにしていますが、このように最初のscriptにならない場合があります。
これは、変更したWebコンテンツがブラウザにロードされた後に、内部のスクリプトによって追加のスクリプトがロードされてるためだと考えられます。また、Content-Security-Policy(CSP)を突破することはできないため、
Content-Security-Policy
のscript-src
の設定によっては、このように直接埋め込んだスクリプトは実行されないことにも注意が必要です。
CSPを突破するためには、IsCSPBypassingでCSPをバイパスする指定のカスタムスキーマ作成してスクリプトをロード必要があります。カスタムスキーマについては次章で説明します。21. カスタムスキーマ
カスタムスキーマとは、chromeでの
chrome://version
のようなURLでのchrome://
の部分のことです。http://
やhttps://
以外に独自のスキーマを提供することができます。
また、カスタムスキーマはContent-Security-Policy(CSP)を突破することができるため、「# 20. レスポンス内容の変更」で説明したレスポンスでscript
のsrc
にカスタムスキーマのリソースを埋め込むと、任意のコードを実行できるようになります。
他にも通常のスキーマではできない、特殊なことを行うスキーマを提供することができます。ここでは、ローカルの任意のファイルの中身を取り出すスキーマを実装してみます。
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メソッドを使って、カスタムスキーマのメソッドを実装します。
- URLからファイルパスを取得する(※URLでファイルパスが参照される仕様とする)。
- 指定されたファイルが存在する場合、FromFilePathメソッドでファイルの内容を返却する。
- 指定されたファイルが存在しない場合、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
ファイルの内容が表示されるはずです。
アドレスバーに「localfile://aaaaa」と入力して、ENTERキーを押下してみます。
※c:\aaaaaがない前提。
すると以下の様にエラーメッセージが表示されます。
以上、カスタムスキーマはブラウザ独自の機能を作りこむときに非常に便利です。
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
メソッドを実装します。OnBeforePopupbool 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
インタフェースをコンテナウィンドウであるフォームに設定します。OnAfterCreatedvoid ILifeSpanHandler.OnAfterCreated(IWebBrowser chromiumWebBrowser, IBrowser browser) { // 新しく作成されたbrowserからコンテナのフォームを取得する SimpleBrowserFrame parent = SimpleBrowserFrame.getMainFrame(browser); if (parent != null) { // コンテナフォームのコンテキストで実行 parent.BeginInvoke(new Action(() => { // 親フォームにIBrowserインスタンスの情報を設定する parent.Browser = browser; })); } }次に、コンテナフォームである
SimpleBrowserFrame
にIBrowser
インスタンスを保持するためのプロパティを追加します。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一択ではないかと思います。
最後まで読んでいただき、ありがとうございました。
- 投稿日:2020-11-17T00:52:09+09:00
ApplicationExceptionとは
初めに
上司「Qiita(社内で)公開しませんか?」
ApplicationExceptionとは
しません。
勉強会でApplicationExceptionなるものを知りました。
Exceptionクラスの派生クラスですが、参考書もMSDNも「非推奨」としています。以下公式より。System.ApplicationException をスローしたり、System.ApplicationException から派生したりしないでください。
何故非推奨?
結論から言うと、MicrosoftがExceptionのルールを破ったかららしいです。
その昔
- 共通言語ランタイム(CLR)がスローする例外→SystemExceptionクラスから派生
- アプリケーションがスローする例外→ApplicationExceptionクラスから派生
というルールをMicrosoftが推奨していました。
しかし、ある日Microsoftの開発者がそのルールを守らず、CLRがスローするのにApplicationExceptionから派生させてたり、その逆、アプリケーションがスローするのにSystemExceptionから派生させてたりしてしまったとかなんとか...。当然設計者も使用者も混乱するので、ある日を境に
「全ての例外はExceptionから派生させよう」
とルールを一新し、混乱の元であるSystemApplicationの使用を非推奨としているわけです。最後に
天下のMicrosoft社の人でも間違えるんですね...。非推奨の理由を知って少し驚いたので、まとめさせていただきました。
もし説明に間違いや御幣がある場合は、教えていただければ幸いです。では。
- 投稿日:2020-11-17T00:44:53+09:00
【Unity】PowerPointファイル表示+音声合成でスライドの動画作成を自動化
先日PowerPointファイルを利用して発表動画を作成する機会があったのですが,何度も噛んだり,制限時間に間に合わなかったり撮り直すことになりました.
すごく喉が痛くなったので,機械に読み上げてもらうようにしようと思った次第です.
やろうとしていることは以下のようなものです.
- PowerPointファイルの読み込み
- スライドを画像に変換し,ノートに書かれている内容を取得
- 画像をUnityに読み込み
- ノートの文を音声合成APIに投げ,音声ファイルを作成
- スライド画像を表示し,音声を再生させる
- (アバターを表示し,口パクさせる)
今回は5までのやり方を示そうと思います.
ここまででナレーションありのスライド動画をUnityで表示させることができるようになります.ナレーションはスライドのメモを読み上げる形で実現します.UnityRecorder等を利用することで動画をエクスポートすることもできます.
今回の記事で実装した内容はGitHubで公開しています.
詳しくはそちらを確認いただけるとありがたいです.6まで実装すると以下のような動画がPowerPointファイルを投げるだけで作成できるようになりました.
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の利用方法
- IBM Cloudにログイン(アカウントを持っていない場合は作成します)
これで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. アバターを表示し,口パクさせる」処理について書きたいと思います.そちらもよろしくお願いします.
- 投稿日:2020-11-17T00:21:10+09:00
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()
を生やしておくと多重ループが平坦にできてさらにイケてるかもしれません。
アロケーションをしないように実装するのは少し骨が折れますが皆さんもこの機能の面白い使い方をご存じでしたらコメント等に残してくだされば幸いです。
それでは、最後までご覧いただきありがとうございました。