Chromium Embedded Framework(CEF3)の使用レポート

WindowsアプリにWebブラウザーの機能を簡単に組み込もうとした時、どういう方法を使いますか?案外とどこから着手したら良いか迷う方が多いのではないでしょうか。

特にマルチプラットフォーム開発の時にWindowsでもIEコンポーネントではなく、iOSのUIWebViewやWKWebView、AndroidのWebViewと似た使い方をできる仕組みがあると便利です。

今回はChromiumEmbedded(CEF3)というオープンソースのフレームワークを紹介します。

CEF3以外にもオープンソースの選択肢としてはWebkitがあります。興味がある方はそちらも検討してください。

CEF3はChromiumの機能をアプリケーション内に組み込むためのフレームワークです。Chromiumの比較的新しいバージョンを利用できるため、いわゆるモダンブラウザの機能をWindowsプログラムへ組み込むのに向いています。

しかし残念ながらあまりドキュメントが充実しておらず、特に少しこだわった使い方をしようとするとハマりどころが多くあります。

また、既にOpenGLを使っているWindowsアプリにWebブラウザー機能を組み込もうとすると、お互いの表示が干渉し合う現象が発生します。その点についても後ほど詳しく記述します。

Windows用WebViewとして実装したい機能

ポジションセット
client上任意のポジションにWebViewを設置すること
リサイズ
WebViewの大きさを変えること
可視性
WebViewの表示/非表示
ScalePageToFit
適切な大きさでページを表示
SetCustomHeader
リクエストのヘッダーをカスタマイズ
JavaScript実行
ページ上のボタンを押すなどのイベントで任意のJavaScriptコードを実行

最初の難関

WindowsアプリにWebViewを組み込む需要はあるはずなのに、CEFの話はあまり聞いたことがありません。

その原因の一つは資料や文献はほとんど英語で、サンプルも少ないことだと考えられます。

どこから着手するのかが分からない人もいれば、途中で挫折して諦める人も少なくないでしょう。

ここでは今回の参考資料のリンクを先に貼っておきます。

CEF wiki
概要をまとめたページで、さまざまな関連リンクがあります。
CEF Forum
先人の知恵はたくさんあるので、はまったことがあったらここでトピックを探すのをおすすめ。
CEF Builds
ここからサンプルやプロジェクトファイルを入手できます。動くものがあれば結構良い足がかりになります。
API Doc
API一覧、メソッドのことならここで一目瞭然です。

CEFの特徴

機能がとても充実していて、パワフルです。しかし全体のイメージを掴むのにはやや学習コストが必要です。いきなり大きな機能をつけるよりも、小さいプロジェクトからスタートしてどんどん機能を追加していくスタイルをお勧めします。

コーディング上の特徴や注意点を簡単に挙げておきます。

C++にはC#やJavaの”interface”に直接該当するものがないので、handlerを多重継承で必要なメソッドを生やすことになります。

RTTIやキャストミスを避けるために、継承したクラスのポインタを取得するgetterを全部上書きしています。

純粋仮想関数の定義が漏れないように継承先のメソッド一覧を一度きちんと把握しておいたほうがいいです。

ポインタは全部STLのshared_ptrのようなクラスにラップされるため、オブジェクトのメモリ解放についてはあまり気にする必要がありません。

実際にCEFを使って必要な機能を揃えるまで

Step 0

サンプルプロジェクトの準備と実行確認

  • CEF BuildsからCEF 3.2556.1368.g535c4fb (もしそれより新しいバージョンがでたら、それを使ってください)をダウンロードし、解凍

  • VS2010用の.slnを開くのがおすすめ

  • ソリューションを開くと、三つのプロジェクトが入っていることを確認

  • 最後のラッパー以外の二つそれぞれスタートプロジェクトとして実行してみる

image02

今回の用途においてはCEF3の全体像を把握するのが重要だと考えました。このため出発地点としてはcefclientのほうがいいと判断し、cefsimpleは不要なので削除しました。

cefclientプロジェクトにはファイルが結構な数ありますが、一通り眺めた結果、ClientHandlerClientAppという二つのクラスがとても重要な役割を担っていることがわかりました。その二つのクラスはそれぞれ幾つかのhandlerクラスを継承し、ブラウザーに対する操作を担います。

image00

Step 1

ClientHandlerClientAppをダイエットさせて、最低限必要な機能だけにしました。

いろいろ試行錯誤した結果、

  • ClientHandlerクラスはCefClientCefLifeSpanHandlerを継承

  • ClientAppクラスはCefAppを継承

という構造にしました。さらにMainWindowの子ウィンドウを作ってそこにブラウザーを生成するようにシンプル化しました。

Step 2

とりあえず現段階できた分をアプリに組み込み(やっと本題に入ります)

ヘッダー等々が認識させるように幾つか工夫が必要

  • 実行に必要なdll等のコピー(サンプルプロジェクト実行する時の.gypから出力したログを読んで把握できます)

  • プロジェクトプロパティの修正(lib内の相対パス参照のエラーを解決するため)

クラス定義やインターフェースを実装してから、ウィンドウの親子関係を整理して実行します。

ありがちな問題

