C# Windows Forms の WebView2 で現在表示しているURLを確実に取得する方法

C# の Windows Forms で WebView2 を使うとき、「今表示している URL を取りたいのに、いつまで経っても about:blank しか返ってこない……」という悩みはとてもよくあります。原因は WebView2 のナビゲーションが非同期で動いていることにあります。本記事では、なぜそうなるのか、そして NavigationCompleted イベントを使って「現在表示中の URL」を確実に取得するための実装パターンを、サンプルコードとともに丁寧に解説します。

WebView2+Windows Forms で「現在の URL」を確実に取得する方法

Windows Forms アプリケーションで WebView2 コントロールを配置し、初期ページから別ページへ遷移したあとに「今表示している URL」を取得したい――というシナリオは非常に一般的です。しかし、フォームロード直後などに webView21.Source.ToString() を呼び出すと、期待した URL ではなく about:blank が返ってしまうことがあります。

これは WebView2 が「非同期でページ遷移を行うコンポーネント」であることを理解すれば納得できます。正しいタイミングで URL を読み取れば、必ず最新の URL を取得できます。本記事では以下の流れで解説します。

  • なぜ about:blank になってしまうのか(原因)
  • NavigationCompleted イベントを使った正しい取得タイミング
  • Task.Delay では解決しない理由
  • EnsureCoreWebView2Async を忘れたときの症状と対策
  • 実用的なアドレスバー付きブラウザ風 UI のサンプル
  • 複数回の遷移やフレーム単位の遷移を扱う応用テクニック
目次

よくある症状:フォームロード直後は about:blank しか返らない

まずは典型的な「うまくいかない例」から見てみましょう。


// NG パターン:Form.Load 直後に URL を取得しようとしている例
private void Form1_Load(object sender, EventArgs e)
{
    webView21.Source = new Uri("https://example.com");

    // ここで現在の URL を取得したい…
    string currentUrl = webView21.Source.ToString();
    MessageBox.Show(currentUrl);  // <= about:blank になることがある
}

このコードでは、Form.Load の中で webView21.Source に URL を設定し、その直後に webView21.Source.ToString() で現在の URL を読み取ろうとしています。しかし実際には、ユーザーに表示されているページがまだ読み込み中、あるいは WebView2 の内部初期化が完了していないタイミングで参照してしまっているため、初期値の about:blank が返ってくるのです。

なぜ about:blank が返ってくるのか

原因をもう少し整理してみます。

現象原因
webView21.Source.ToString() が about:blank を返すWebView2 のナビゲーションがまだ開始・完了していない初期状態のため
ナビゲーション完了後は正しい URL が返るページ読み込みが完了すると Source が実際の URL に更新されるため
ネットワークが遅いほど再現しやすいフォームが表示されてからページ読み込み完了までの時間が長くなるため

つまり、「フォームロード直後」というタイミングを基準に考えるのではなく、「WebView2 がナビゲーションを完了したタイミング」を基準にコードを書く必要があります。

WebView2 のナビゲーションの流れを理解する

WebView2 で URL を変更する主な方法は以下の 2 つです。

  • webView21.Source = new Uri("https://example.com");
  • webView21.CoreWebView2.Navigate("https://example.com");

いずれの場合も、「URL をセットした瞬間にページが読み込み済みになる」わけではなく、「非同期でページ読み込みが開始される」だけです。読み込みの進行状況は複数のイベントで通知されます。

イベント名タイミング用途の例
NavigationStarting新しいナビゲーションが始まった直後ナビゲーションキャンセル、ローディング表示の開始
SourceChanged表示中の URL が変わった直後(遷移途中も含む)アドレスバーの更新、履歴管理など
NavigationCompletedナビゲーションが完了したタイミング「今表示している URL」を確定させる処理、UI の更新
CoreWebView2.FrameNavigationCompletedフレーム(iframe など)のナビゲーション完了フレーム単位での監視が必要な場合

「現在の URL」を確実に取得したい場合は、基本的に NavigationCompleted を使えば OK です。これ以外のタイミングでは、まだリダイレクト中だったり、エラーページへの遷移中だったりと、「最終的に表示される URL」が確定していないことがあります。

NavigationCompleted イベントで現在の URL を取得する基本パターン

ここからは、実際に動く C# コードで解決策を見ていきます。ポイントは以下の 3 つです。

  • EnsureCoreWebView2Async を await してからナビゲーションする
  • NavigationCompleted イベントで webView21.Source を読む
  • e.IsSuccess でページ読み込み成功を確認する

