WindowsアプリにWebブラウザーの機能を簡単に組み込もうとした時、どういう方法を使いますか?案外とどこから着手したら良いか迷う方が多いのではないでしょうか。
特にマルチプラットフォーム開発の時にWindowsでもIEコンポーネントではなく、iOSのUIWebViewやWKWebView、AndroidのWebViewと似た使い方をできる仕組みがあると便利です。
今回はChromiumEmbedded(CEF3)というオープンソースのフレームワークを紹介します。
CEF3以外にもオープンソースの選択肢としてはWebkitがあります。興味がある方はそちらも検討してください。
CEF3はChromiumの機能をアプリケーション内に組み込むためのフレームワークです。Chromiumの比較的新しいバージョンを利用できるため、いわゆるモダンブラウザの機能をWindowsプログラムへ組み込むのに向いています。
しかし残念ながらあまりドキュメントが充実しておらず、特に少しこだわった使い方をしようとするとハマりどころが多くあります。
また、既にOpenGLを使っているWindowsアプリにWebブラウザー機能を組み込もうとすると、お互いの表示が干渉し合う現象が発生します。その点についても後ほど詳しく記述します。
WindowsアプリにWebViewを組み込む需要はあるはずなのに、CEFの話はあまり聞いたことがありません。
その原因の一つは資料や文献はほとんど英語で、サンプルも少ないことだと考えられます。
どこから着手するのかが分からない人もいれば、途中で挫折して諦める人も少なくないでしょう。
ここでは今回の参考資料のリンクを先に貼っておきます。
機能がとても充実していて、パワフルです。しかし全体のイメージを掴むのにはやや学習コストが必要です。いきなり大きな機能をつけるよりも、小さいプロジェクトからスタートしてどんどん機能を追加していくスタイルをお勧めします。
コーディング上の特徴や注意点を簡単に挙げておきます。
C++にはC#やJavaの”interface”に直接該当するものがないので、handlerを多重継承で必要なメソッドを生やすことになります。
RTTIやキャストミスを避けるために、継承したクラスのポインタを取得するgetterを全部上書きしています。
純粋仮想関数の定義が漏れないように継承先のメソッド一覧を一度きちんと把握しておいたほうがいいです。
ポインタは全部STLのshared_ptrのようなクラスにラップされるため、オブジェクトのメモリ解放についてはあまり気にする必要がありません。
サンプルプロジェクトの準備と実行確認
CEF BuildsからCEF 3.2556.1368.g535c4fb (もしそれより新しいバージョンがでたら、それを使ってください)をダウンロードし、解凍
VS2010用の.slnを開くのがおすすめ
ソリューションを開くと、三つのプロジェクトが入っていることを確認
最後のラッパー以外の二つそれぞれスタートプロジェクトとして実行してみる
今回の用途においてはCEF3の全体像を把握するのが重要だと考えました。このため出発地点としてはcefclientのほうがいいと判断し、cefsimpleは不要なので削除しました。
cefclientプロジェクトにはファイルが結構な数ありますが、一通り眺めた結果、ClientHandler
とClientApp
という二つのクラスがとても重要な役割を担っていることがわかりました。その二つのクラスはそれぞれ幾つかのhandlerクラスを継承し、ブラウザーに対する操作を担います。
ClientHandler
とClientApp
をダイエットさせて、最低限必要な機能だけにしました。
いろいろ試行錯誤した結果、
ClientHandler
クラスはCefClient
とCefLifeSpanHandler
を継承
ClientApp
クラスはCefApp
を継承
という構造にしました。さらにMainWindowの子ウィンドウを作ってそこにブラウザーを生成するようにシンプル化しました。
とりあえず現段階できた分をアプリに組み込み(やっと本題に入ります)
ヘッダー等々が認識させるように幾つか工夫が必要
実行に必要なdll等のコピー(サンプルプロジェクト実行する時の.gypから出力したログを読んで把握できます)
プロジェクトプロパティの修正(lib内の相対パス参照のエラーを解決するため)
クラス定義やインターフェースを実装してから、ウィンドウの親子関係を整理して実行します。
WebViewがちらつく(OpenGLとの干渉が発生)
この問題はネットで検索すれば結構出ます。
WindowsのマルチウィンドウプログラムとOpenGLの相性の問題はありがちのようです。
解消方法も幾つか書いてありますが、残念ながら自分が試した結果いずれもうまく対応できませんでした。
散々試したあげく、何もしないスーバーウィンドウをメインウィンドウとして作って、アプリウィンドウ(本来のメインウィンドウ、OpenGL使用)とWebViewウィンドウの両方を子ウィンドウにした上で、SetWindowPos
でwebviewウィンドウを手前に出すという非常に遠回りな方法で解消しました。
機能を増やす
ポジションセット
リサイズ
可視性
SetWindowPos
やMoveWindow
でウィンドウの位置や大きさを設定できます。ShowWindow
で可視性を設定できます。
一点気を付けることがあります。WebViewウィンドウとスーパーウィンドウの親子関係の都合上、スーパーウィンドウの位置が変わった場合にはWebViewウィンドウも一緒に移動させる必要があります。
ブラウザーの大きさが変わらない(もしくは微妙に小さくなったりする)
大体の場合、原因はbrowserウィンドウのパラメータ変更忘れです
WebViewウィンドウの大きさだけを変えた場合、browserウィンドウが大きくなることはないでしょう。
WebViewウィンドウはOpenGLとの干渉問題を解決するための存在で、browserウィンドウの親ウィンドウです。画面上にブラウザーが一つしかない場合、これらのウィンドウの大きさを完全に同じにすればよいです。また、browserウィンドウのポジションはWebViewウィンドウとの相対位置(デフォルトは同位置)なので、特に設定するの必要はありません。
browserウィンドウはbrowser->GetHost()->GetWindowHandle()
で取得できます。
更に機能追加
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
の呼び出し位置を変更し、CefRequestHandler
のOnBeforeResourceLoad
イベントで呼ぶことでうまく反映されます。
JavaScript実行
wiki上にはcef / JavaScriptIntegrationというページがあってJSObjectやJSFunctionなどの記述は全部書いてありますので、これらは省略します。
JavaScriptコードの実行はとても簡単で、基本的にExecuteJavaScript
へ引数を渡せば終わりです。
Window Bindingをしたい時に、OnContextCreated()
メソッドが全く呼ばれない
まず、ClientApp
クラスはCefRenderProcessHandler
を継承する必要があります。
初期化の時にCefSettings settings; settings.single_process = true;
を設定し、singleProcessにしないと各browserのrenderProcessになります。
後はおまけとして、JSObject
とJSFunction
が組み合わせて階層化可能で、javascript:window.JSObject.JSFunction()
という構造を作れます。
実際にはJSFunction
をJSObject
として生成し、親の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のゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。