WebView2 を使った WinForms アプリで「右クリック=既定の Edge 風メニュー」ではなく、Windows Forms 標準の ContextMenuStrip を出したい——現場でよくある要望です。本記事は VB.NET での実装を、最小構成から高 DPI・キーボード操作・リンク/画像など対象要素に応じたメニュー切替まで、実務向けに整理して解説します。コピペ可能なコードとチェックリスト付き。
質問の要点(再掲)
WebView2 コントロール上で右クリックした際、標準の Edge ベースのメニューではなく、ContextMenuStrip を表示したい。必要な手順と VB.NET のサンプルコードは?
結論(先に手順を表で確認)
| 手順 | 要点 | 補足 |
|---|---|---|
| 1. ContextMenuRequested を購読 | WebView2.ContextMenuRequested で右クリック/長押し/Shift+F10 を捕捉 | EnsureCoreWebView2Async() 完了後に購読 |
| 2. 既定メニューの抑止 | ハンドラで e.Handled = True | 抑止しないと既定メニューも出て二重表示 |
| 3. ContextMenuStrip を生成 | ContextMenuStrip + ToolStripMenuItem を通常どおり作成 | クリック処理は各 ToolStripMenuItem.Click で記述 |
| 4. 右クリック位置に表示 | cm.Show(WebView2, e.Location) | 高 DPI 環境で座標がずれる場合はスケール補正 |
対応環境
- 言語:VB.NET(.NET 6/7/8 もしくは .NET Framework 4.8 以上)
- UI:Windows Forms
- Web コンポーネント:Microsoft Edge WebView2(ランタイムの導入が必要)
最小構成サンプル(コピペで動作)
フォームに WebView2(名前:WebView21)を 1 つ貼り付け、以下を記述します。
Imports Microsoft.Web.WebView2.Core
Public Class Form1
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' WebView2 の初期化
Await WebView21.EnsureCoreWebView2Async(Nothing)
' (任意)既定のコンテキストメニューを全体で無効化したい場合
' WebView21.CoreWebView2.Settings.AreDefaultContextMenusEnabled = False
' 初期ページ
WebView21.Source = New Uri("https://example.com")
End Sub
' 右クリック等の要求を捕捉して独自メニューを出す
Private Sub WebView21_ContextMenuRequested(
sender As Object,
e As CoreWebView2ContextMenuRequestedEventArgs) _
Handles WebView21.ContextMenuRequested
' 既定メニューを抑止
e.Handled = True
' 独自メニューを作成
Dim cm As New ContextMenuStrip()
Dim itemReload As New ToolStripMenuItem("ページを再読み込み")
AddHandler itemReload.Click,
Sub() WebView21.CoreWebView2.Reload()
Dim itemCopy As New ToolStripMenuItem("コピー(選択テキスト)")
AddHandler itemCopy.Click,
Async Sub()
Try
' 選択テキストを取得してクリップボードへ
Dim json = Await WebView21.CoreWebView2.ExecuteScriptAsync(
"window.getSelection().toString()")
Dim text = System.Text.Json.JsonSerializer.Deserialize(Of String)(json)
If Not String.IsNullOrEmpty(text) Then Clipboard.SetText(text)
Catch
End Try
End Sub
cm.Items.AddRange({itemReload, itemCopy})
' 右クリック位置で表示(ズレる場合は後述の DPI 補正を適用)
cm.Show(WebView21, e.Location)
End Sub
End Class
実務で使える完成形サンプル(要素に応じてメニュー切替・DPI/キーボード配慮)
以下はリンク上/画像上/編集可能フィールド上などに応じて動的に項目を切り替え、戻る/進むや印刷・DevTools も備えた実用寄りの例です。
Imports Microsoft.Web.WebView2.Core
Imports System.Diagnostics
Imports System.Text.Json
Public Class Form1
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 初期化
Await WebView21.EnsureCoreWebView2Async(Nothing)
WebView21.CoreWebView2.Settings.AreDefaultContextMenusEnabled = False ' 任意
WebView21.Source = New Uri("https://www.bing.com")
End Sub
Private Sub WebView21_ContextMenuRequested(
sender As Object,
e As CoreWebView2ContextMenuRequestedEventArgs) _
Handles WebView21.ContextMenuRequested
e.Handled = True
Dim cm As New ContextMenuStrip()
' 共通項目
Dim mnuBack As New ToolStripMenuItem("戻る") With {
.Enabled = WebView21.CoreWebView2.CanGoBack
}
AddHandler mnuBack.Click, Sub() WebView21.CoreWebView2.GoBack()
Dim mnuForward As New ToolStripMenuItem("進む") With {
.Enabled = WebView21.CoreWebView2.CanGoForward
}
AddHandler mnuForward.Click, Sub() WebView21.CoreWebView2.GoForward()
Dim mnuReload As New ToolStripMenuItem("再読み込み")
AddHandler mnuReload.Click, Sub() WebView21.CoreWebView2.Reload()
Dim mnuPrint As New ToolStripMenuItem("印刷...")
AddHandler mnuPrint.Click, Sub()
Try
WebView21.CoreWebView2.ShowPrintUI()
Catch
End Try
End Sub
Dim mnuDevTools As New ToolStripMenuItem("開発者ツール")
AddHandler mnuDevTools.Click, Sub() WebView21.CoreWebView2.OpenDevToolsWindow()
' 対象要素に応じた項目
Dim target = e.ContextMenuTarget ' 要素情報
' 1) リンク上
Dim mnuOpenLink As ToolStripMenuItem = Nothing
Dim mnuCopyLink As ToolStripMenuItem = Nothing
If target IsNot Nothing AndAlso
target.Kind = CoreWebView2ContextMenuTargetKind.Link Then
Dim link = target.LinkUri
mnuOpenLink = New ToolStripMenuItem("リンクを既定ブラウザーで開く")
AddHandler mnuOpenLink.Click,
Sub()
If Not String.IsNullOrEmpty(link) Then
Process.Start(New ProcessStartInfo(link) With {.UseShellExecute = True})
End If
End Sub
mnuCopyLink = New ToolStripMenuItem("リンクのアドレスをコピー")
AddHandler mnuCopyLink.Click,
Sub()
If Not String.IsNullOrEmpty(link) Then Clipboard.SetText(link)
End Sub
End If
' 2) 画像上
Dim mnuCopyImageUrl As ToolStripMenuItem = Nothing
If target IsNot Nothing AndAlso
target.Kind = CoreWebView2ContextMenuTargetKind.Image Then
Dim src = target.SourceUri ' 画像の URL
mnuCopyImageUrl = New ToolStripMenuItem("画像アドレスをコピー")
AddHandler mnuCopyImageUrl.Click,
Sub()
If Not String.IsNullOrEmpty(src) Then Clipboard.SetText(src)
End Sub
End If
' 3) テキスト選択あり
Dim mnuCopySel As New ToolStripMenuItem("選択テキストをコピー")
AddHandler mnuCopySel.Click,
Async Sub()
Try
Dim json = Await WebView21.CoreWebView2.ExecuteScriptAsync(
"window.getSelection().toString()")
Dim text = JsonSerializer.Deserialize(Of String)(json)
If Not String.IsNullOrEmpty(text) Then Clipboard.SetText(text)
Catch
End Try
End Sub
' 4) 編集可能要素上
Dim mnuPaste As ToolStripMenuItem = Nothing
If target IsNot Nothing AndAlso target.IsEditable Then
mnuPaste = New ToolStripMenuItem("貼り付け")
AddHandler mnuPaste.Click,
Async Sub()
Try
Dim clip = If(Clipboard.ContainsText(), Clipboard.GetText(), "")
If Not String.IsNullOrEmpty(clip) Then
Dim js = $"(function(t){{
var el=document.activeElement;
if(el && el.isContentEditable){{document.execCommand('insertText', false, t);}}
else if(el && 'value' in el){{
var s=el.selectionStart||0, e=el.selectionEnd||0;
el.value = el.value.substring(0,s) + t + el.value.substring(e);
}}
}})({JsonSerializer.Serialize(clip)});"
Await WebView21.CoreWebView2.ExecuteScriptAsync(js)
End If
Catch
End Try
End Sub
End If
' メニュー構成(仕切りも適宜追加)
cm.Items.AddRange({mnuBack, mnuForward, mnuReload})
cm.Items.Add(New ToolStripSeparator())
If mnuOpenLink IsNot Nothing Then cm.Items.Add(mnuOpenLink)
If mnuCopyLink IsNot Nothing Then cm.Items.Add(mnuCopyLink)
If mnuCopyImageUrl IsNot Nothing Then cm.Items.Add(mnuCopyImageUrl)
cm.Items.Add(mnuCopySel)
If mnuPaste IsNot Nothing Then cm.Items.Add(mnuPaste)
cm.Items.Add(New ToolStripSeparator())
cm.Items.AddRange({mnuPrint, mnuDevTools})
' 位置決定:キーボード(Shift+F10)等で座標が負の場合のフォールバック
Dim pt = e.Location
If pt.X < 0 OrElse pt.Y < 0 Then
pt = WebView21.PointToClient(Cursor.Position)
End If
' 高 DPI でズレる環境向け:必要時のみ補正(96DPI 基準)
' Dim scale As Double = WebView21.DeviceDpi / 96.0
' pt = New Point(CInt(pt.X / scale), CInt(pt.Y / scale))
cm.Show(WebView21, pt)
End Sub
End Class
要点の詳解
なぜ ContextMenuRequested なのか
WebView2 はブラウザー由来の既定メニューを持ちます。単純に Settings.AreDefaultContextMenusEnabled = False で無効化すると確かに既定メニューは消せますが、「クリック位置」や「対象要素(リンク/画像/選択テキスト)」の情報が得られません。ContextMenuRequested なら CoreWebView2ContextMenuRequestedEventArgs 経由で ContextMenuTarget(対象の種類や URI など)と Location(座標)が取得でき、さらに Handled = True で既定メニューを確実に抑止できます。
初期化のタイミング
EnsureCoreWebView2Asyncが完了する前にCoreWebView2を触ると NullReferenceException の原因になります。Form_LoadをAsync化してAwaitするのが安全。完了後にイベント購読や設定変更を行いましょう。
ContextMenuTarget でできること
| 代表プロパティ | 用途 | 例 |
|---|---|---|
Kind(Link/Image/Editable など) | 対象の種類でメニューを切り替える | リンク上のみ「新しいウィンドウで開く」を表示 |
LinkUri | リンク URL を取得 | 「リンクのアドレスをコピー」 |
SourceUri | 画像/メディアの URL を取得 | 「画像を新しいタブで開く」「画像 URL をコピー」 |
IsEditable | 入力欄上かどうか判定 | 編集欄では「貼り付け」「全選択」を表示 |
高 DPI(マルチモニター)で座標がずれるとき
多くの環境では cm.Show(WebView21, e.Location) で問題ありません。ずれる場合はアプリの DPI 対応状況により座標系が一致していない可能性があります。その場合は次のように DeviceDpi を用いて補正してください(96DPI 基準)。
' 必要な場合のみ有効化
Dim scale As Double = WebView21.DeviceDpi / 96.0
Dim pt As New Point(CInt(e.Location.X / scale), CInt(e.Location.Y / scale))
cm.Show(WebView21, pt)
補正の要否は環境差があるため、「そのまま」「補正あり」の両方を試し、最も自然に出る方を採用するのが実務的です。
キーボードとタッチへの配慮
- 長押し(タッチ)や Shift+F10 でも同じイベントが発火します。
- キーボード起因の要求では座標が負値になることがあります。サンプルのように
Cursor.Positionを使ってフォールバックしましょう。
コピーの実装は「Click 時に実行」がおすすめ
選択テキストの有無を事前判定してメニューの有効/無効を切り替える方法もありますが、ハンドラ内で ExecuteScriptAsync を同期的に待つと UI が止まります。最小負荷にするには、メニューは常時有効にして、クリックされたときだけ JavaScript で選択テキストを取得 → クリップボードへ、という流れが快適です。
よくある落とし穴と対策
| 落とし穴 | 症状 | 対策 |
|---|---|---|
CoreWebView2 が未初期化 | Null 参照で例外 | Await EnsureCoreWebView2Async 後にイベント購読・設定 |
Handled を立て忘れ | 既定メニューと自作メニューが同時に出る | 必ず e.Handled = True を先頭で実行 |
| DPI 差による座標ズレ | マルチモニターでメニュー位置がずれる | DeviceDpi による補正、または Cursor.Position フォールバック |
| 貼り付けが効かない | 一部サイトで JS 側制限 | 編集欄では insertText、contenteditable では execCommand など複線対応 |
拡張アイデア(すぐ使えるレシピ)
Alt キーで「既定メニュー」を一時的に許可
デバッグ時など既定メニューを見たいケース向けに「Alt が押されていたら Handled=False」の裏口を用意できます。
If (Control.ModifierKeys And Keys.Alt) = Keys.Alt Then
e.Handled = False ' 既定メニューを見たいとき
Return
End If
メニューの再利用(パフォーマンス)
毎回 ContextMenuStrip を作るのではなく、フォームのフィールドに保持して Opening で有効/無効やテキストを切り替えると割り当てが減り軽量です。
「選択+翻訳」や「選択+検索」
選択文字列を URL クエリに詰めて検索サイトを新ウィンドウで開くレシピです。
Dim mnuSearch As New ToolStripMenuItem("選択文字列を検索")
AddHandler mnuSearch.Click,
Async Sub()
Dim json = Await WebView21.CoreWebView2.ExecuteScriptAsync("window.getSelection().toString()")
Dim text = System.Text.Json.JsonSerializer.Deserialize(Of String)(json)
If Not String.IsNullOrWhiteSpace(text) Then
Dim url = "https://www.bing.com/search?q=" & Uri.EscapeDataString(text)
Process.Start(New ProcessStartInfo(url) With {.UseShellExecute = True})
End If
End Sub
導入から完成までのチェックリスト
- WebView2 ランタイムが PC に入っている
Await EnsureCoreWebView2Asyncを必ず経由ContextMenuRequestedを購読しe.Handled = Trueを先頭で実行- メニューは
ContextMenuStripとToolStripMenuItemで実装 - 位置は
cm.Show(WebView2, e.Location)。ズレたら DPI 補正またはマウス座標フォールバック - 要素別の分岐(リンク/画像/編集可能)で UX を最適化
- Shift+F10/長押しでも同じ体験になるよう配慮
まとめ
WebView2 の ContextMenuRequested を押さえ、e.Handled = True で既定メニューを抑止し、ContextMenuStrip を その場 に表示する——この 3 点が中核です。さらに ContextMenuTarget を活用すれば、リンク・画像・入力欄など対象に応じたプロ仕様の右クリックメニューを実装できます。サンプルをベースに、自社アプリならではの業務メニュー(検索、辞書、社内ツール連携など)へ拡張してください。
付録:最小プロジェクト雛形(Form1.vb 全体)
Imports Microsoft.Web.WebView2.Core
Public Class Form1
Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Await WebView21.EnsureCoreWebView2Async(Nothing)
AddHandler WebView21.ContextMenuRequested, AddressOf WebView21_ContextMenuRequested
WebView21.Source = New Uri("https://example.com")
End Sub
Private Sub WebView21_ContextMenuRequested(
sender As Object,
e As CoreWebView2ContextMenuRequestedEventArgs)
e.Handled = True
Dim cm As New ContextMenuStrip()
Dim itemReload As New ToolStripMenuItem("ページを再読み込み")
AddHandler itemReload.Click, Sub() WebView21.CoreWebView2.Reload()
Dim itemCopy As New ToolStripMenuItem("コピー")
AddHandler itemCopy.Click,
Async Sub()
Try
Dim json = Await WebView21.CoreWebView2.ExecuteScriptAsync(
"window.getSelection().toString()")
Dim text = System.Text.Json.JsonSerializer.Deserialize(Of String)(json)
If Not String.IsNullOrEmpty(text) Then Clipboard.SetText(text)
Catch
End Try
End Sub
cm.Items.AddRange({itemReload, itemCopy})
cm.Show(WebView21, e.Location)
End Sub
End Class
FAQ(現場の「あるある」)
既定メニューは完全に無効化すべき? 多くの業務アプリでは完全無効化で問題ありません。デバッグ時のみ Alt 押下で既定メニューを出す裏口を設けると便利です。 WPF でも同じ? 考え方は同じですが、右クリック位置の座標系 と メニュー表示 API が異なります。WPF の場合は ContextMenu(PlacementTarget/Placement/HorizontalOffset/VerticalOffset)で実装します。 コピーが動かないサイトがある サイト側のスクリプトで制限されている場合があります。サンプルのように選択テキストを .NET 側でクリップボードへ移す方式にしておくと影響を受けにくくなります。
コード断片(すぐ流用できる小ネタ集)
ズーム操作
Dim mnuZoomIn As New ToolStripMenuItem("拡大")
AddHandler mnuZoomIn.Click, Sub() WebView21.ZoomFactor = Math.Min(3.0, WebView21.ZoomFactor + 0.1)
Dim mnuZoomOut As New ToolStripMenuItem("縮小")
AddHandler mnuZoomOut.Click, Sub() WebView21.ZoomFactor = Math.Max(0.25, WebView21.ZoomFactor - 0.1)
Dim mnuZoomReset As New ToolStripMenuItem("等倍")
AddHandler mnuZoomReset.Click, Sub() WebView21.ZoomFactor = 1.0
選択画像をファイル保存(URL からダウンロード)
企業内規約でダウンロード制限がある場合は必ず遵守してください。
Dim mnuSaveImage As New ToolStripMenuItem("画像を保存...")
AddHandler mnuSaveImage.Click,
Async Sub()
Dim target = WebView21.CoreWebView2.Environment ' 環境取得だけの例
' 実装は HttpClient 等で targetUri を保存する(省略)
End Sub
サマリー
WebView2 の独自コンテキストメニューは ContextMenuRequested + Handled + ContextMenuStrip.Show の 3 点で成立します。あとは ContextMenuTarget を使って「どの上を右クリックしたのか」を読み取り、必要なメニューを差し替えるだけ。高 DPI やキーボード操作にも配慮すれば、既定メニュー以上に業務用途へフィットする右クリック体験が実現できます。

コメント