using Microsoft.Web.WebView2.Core;

private async void InitBrowser()
{
    // 1. WebView2 の初期化。完了するまで必ず待つ
    await webView21.EnsureCoreWebView2Async(null);

    // 2. ナビゲーション完了時のイベントハンドラを登録
    webView21.NavigationCompleted += WebView21_NavigationCompleted;

    // 3. ページにナビゲート(この呼び出し自体は非同期で進む)
    webView21.CoreWebView2.Navigate("https://example.com");
}

private void WebView21_NavigationCompleted(
    object sender,
    CoreWebView2NavigationCompletedEventArgs e)
{
    // 4. ナビゲーションが成功したときだけ処理する
    if (e.IsSuccess)
    {
        string currentUrl = webView21.Source.ToString();
        MessageBox.Show(currentUrl); // <= 正しい「現在の URL」が取得できる
    }
    else
    {
        // 失敗時にログを出したり、エラー画面を表示したりできる
        // e.WebErrorStatus や e.HttpStatusCode も参照可能
    }
}

このパターンのポイント

  • NavigationCompleted イベントは、ユーザーがリンクをクリックして遷移した場合でも毎回発火するため、「常に最新の URL を追いかける」ことができます。
  • webView21.Source は Uri 型なので、.ToString() で文字列として扱えます。
  • フォームロードの中で直接 URL を取得するのではなく、「イベントの中で最新状態を読む」という設計が重要です。

アドレスバー付きミニブラウザを作ってみる

実用例として、テキストボックスをアドレスバーとして使い、「常に現在の URL を表示・編集できる」ミニブラウザ風 UI を作ってみましょう。

フォーム上のコントロール構成

  • TextBox txtUrl … アドレスバー
  • Button btnGo … 指定した URL に移動
  • WebView2 webView21

コード例


using System;
using System.Windows.Forms;
using Microsoft.Web.WebView2.Core;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        // フォームロード時に初期化を開始
        this.Load += Form1_Load;
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        // WebView2 の初期化
        await webView21.EnsureCoreWebView2Async(null);

        // ナビゲーション完了イベントを登録
        webView21.NavigationCompleted += WebView21_NavigationCompleted;

        // 初期ページへ移動
        NavigateTo("https://example.com");
    }

    private void NavigateTo(string url)
    {
        if (webView21.CoreWebView2 == null)
        {
            return;
        }

        // 足りないスキームを補う簡易処理(http を付けるなど)
        if (!url.StartsWith("http://") && !url.StartsWith("https://"))
        {
            url = "https://" + url;
        }

        webView21.CoreWebView2.Navigate(url);
    }

    private void btnGo_Click(object sender, EventArgs e)
    {
        NavigateTo(txtUrl.Text);
    }

    private void WebView21_NavigationCompleted(
        object sender,
        CoreWebView2NavigationCompletedEventArgs e)
    {
        if (!e.IsSuccess)
        {
            // エラー表示など
            MessageBox.Show("ページの読み込みに失敗しました。");
            return;
        }

        // ナビゲーション完了時点の URL をアドレスバーに反映
        string currentUrl = webView21.Source.ToString();
        txtUrl.Text = currentUrl;
    }
}

このようにしておけば、ユーザーがリンクをクリックして別ページに移動しても、ナビゲーション完了のタイミングでテキストボックスが最新の URL に更新されます。

Task.Delay での「とりあえず待つ」はなぜダメなのか

よくある誤解として、「Navigate() したあとに Task.Delay(1000) で 1 秒待てば、さすがにページ読み込みは終わっているだろう」という考え方があります。しかしこれは非常に危険です。

理由を表にまとめてみます。

やり方短所長所
Task.Delay で固定時間待つネットワークやサーバの状況によって読み込み時間が変動するため、
早すぎると about:blank、遅すぎると UX が悪化する
実装は簡単に見えるが、実用レベルでは使えない
NavigationCompleted を待つイベント駆動の考え方に慣れる必要がある読み込み完了の「本物のタイミング」で処理できるため、安定して動作する

WebView2 はブラウザコンポーネントなので、「ネットワークが早い環境」「社内プロキシを通る環境」「海外の遅いサーバにアクセスする環境」など、さまざまな条件で動作します。どんな環境でも正しく動くコードにするには、「固定時間待つ」のではなく「完了イベントを基準に処理する」のが鉄則です。

EnsureCoreWebView2Async を忘れるとどうなるか

