WebView2で独自コンテキストメニューを実装する方法|VB.NETとContextMenuStripで右クリックメニューを完全制御

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 やキーボード操作にも配慮すれば、既定メニュー以上に業務用途へフィットする右クリック体験が実現できます。

この記事を書いた人

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

コメント

コメントする

目次