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<string> NavigateAndGetFinalUrlAsync(string url)
{
var tcs = new TaskCompletionSource<string>();
CoreWebView2NavigationCompletedEventHandler handler = null;
handler = (sender, e) =>
{
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 問題」から卒業しましょう。

コメント