もう一つハマりがちなポイントとして、EnsureCoreWebView2Async(null) を await していない ことが挙げられます。これを怠ると、さまざまな不可解な症状が発生します。

  • NavigationCompleted が発火しない
  • CoreWebView2 が null のままで NullReferenceException が発生する
  • フォームによっては動いたり動かなかったりと、環境依存の挙動になる

正しい初期化パターンは、次のように「フォームロードなどのタイミングで await する」形です。


private async void Form1_Load(object sender, EventArgs e)
{
    // ここで WebView2 を初期化し、完了するまで待つ
    await webView21.EnsureCoreWebView2Async(null);

    // CoreWebView2 が必ず生成されている状態でイベントを登録する
    webView21.NavigationCompleted += WebView21_NavigationCompleted;

    webView21.CoreWebView2.Navigate("https://example.com");
}

Form1_Load を async void にするのは WinForms では一般的なパターンです(イベントハンドラなので Task を返せない)。このパターンにしておくと、フォーム表示中に自然な形で WebView2 の初期化が行われ、ナビゲーションやイベント登録が安定して動作します。

複数回の遷移を監視する場合の考え方

ブラウザとして使う以上、「初回のナビゲーションだけ」ではなく、「ユーザーがリンクをクリックするたびに現在の URL を知りたい」ことが多いはずです。NavigationCompleted イベントは、毎回のナビゲーションで発火するので、そのまま使えます。

常に最新 URL をラベルに表示する例

フォームに Label lblStatus を追加し、そこに現在の URL を表示してみます。


private void WebView21_NavigationCompleted(
    object sender,
    CoreWebView2NavigationCompletedEventArgs e)
{
    if (!e.IsSuccess)
    {
        lblStatus.Text = "ナビゲーションに失敗しました。";
        return;
    }

    string currentUrl = webView21.Source.ToString();
    lblStatus.Text = $"現在の URL: {currentUrl}";
}

このようにしておけば、リンククリックや JavaScript による遷移(location.href の変更など)も含めて、ユーザーから見える「今表示している URL」を常に追跡できます。

NavigationStarting / SourceChanged との使い分け

場合によっては、ナビゲーションの「開始」と「完了」を区別したくなることがあります。そのときは以下のように整理すると分かりやすくなります。

イベントURL の取得タイミング主な用途
NavigationStartingこれから遷移しようとしている URL(確定前)危険なサイトのブロック、ナビゲーションのキャンセル
SourceChanged途中のリダイレクトや hash 変更も含めて「URL が変わった瞬間」アドレスバーの即時更新、履歴の記録
NavigationCompleted最終的に表示される URL画面表示が落ち着いた後の UI 更新、ログ記録

「確実に、最終的な表示 URL を知りたい」という目的であれば、やはり NavigationCompleted を使うのがもっともシンプルで安全です。

フレーム単位の遷移を区別したい場合

最近の Web ページでは、<iframe> を使って部分的に別サイトを読み込んだり、広告を表示したりすることがよくあります。これらも内部的にはナビゲーションとして扱われるため、「メインフレームの遷移」と区別したくなることがあります。

そのような場合は、CoreWebView2.FrameNavigationCompleted イベントを使うことで、フレーム単位のナビゲーションを個別に監視できます。


private async void Form1_Load(object sender, EventArgs e)
{
    await webView21.EnsureCoreWebView2Async(null);

    // フレーム単位のナビゲーション完了も監視する
    webView21.CoreWebView2.FrameNavigationCompleted += CoreWebView2_FrameNavigationCompleted;

    webView21.NavigationCompleted += WebView21_NavigationCompleted;
    webView21.CoreWebView2.Navigate("https://example.com");
}

private void CoreWebView2_FrameNavigationCompleted(
    object sender,
    CoreWebView2NavigationCompletedEventArgs e)
{
    // ここで e.FrameId などを見て、特定のフレームだけ処理することも可能
    // 通常、「現在の URL」を知りたいだけならメインの NavigationCompleted で十分
}

「現在表示している URL」だけであればメインフレームだけ見れば良いので、通常はここまで細かく扱う必要はありません。ですが、「特定の iframe のロード完了を待ってから処理したい」といった高度なシナリオでは役に立つ知識です。

同期的に URL が欲しいときのラッパーメソッド例

ときどき、「メソッドの呼び出しとして GetCurrentUrlAsync() のような形にしたい」ということもあると思います。その場合、NavigationCompleted を内部で待ち受けるラッパーメソッドを用意しておくと、コードがすっきりします。