WebViewがちらつく(OpenGLとの干渉が発生)

  • この問題はネットで検索すれば結構出ます。

  • WindowsのマルチウィンドウプログラムとOpenGLの相性の問題はありがちのようです。

  • 解消方法も幾つか書いてありますが、残念ながら自分が試した結果いずれもうまく対応できませんでした。

散々試したあげく、何もしないスーバーウィンドウをメインウィンドウとして作って、アプリウィンドウ(本来のメインウィンドウ、OpenGL使用)とWebViewウィンドウの両方を子ウィンドウにした上で、SetWindowPosでwebviewウィンドウを手前に出すという非常に遠回りな方法で解消しました。

Step 3

機能を増やす

  • ポジションセット

  • リサイズ

  • 可視性

SetWindowPosMoveWindowでウィンドウの位置や大きさを設定できます。ShowWindowで可視性を設定できます。

一点気を付けることがあります。WebViewウィンドウとスーパーウィンドウの親子関係の都合上、スーパーウィンドウの位置が変わった場合にはWebViewウィンドウも一緒に移動させる必要があります。

ありがちな問題

ブラウザーの大きさが変わらない(もしくは微妙に小さくなったりする)

  • 大体の場合、原因はbrowserウィンドウのパラメータ変更忘れです

  • WebViewウィンドウの大きさだけを変えた場合、browserウィンドウが大きくなることはないでしょう。

WebViewウィンドウはOpenGLとの干渉問題を解決するための存在で、browserウィンドウの親ウィンドウです。画面上にブラウザーが一つしかない場合、これらのウィンドウの大きさを完全に同じにすればよいです。また、browserウィンドウのポジションはWebViewウィンドウとの相対位置(デフォルトは同位置)なので、特に設定するの必要はありません。

browserウィンドウはbrowser->GetHost()->GetWindowHandle()で取得できます。

image01

Step 4

更に機能追加

  • ScalePageToFit

  • SetCustomHeader

HTTPリクエスト時にカスタムヘッダを付加する仕組み(SetCustomHeader)は割と簡単です。まずClientHandlerの継承にCefRequestHandlerを追加します。

そしてリクエストの生成時にHeaderMapへとkey-valueペアを登録してLoadRequestを呼ぶだけで終わりです。

ScaleToFitPageの対応は若干手間です。

まず、scale = 1.2 ^ zoomLevelという計算式があります(Forumから入手した情報)。

その逆算でzoomLevel = lg(scale) / lg(1.2)が分かります。

一定の基準となるscaleからzoomLevelを計算できたら、SetZoomLevelを読んでコンテンツの大きさを変更できるはずです

が、大きさが全然変わりません

数時間の試行錯誤の後に、SetZoomLevelをリクエストの処理よりも前に呼んでも何も反映されないことがわかりました。

SetZoomLevelの呼び出し位置を変更し、CefRequestHandlerOnBeforeResourceLoadイベントで呼ぶことでうまく反映されます。

Step 5

JavaScript実行

wiki上にはcef / JavaScriptIntegrationというページがあってJSObjectやJSFunctionなどの記述は全部書いてありますので、これらは省略します。

JavaScriptコードの実行はとても簡単で、基本的にExecuteJavaScriptへ引数を渡せば終わりです。

ありがちな問題

Window Bindingをしたい時に、OnContextCreated()メソッドが全く呼ばれない

  • まず、ClientAppクラスはCefRenderProcessHandlerを継承する必要があります。

  • 初期化の時にCefSettings settings; settings.single_process = true;を設定し、singleProcessにしないと各browserのrenderProcessになります。

後はおまけとして、JSObjectJSFunctionが組み合わせて階層化可能で、javascript:window.JSObject.JSFunction()という構造を作れます。

実際にはJSFunctionJSObjectとして生成し、親のJSObjectの下にぶら下げます。

たとえば、JavaScript側からwindow.engine.func()でC++側の指定メソッドを呼び出したい場合、コードは以下のようになります。

void MyWebView::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
  CefRefPtr<CefV8Handler> handler = new MyV8Handler(this);
  CefRefPtr<CefV8Value> obj = context->GetGlobal();

  CefRefPtr<CefV8Value> val = CefV8Value::CreateObject(NULL);
  val->SetValue("func", CefV8Value::CreateFunction("func", handler),V8_PROPERTY_ATTRIBUTE_NONE);
  obj->SetValue("engine", val, V8_PROPERTY_ATTRIBUTE_NONE);
}

CreateObjectで作成したJavaScript側のオブジェクトに対してCreateFunctionで作成した関数を割り当て、JavaScript側のglobalオブジェクト(=Webブラウザ上ではwindowオブジェクト)へぶら下げるのがポイントです。

まとめ

今回はCEF3を使ってiOSやAndroidのWebViewに用意されている機能と似た仕組みをWindows上で実現する方法と、その中で遭遇するいくつかの問題への解決策を紹介しました。

個人的には、CEF3があまり普及しない理由はひょっとすると機能が充実しすぎて、規模が大きすぎるためかもしれないと思いました。

本文でも書いたように、最小化したプロジェクトに対して、追加したい機能に応じてhandlerをどんどん継承し構築していく方法で取り組むのをおすすめします。

Kyo Shinyuu

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

おすすめ

合わせて読みたい

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。