WebView2をヘッドレスで使用する

WebView2 は Microsoft Edge ブラウザをネイティブアプリに組み込んで Web コンテンツを表示することができるコントロールで、ブラウザアプリやハイブリットアプリの開発に利用できます。

今回は WebView2 のちょっと変わった使用方法である、 GUI を利用しないヘッドレスブラウザとして使用する方法について紹介します。


はじめに

ヘッドレスブラウザとは GUI (ウェブページを表示するための画面)を利用せず、コマンドや API によって操作するウェブブラウザです。ヘッドレスブラウザの主な目的はスクレイピングやテストなど、ウェブブラウザを使用する自動化でしょう。昔はヘッドレス専用の PhantomJS というブラウザも存在していましたが、現在では Chrome 、 Edge 、 Firefox などの主要ブラウザがヘッドレスモードをサポートしており手軽に利用することができます。

本記事では Chrome や Edge 、 Firefox の通常のヘッドレスモードではなく、 Edge をネイティブアプリに組み込むことができる WebView2 をヘッドレスで動作させて Selenium からの操作を試みます。なんとなく筆者が動かしてみたかっただけですので、特にこれと言ったユースケースは思いつかないのですが。

TL;DR

WebView2 をヘッドレスで動作させるためには UI スレッドを立てて CoreWebView2Environment#CreateCoreWebView2ControllerAsync の引数に HWND_MESSAGE を指定します。

試した環境

要素 バージョン
.NET SDK 6.0.411
OS Windows 11 22H2
Microsoft Edge 114.0.1823.67
Microsoft.Web.WebView2 1.0.1823.32

本記事に記載のコードは以下のレポジトリに置いてあります。

github.com

WebView2 をヘッドレスで実行する

WebView2 をヘッドレスで実行する方法として、 WebView2 をコンソールアプリ上で Window を作成せずに実行する方法が挙げられます。実装は GitHub の Issue 1 を参考にしました。

プロジェクトの作成

まずはコンソールアプリのプロジェクト作成です。

Visual Studio からコンソールアプリのプロジェクトを作成し、汎用ホストと WebView2 の NuGet 参照を追加します。また WebView2 は UI コントロールですので、インスタンスの作成や実行など、あらゆる操作には UI スレッドが要求されます。そのため csproj ファイルを直接編集して WPF を有効化します。

<!-- BEFORE -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

<!-- AFTER -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!-- ターゲットフレームワークを net6.0-windows に変更 -->
    <TargetFramework>net6.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <!-- WPF有効化 -->
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <!-- NuGet参照追加 -->
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.*" />
    <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1823.32" />
  </ItemGroup>

</Project>

WebView2 ホストサービスの実装

続いて WebView2 を実行するサービスクラスの実装を行います。 WebView2 をただヘッドレスで動かすだけであれば、以下のコードで済みます。

// Message-Only Windowを作成するためのWindowハンドル
var HWND_MESSAGE = new IntPtr(-3);

// WebView2Controller作成
var env = await CoreWebView2Environment.CreateAsync();
var controller = await env.CreateCoreWebView2ControllerAsync(HWND_MESSAGE);

しかしこれは動作しません。

先に記した通り WebView2 は UI コントロールですので、 UI スレッド( STA スレッド)に紐付いた Dispatcher 経由で操作する必要があります。そのためちょっと手間がかかりますが、このような形になります。

using System.Windows.Threading;
using Microsoft.Extensions.Hosting;
using Microsoft.Web.WebView2.Core;

namespace NX.HeadlessWebView;

public class WebViewHost : IHostedService
{
    private static readonly IntPtr HWND_MESSAGE = new(-3);

    private readonly CoreWebView2Controller _controller;
    private readonly Dispatcher _dispatcher;

    public WebViewHost()
    {
        var dispatcherSource = new TaskCompletionSource<Dispatcher>();
        var controllerSource = new TaskCompletionSource<CoreWebView2Controller>();

        // UI用のスレッドを作成してWebView2Controllerの生成
        var uiThread = new Thread(() =>
        {
            var dispatcher = Dispatcher.CurrentDispatcher;
            dispatcherSource.SetResult(dispatcher);

            dispatcher.Invoke(async () =>
            {
                // WebView2Controllerの生成
                var env = await CoreWebView2Environment.CreateAsync();
                var controller = await env.CreateCoreWebView2ControllerAsync(HWND_MESSAGE);
                controllerSource.SetResult(controller);
            });
            Dispatcher.Run();
        });

        // UIスレッドとして実行
        uiThread.SetApartmentState(ApartmentState.STA);
        uiThread.Start();

        // 作成したDispatcherとWebView2Controllerをフィールドに保持
        _dispatcher = dispatcherSource.Task.Result;
        _controller = controllerSource.Task.Result;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WebView2Controllerの破棄(しているつもりなのだが、エラーが発生しているのか終了しない)
        _dispatcher.Invoke(() =>
        {
            _controller.Close();
        });

        return Task.CompletedTask;
    }
}

筆者はあまりスレッドや Dispatcher に詳しくないためこれで必要十分なのかあまり自信が無いのですが、 WebView2 の生成と実行は意図通りに動作します。

ただし終了処理に問題があり、筆者のコードの問題か WebView2 側の不具合か、以下のエラーが発生して Ctrl + C を2回押さないとコンソールアプリの終了ができません。

[0710/012013.735:ERROR:window_impl.cc(119)] Failed to unregister class Chrome_WidgetWin_0. Error = 0

ホストサービスを実行する

WebView を生成・実行するサービスをホストサービスとして実装しましたので、正しく実行できるように Program.cs を変更します。

以下のように .NET の汎用ホストに対してホストサービスとして登録しましょう。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NX.HeadlessWebView;

await Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<WebViewHost>();
    })
    .RunConsoleAsync();

Selenium から接続する

最後に今回作成したヘッドレス WebView2 に Selenium から接続して操作を行います。

Selenium から接続するためには WebView2 を実行するアプリを環境変数 WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS--remote-debugging-port=9222 とリモートデバッグ用のポート番号を指定します 2 。そして EdgeDriver 生成時のオプションで環境変数に設定したリモートでバッグ用のポート番号を設定します。

var options = new EdgeOptions
{
    DebuggerAddress = "localhost:9222"
};
var driver = new EdgeDriver(options);

おわりに

WebView2 をヘッドレスで実行する方法について紹介しました。

ほとんどのユースケースではわざわざ ヘッドレス WebView2 を作らずとも普通のヘッドレス Edge で良いはずですので、なにがしかの理由があってどうしても使用してみたいとなったときの参考にでもなれば幸いです。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