WinFormsでChromiumブラウザコンポーネント(CefSharp)を使ってみる

# はじめに
これは[Visual Basic Advent Calendar 2018](https://qiita.com/advent-calendar/2018/vb)の19日目の記事となります。

お仕事で WinForms に 標準(IE)のWebブラウザーコントロールを作成したアプリケーションを、「Microsoft Edge」ベースのWebブラウザーコントロールに変更して欲しいという要望がありました。

Windows 10で標準(IE)を長期間使い続けると、応答なしになってしまうとユーザーから指摘があり、Edgeでは長期間でも問題が出なかったとのこと。
確かに標準(IE)のWebブラウザーコントロールは、デフォルトの状態ではIE7互換でありIE11互換で動かすにはレジストリの設定が必要だし、IE自体が他にもいろいろと問題があります。

EdgeベースのWebブラウザーは下記記事をみると、Windows 10 1803以降とVisual Studio 2017以降および、.NET Framework 4.6.2以降が必要である。
[WPFやWindowsフォームでEdgeのWebViewを使うには？［Windows 10 1803以降］](http://www.atmarkit.co.jp/ait/articles/1807/04/news017.html)

Windows 7版(IE) と Windows 10版(Edge) でプログラムを分けるのが面倒だなと、取り掛かりを先延ばししていたところ、下記のニュースが飛び込んできました。
[EdgeブラウザのChromiumベース移行、マイクロソフトが正式発表。Macなど他OSにも提供へ](https://japanese.engadget.com/2018/12/07/edge-chromium-mac-os/)

Chromiumブラウザコンポーネント(CefSharp)の存在を知ったので、提案してみたところ受け入れて頂きました。これならWindows 7とWindows 10で分ける必要もなくなります。

【2022/07/18追記】
今ならWebView2ブラウザコンポーネントがChromium版Edgeになっているので、CefSharpにしなくてもいいです。

https://qiita.com/yaju/items/2e2c7c421a8fa6e7c0a2

# CefSharpとは
CefはChromium Embedded Frameworkの略で、Chromiumをアプリケーションに組み込むためのフレームワークです。
https://bitbucket.org/chromiumembedded/cef

Cefには各言語向けのラッパーがあり、その中のC#実装がCefSharpです。
https://github.com/cefsharp/CefSharp

WPF対応版とWinForms対応のコンポーネントに分かれていて、.NET Framework 4.5.2以上が必要となります。

【2019/01/19追記】
とあるPC環境では「FileNotFoundException: ファイルまたはアセンブリ 'CefSharp.Core ... Core.dll'、またはその依存関係の 1 つが読み込めませんでした。」のエラーが出ました。
原因として「Visual Studio C++ 2012/2013/2015 Redistributables(再頒布可能パッケージ)」が必要となります。2017版でもいいです。

|CefSharp Version|VC++ Version|.Net Version|
|:--|:--|:--|
|65.0.0 and above|2015|4.5.2|
|51.0.0 to 63.0.0|2013|4.5.2|
|45.0.0 to 49.0.0|2013|4.0.0|
|43.0.0 and below|2012|4.0.0|

* [Visual Studio 2015 の Visual C++ 再頒布可能パッケージ ダウンロード](https://www.microsoft.com/ja-jp/download/details.aspx?id=48145)
* [最新のサポートされる Visual C++ のダウンロード](https://support.microsoft.com/ja-jp/help/2977003/the-latest-supported-visual-c-downloads)
* [CefSharp/wiki/Frequently-asked-questions](https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions#Including_vcredist)
* [Output files description table (Redistribution)](https://github.com/cefsharp/CefSharp/wiki/Output-files-description-table-(Redistribution))

# 導入
今回は、NuGetでCefSharp.WinFormsをインストールします。
![NuGet_cefsharp.png](https://qiita-image-store.s3.amazonaws.com/0/14053/d0e4a399-7bdd-aabf-9499-eefcd9789cc7.png)

Any CPUには対応していないのでプラットフォームを x86 のみにしてもいいが、プラットフォームを x86 か x64 を自動で判別する方法がある。
今回は自動判別で対応する。コンパイルのAny CPUに下にある32bit優先チェックは外しておく。

1. NuGetでCefSharp.WinFormsをインストールする
2. 一旦、Visual Studioを終了する。
3. 対象プロジェクト(vbproj or csproj)ファイルの最初のProperyGroupに末尾に「`<CefSharpAnyCpuSupport>true</CefSharpAnyCpuSupport>`」を追加する
4. Visual Studioを起動してプロジェクトを読み込む。
5. ビルドすると Debug または Relese フォルダ内に x86 と x64 フォルダが作成される。

![cefsharp_debug.png](https://qiita-image-store.s3.amazonaws.com/0/14053/d1fdd24f-c0b8-0dfa-7317-505f36baf0c8.png)


C#版は下記サイトで指定されたプログラムを組み込む。 
[CefSharpでAnyCPU対応に苦慮した話2](https://www.valuestar.work/news/archives/27)

Any CPUで32bit優先にした場合(32bit優先とはARMのWindows環境でも動作可能となる)
[How to use CefSharp (chromium embedded framework c#) in a Winforms application](https://ourcodeworld.com/articles/read/173/how-to-use-cefsharp-chromium-embedded-framework-csharp-in-a-winforms-application)

# プログラム
今回のサンプルとしてフォームにMenuStripsをTopに配置し、それ以外はパネルでFillにして配置し、ChromiumWebBrowserコントロールをパネル配下にセットするようにした。
[VB.net(WinForms)でのCefsharpの基本的な扱い方について](https://qiita.com/masteralice3104/items/1ac0b86dba3ad5234c62)
C#版は下記サイトを参考
[CefSharpでAnyCPU対応に苦慮した話2](https://www.valuestar.work/news/archives/27)

フォーム表示前に x86 か x64 の自動判別処理をする必要があります。
新しいモジュール(例 Program.vb)を追加し、その中に Sub Main を宣言します。
このモジュールを有効にするには、プロジェクトのプロパティ設定で「アプリケーションフレームワークを有効にする」のチェックを外し無効にします。 
無効にすることで、「スタートアップ オブジェクト」のコンボボックスに Sub Main が選択できます。 

![cefSample_Startup.png](https://qiita-image-store.s3.amazonaws.com/0/14053/2cf04d23-a9b3-177b-53c3-f94b97670b95.png)

```vb:Program.vb
Imports System.IO
Imports System.Reflection

Module Program
    <STAThread()>
    Sub Main()
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf Resolve
        Application.Run(New frmMain())
    End Sub

    Function Resolve(ByVal sender As Object, ByVal args As ResolveEventArgs) As [Assembly]
        If args.Name.StartsWith("CefSharp") Then
            Dim assemblyName As String = args.Name.Split({","c}, 2)(0) & ".dll"
            Dim archSpecificPath As String = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                                                          If(Environment.Is64BitProcess, "x64", "x86"), assemblyName)
            Return If(File.Exists(archSpecificPath), Assembly.LoadFile(archSpecificPath), Nothing)
        End If

        Return Nothing
    End Function

End Module
```

```vb:frmMain.vb
Imports System.IO
Imports CefSharp.WinForms

Public Class frmMain

    Private _webBrowser As ChromiumWebBrowser

    Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' カレントディレクトリをアプリケーション起動パスに設定する
        Dim appPath As String = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)
        Directory.SetCurrentDirectory(appPath)

        Dim settings As CefSettings = New CefSettings()
        settings.BrowserSubprocessPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                                                      If(Environment.Is64BitProcess, "x64", "x86"), "CefSharp.BrowserSubprocess.exe")
        ' 日本語に設定する
        settings.Locale = Globalization.CultureInfo.CurrentCulture.Parent.ToString()
        settings.AcceptLanguageList = Globalization.CultureInfo.CurrentCulture.Name
        ' ユーザーデータの保存先を設定する
        settings.UserDataPath = appPath
        CefSharp.Cef.Initialize(settings, performDependencyCheck:=False, browserProcessHandler:=Nothing)

        ' サイト読み込みと表示
        _webBrowser = New ChromiumWebBrowser("https://www.google.co.jp/")
        Me.pnlBrowser.Controls.Add(_webBrowser)
        _webBrowser.Dock = DockStyle.Fill
    End Sub

    Private Sub frmMain_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles MyBase.FormClosing
        CefSharp.Cef.Shutdown()
    End Sub

End Class
```

# 結果
![CefSample.png](https://qiita-image-store.s3.amazonaws.com/0/14053/5e3e0a43-fbe8-8397-cd91-af9405715362.png)

## 初回起動が遅い
.NETは初回起動が遅いです。上記のGoogleのサイトが開くまで真っ白な画面が10秒くらい続きます。次からは速いんですけどね。
速くなる対処方法はないんですが、Web画面でよくあるローディングGif画像を表示して体感速度を速くします。
pictureBoxコントロール(例 picLoading)にローディングGifをセットしてSizeModeをCenterImage、DockをDockStyle.Fillにしておきます。

```vb:frmMain.vb
' サイト読み込みと表示
_webBrowser = New ChromiumWebBrowser("https://www.google.co.jp/")
Me.pnlBrowser.Controls.Add(picLoading)
Me.pnlBrowser.Controls.Add(_webBrowser)
_webBrowser.Dock = DockStyle.Fill
```
最初に追加されたコントロールが最前面になるコントロールとなります。
これで真っ白な画面上にローディングGif画像を表示されるようになりました。
ローディングGifを非表示にするタイミングは、読み込み完了時または接続エラー時ですね。

# 補足
## ブラウザ言語を変更する
表示サイトによってはひらがなと漢字が混在していた場合、ひらがなのみ太いなどのフォント表示(中国語っぽい)がされてしまう問題点があった。

原因はロケールが違うため、言語を日本語に変更する。
[CefSharp.Wpfのlocaleを変更する - stackoverflow](https://ja.stackoverflow.com/questions/12710/cefsharp-wpf%E3%81%AElocale%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B)

```vb
Dim settings As CefSettings = New CefSettings()
settings.Locale = "ja"
settings.AcceptLanguageList = "ja-JP"
CefSharp.Cef.Initialize(settings)
```

または、OSのカルチャーを取得してセットする。

```vb
Dim settings As CefSettings = New CefSettings()
settings.Locale = Globalization.CultureInfo.CurrentCulture.Parent.ToString()
settings.AcceptLanguageList = Globalization.CultureInfo.CurrentCulture.Name
CefSharp.Cef.Initialize(settings)
```

## debug.logを出力しない
デフォルトでは debug.log が出力されますので、LogSeverity プロパティを無効(Disable)にセットします。

```vb
Dim settings As CefSettings = New CefSettings()
settings.LogSeverity = LogSeverity.Disable
CefSharp.Cef.Initialize(settings)
```

## GPUCacheやblob_storageの保存先
実行ファイルを直接実行した場合には実行ファイルと同じところに GPUCache や blob_storage フォルダが自動作成された。

しかし、インストーラーで CefSharp を使用したアプリケーションをデスクトップのショートカットに作成(ショートカットの作業フォルダが空の状態)した際、ショートカットから実行すると GPUCache と blob_storage フォルダがデスクトップに作成されてしまう問題があった。

### 対応１
ショートカットのプロパティで作業フォルダを指定することで、作業フォルダ先にGPUCache と blob_storage フォルダが作成されるようになる。

インストーラーでショートカットキーを作成する際に作業フォルダをインストーラー先に指定するようにした。

### 対応２
ショートカットの作業フォルダが空のままの状態でも対応する。
プログラムで対応する場合、カレントフォルダを設定しただけでは、GPUCacheフォルダはカレントフォルダに作成されるが、blob_storageフォルダはデスクトップに作成されてしまった。
対応として、`CefSettings.UserDataPath` に保存先フォルダを指定する必要がある。

```vb
' カレントディレクトリをアプリケーション起動パスに設定する
Dim appPath As String = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)
Directory.SetCurrentDirectory(appPath)

Dim settings As CefSettings = New CefSettings()
' ユーザーデータの保存先を設定する
settings.UserDataPath = appPath
CefSharp.Cef.Initialize(settings)
```
## Cef.Initializeの設定は1回のみ
Cef.Initializeの設定は1回のみで、new ChromiumWebBrowser する前に設定する。
Cef.Initialize したかどうかは、Cef.IsInitialized で判断(false:未設定、true:設定済)できる。
[C# (CSharp) CefSettings Code Examples](https://csharp.hotexamples.com/examples/-/CefSettings/-/php-cefsettings-class-examples.html)

# アプリケーションの配布
CefSharpのWinFormアプリケーション、.NET Framework 4.6以上の再頒布可能パッケージ、Visual Studio C++ 2015 Redistributable(再頒布可能パッケージ x86とx64)、これに加えてCefSharpの関連モジュールのx86フォルダが145Mbyte、x64フォルダが162MByte と大量です。AnyCPUにしなければ片方のCPUのみで減らすことができますが・・・。

標準(IE)のWebブラウザーコントロールのWinFormアプリケーションが、必要な.NET Frameworkの再頒布可能パッケージくらいで済むのに比べるとちょっとね。

今の時代は気にしちゃいけないのかな。

追記：
[ブルネイ工廠電気実験部 「艦隊これくしょん」用補助ツール「七四式電子観測儀」](http://electronicobserver.blog.fc2.com/blog-entry-104.html)の配布ファイルを見るとZIP圧縮して109MByte(AnyCPU、再頒布可能パッケージは除く)ですね。あと、CefSharpの関連モジュール内のpdbファイルとXMLファイルは除去されているので、これで各10MByteは減らせます。

# 最後に
企業によっては、MicrosoftのIEおよびEdge以外は駄目というところがある。
しかし、MicrosoftがEdgeブラウザのレンダリングエンジンは独自のEdgeHTMLからChromiumが使用するBlinkへと切り替え、Windows 7、8でも作動する新しいEdgeを発表するとなるといささか楽になる。

# 終了時に落ちる
【2019/01/19追記】
ほぼ初期状態のWindow 7のPC上に最低限の.NET Framework 4.6とVisual Studio 2015 の Visual C++ 再頒布可能パッケージをインストールした状態だとアプリケーションの終了時に落ちてしまいます。
イベントログには障害が発生しているモジュール名: libcef.dll、バージョン: 3.3497.1841.0、タイム スタンプ: 0x5bcada64 例外コード: 0xc0000005 が出ています。

調査用に32bit(x86)上でブラウザを表示するだけのミニプログラム(New ChromiumWebBrowser と  Cef.Shutdownを実装)を作成して動かすと正常終了するが、Cef.Initialize(settings) を入れると落ちることが分かっています。
他PCでは正常終了するので何かが足りないんだと思うのですが、まだ何かが分かってません。
[Cef.Shutdown() hangs if ChromiumWebBrowser is already disposed](https://github.com/cefsharp/CefSharp/issues/820)のkevinWu7のコメントをやってみる予定。

## 解決
【2019/01/27追記】
実機で確認してたのですが、ウィルスバスターをインストールしたら正常終了してしまったり、ウィルスバスターをアンイストールしても現象が出なくなり、最初からインストールし直し、今度は異常終了が出た時点で復元ポイントでバックアップしたら、正常終了するようになってしまったりと原因不明かつ検証が面倒。そこで実機は諦めVirtualBoxで仮想環境を作成、異常終了が出た時点でスナップショットを取りこれで何度でも確認できるようになりました。

### 対応
プロセス終了時に異常終了することから、Cef.Shutdown() メソッドが怪しい。
Cef.Initialize メソッドより前に、`CefSharpSettings.ShutdownOnExit = false` を追加します。次に Closing イベントの Cef.Shutdown() メソッドをコメントアウトまたはそもそも呼ばない。

別に Cef.Shutdown() メソッドを呼ばなくても、CefSharp.BrowserSubprocess.exe のプロセスはきちんと終了してくれました。

# マウスカーソルの位置とクリックに反応する位置がずれる
一部のHigh-DPI 環境のPCでクリックする位置が10ドット上くらいでないと反応しない現象があった。また、画面上部に黒く太い線が表示されていた。

同僚のPCではEXEファイルから直接起動すると現象が発生しないが、デスクトップのショートカットやスタートアップから起動するとこの現象が発生した。

## 暫定対応
ショートカットのプロパティの互換性タブにある「高DPIスケール設定の上書き」の「高いDPIスケールの動作を上書きします。」にチェックを付ける。
Windows 7では互換性タブにある「設定」の「高DPI設定では画面のスケーリングを無効にする」にチェックを付ける。「高DPI設定では画面のスケーリングを無効にする」にチェックを付ける。

参照：[ブルネイ工廠電気実験部 七四式電子観測儀 ver. 4.0.0 コメント欄](http://electronicobserver.blog.fc2.com/blog-entry-103.html#comment)

## 恒久対応
プログラムにて高DPI対応するために、app.manifest ファイルを追加して、dpiAwareをtrueに設定する。
manifest ファイルの配布は不要。
[アプリの高DPI(High DPI)対応について 第3回 ～ マニフェストでアプリのDPI対応レベルを変更する ～](https://blogs.msdn.microsoft.com/ttanaka/2014/08/22/dpihigh-dpi-3-12503/)

「dpiAware」の設定を「true」または「true/PM」または「per monitor」に設定する。
違いは下記参照
[[C#][VB.NET]Windows Formアプリケーションで表示がぼやけるのを防ぐ](http://blog.itparadise.jp/?p=802)

```xml:app.manifest
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
    </windowsSettings>
  </application>
</assembly>
```

プログラム上では、Cef.EnableHighDPISupport() メソッドを追加する。

```vb
CefSharp.Cef.EnableHighDPISupport()
CefSharp.Cef.Initialize(setting)
```