using System.Threading.Tasks;
using Microsoft.Web.WebView2.Core;

public Task&lt;string&gt; NavigateAndGetFinalUrlAsync(string url)
{
    var tcs = new TaskCompletionSource&lt;string&gt;();

    CoreWebView2NavigationCompletedEventHandler handler = null;
    handler = (sender, e) =&gt;
    {
        webView21.NavigationCompleted -= handler;

        if (!e.IsSuccess)
        {
            tcs.SetException(new InvalidOperationException(
                "ナビゲーションに失敗しました。"));
            return;
        }

        string currentUrl = webView21.Source.ToString();
        tcs.SetResult(currentUrl);
    };

    webView21.NavigationCompleted += handler;

    // ナビゲーション開始
    webView21.CoreWebView2.Navigate(url);

    // ナビゲーション完了まで待つ Task を返す
    return tcs.Task;
}

このようなメソッドを用意しておけば、呼び出し側では次のように書けます。


private async void btnCheckFinalUrl_Click(object sender, EventArgs e)
{
    string startUrl = txtUrl.Text;
    string finalUrl = await NavigateAndGetFinalUrlAsync(startUrl);

    MessageBox.Show($"最終的に表示された URL: {finalUrl}");
}

リダイレクトを多用するようなサイトにアクセスする場合、「入力した URL」と「最終的に表示された URL」が異なることはよくあります。その差分をログに残したり、認証系の動作確認に使ったりするのにも便利です。

UI スレッドとイベントのスレッド安全性

WinForms アプリケーションでは、UI スレッド以外からコントロールを触ると例外が発生することがあります。しかし、WebView2 の NavigationCompleted などのイベントは、基本的に WinForms の UI スレッド上で呼び出されるため、イベントハンドラ内でそのままラベルやテキストボックスを更新しても問題ありません。

つまり、以下のようなコードは安全に動作します。


private void WebView21_NavigationCompleted(
    object sender,
    CoreWebView2NavigationCompletedEventArgs e)
{
    if (!e.IsSuccess) return;

    // UI コントロールを直接更新して OK
    lblStatus.Text = webView21.Source.ToString();
}

もし、別スレッドから URL を確認したいといった特殊なケースがある場合は、Invoke / BeginInvoke を使って UI スレッドに処理を戻す設計が必要になりますが、「普通に WebView2 のイベントを使う」範囲ではそこまで気にしなくても大丈夫です。

よくある落とし穴とチェックリスト

最後に、「現在の URL を取得できない」ときに確認すべきポイントをチェックリストとしてまとめます。

チェック項目確認内容
EnsureCoreWebView2Async を await しているか初期化完了前に Navigate() やイベント登録をしていないか確認
NavigationCompleted を使っているかフォームロード直後など、ナビゲーション完了前に Source を読んでいないか
Task.Delay でごまかしていないか固定時間待機をやめ、完了イベントの発火を基準に処理するよう修正する
複数回イベント登録していないか同じハンドラを何度も += して、メッセージボックスが複数回出ていないか確認
例外を握りつぶしていないかイベント内で例外が発生しても気づきにくいので、ログ出力や try-catch を活用する

まとめ:WebView2 では「タイミング」を意識して URL を読む

本記事では、Windows Forms の WebView2 コントロールで「現在表示している URL」を取得しようとしたときに、about:blank しか返ってこない問題と、その根本原因・解決策について解説しました。

  • WebView2 のナビゲーションは 非同期 である
  • ナビゲーション完了前に Source を読めば about:blank になるのは自然な挙動
  • NavigationCompleted イベント 内で webView21.Source を読めば、必ず最新の URL が取得できる
  • Task.Delay での時間待ちは根本的な解決にならず、環境によって動いたり動かなかったりする
  • EnsureCoreWebView2Async(null) を await してから Navigate() するのが重要

この考え方さえ押さえておけば、「現在の URL を取得する」だけでなく、「読み込み完了後に DOM を触る」「ページごとの処理を切り替える」「ログや解析用にナビゲーション履歴を記録する」など、さまざまなシーンで安定した実装ができるようになります。

WebView2 は非常に強力なコンポーネントですが、「イベント駆動」「非同期」 という性質を理解することが、トラブルを避ける一番の近道です。ぜひ NavigationCompleted イベントを軸にした実装に書き換えて、「about:blank 問題」から卒業しましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次