WebBrowserからWebView2へ(5)

WEBVIEW2を使用してみる(5)JavaScriptの外出し/右クリックメニューの追加

前回までの内容で、ひとまず要素操作の基本的なところはできたかなと思う。
今回はオリジナルの右クリックメニュー追加を行うついでに、コードがもっさりしてきたので
直書きしているJavaScriptのファイル化などコード整理も行う。

やりたいこと
・ 右クリックしたときオリジナルのコンテキストメニューを表示する。
・ マウスオーバーしている要素に枠線をつけて、対象要素を視覚的にわかるようにする。

ソース整理
・ ソースに直書きしているJavaScriptコード部分をjsファイルで管理する。
・ マウスイベント関連のスクリプトはサイト読込完了時に自動で設定する。
・ マウスイベントでVB側に送る情報はjson形式にする。
・ VB側はjson形式データを受信後はクラスデータにデシリアライズする。

開発環境

・ Windows10
・ Microsoft Visual Studio 2019 (VB .NET)

JavaScriptコード部分をjsファイル化する

test.jsを作成しそちらにjavaScriptのイベントリスナーや呼び出し関数をまとめた。
あとはReadAllTextで読み取ってあげればいいだけ。これは思いのほか簡単。

    ''' <summary>
    ''' サイト内にJavaScriptイベント追加
    ''' </summary>
    Public Async Sub AddJsEvent()

        'JavaScriptを別ファイルで管理。テキストデータで読み込み
        Dim js As String = System.IO.File.ReadAllText("Js\test.js")

        'ExecuteScriptAsyncでサイト内にJavaScriptイベント追加
        Await Me.ExecuteScriptAsync(js)

    End Sub

実装したJavaScriptは以下の通り、
・mouseoverイベント:マウスオーバー時にアクティブ要素にボーダー設定
・mouseoutイベント:マウスアウト時にボーダー解除
・contextmenuイベント:右クリック時にjsonデータ(位置と要素情報)をwebView2へ送信
・mousedownイベント:右クリック時にjsonデータ(位置)をwebView2へ送信
・ElementClick/ElementInput関数:前回作ったものをファイルへ移動

test.js

//-----------------------------------------------
//イベントリスナー追加
//マウスオーバーした要素にボーダー線追加
//-----------------------------------------------
document.addEventListener('mouseover', function (event)
{
		event.target.style.border = "thin solid rgb( 0, 0, 255 )";
});

//-----------------------------------------------
//イベントリスナー追加
//マウスアウトした要素のボーダー線削除
//-----------------------------------------------
document.addEventListener('mouseout', function (event)
{
		event.target.style.border = "none";	
});

//-----------------------------------------------
//イベントリスナー追加
//右クリックメニュー表示時にマウス位置と要素情報を送る
//-----------------------------------------------
document.addEventListener('contextmenu', function (event)
{
	let jsonObject =
	{
		Key: 'contextmenu',
		Point:
		{
			X: event.screenX,
			Y: event.screenY
		},
		Target:
		{
			tagName		:event.target.tagName ,
			id		:event.target.id,
			className	:event.target.className,
			innerText	:event.target.innerText,
		}
	};
	window.chrome.webview.postMessage(jsonObject);
});

//-----------------------------------------------
//イベントリスナー追加
//マウスダウン時にマウス位置を送る
//-----------------------------------------------
document.addEventListener('mousedown', function (event)
{
	let jsonObject =
	{
		Key: 'mousedown',
		Point:
		{
			X: event.screenX,
			Y: event.screenY
		}
	};
	window.chrome.webview.postMessage(jsonObject);
});

//-----------------------------------------------
//VB.NETから呼び出し用の関数
//指定情報(タグ、クラス名、Id、内部テキスト)より要素判定してクリック
//-----------------------------------------------
function ElementClick(tag, classname, id, innertext)
{
  let items = document.getElementsByTagName(tag);
  for (let i = 0; i < items.length; i++)
  {
    if (items[i].className == classname && items[i].id == id && items[i].innerText == innertext)
    {
      console.log('ElementClick Run');
      items[i].click();
      break;
    }
  }
}

//-----------------------------------------------
//VB.NETから呼び出し用の関数
//指定情報(タグ、クラス名、Id、内部テキスト)より要素判定して入力
//-----------------------------------------------
function ElementInput(tag, classname, id, innertext, setword)
{
  let items = document.getElementsByTagName(tag);
  for (let i = 0; i < items.length; i++)
  {
    if (items[i].className == classname && items[i].id == id && items[i].innerText == innertext)
    {
      console.log('ElementInput Run');
      items[i].value = setword;
      break;
    }
  }
}

VB側の受信処理/受信Jsonのデシリアライズ

マウスイベント関連でpostMessageされたjsonデータは、
WebMessageReceivedイベントのWebMessageAsJsonより取得できる。
jsonのデシリアライズにはNewtonsoft.Jsonをインポートして使用。

受信データよりキーが”contextmenu”ならば右クリックイベントとして判定して
同じくjsonで受信した位置情報にもとにコンテキストメニューを表示する。

    ''' <summary>
    ''' マウスイベントの戻り値を取得するためのJson構造の定義
    ''' </summary>
    Private Class JsonObject
        Public key As String
        Public point As PointF
        Public target As ElementPram
    End Class

    ''' <summary>
    ''' 要素パラメータはタグ、ID、クラス名、内部文字列
    ''' </summary>
    Public Class ElementPram
        Public tagName As String
        Public id As String
        Public className As String
        Public innerText As String
    End Class

    ''' <summary>
    ''' JavaScriptからメッセージを受信したときに実行されるイベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="args"></param>
    Private Sub MessageReceived(sender As Object, args As Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs) Handles Me.WebMessageReceived
        Try
            '受信したJsonデータをデシリアライズ
            Dim JsonObject As JsonObject = JsonConvert.DeserializeObject(Of JsonObject)(args.WebMessageAsJson)

            'アクティブ要素を取得
            Me.ActiveElement = JsonObject.target

            '右クリックイベントの場合
            If JsonObject.key = "contextmenu" Then
                Me.CustomContextMenu.Show(Point.Truncate(JsonObject.point))
            Else
                Me.CustomContextMenu.Hide()
            End If

        Catch ex As Exception
            Debug.WriteLine(ex.ToString)
        End Try
    End Sub

サイト読込完了時にスクリプトを自動設定

NavigationCompletedイベントでサイト遷移完了を検知したときにtest.jsを設定するようにした。
またNavigationStartingイベントでサイト遷移開始時に右クリックメニュー禁止+状態判定プロパティを初期化させている。

これでサイト内リンクなどで別サイトへ遷移したときも、また遷移完了イベントが走りtest.jsを設定してくれる。

    ''' <summary>
    ''' URL読み込み開始イベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MeNavigationStarting(sender As Object, e As CoreWebView2NavigationStartingEventArgs) Handles Me.NavigationStarting

        '今回はカスタム右クリックメニューを表示するためデフォルトの右クリックメニューを禁止
        Me.CoreWebView2.Settings.AreDefaultContextMenusEnabled = False

        'ステータス更新(読み込み中)
        Me._NowStatus = NAVIGATE_STATUS.Navigating

    End Sub

    ''' <summary>
    ''' URL読み込み完了時イベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MeNavigationCompleted(sender As Object, e As CoreWebView2NavigationCompletedEventArgs) Handles Me.NavigationCompleted

        If e.IsSuccess Then
            '前回ステータス未完了から今回完了に変化した場合。
            If Me._NowStatus <> NAVIGATE_STATUS.Complate Then

                'ステータス更新(完了)
                Me._NowStatus = NAVIGATE_STATUS.Complate

                'サイト内にJavaScriptイベント追加
                Call AddJsEvent()

            End If
        Else

            'ステータス更新(エラー)
            Me._NowStatus = NAVIGATE_STATUS.NavigateError

        End If

        condition.Signal()
        System.Threading.Thread.Sleep(1)
        condition.Reset()

    End Sub

サンプルコード

Form1.vbのほうでカスタムメニュー作成してWebviewへセット。
右クリックメニューの内容はとりあえず要素データをポップアップする感じにした。

Form1.vb

Public Class Form1

    ''' <summary>
    ''' フォームロード
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        Try

            'ボタン作成 + コントロール配置
            Dim navigateBtn As Button = New Button()
            Me.Controls.Add(navigateBtn)
            navigateBtn.Text = "Google表示"
            navigateBtn.Left = 0
            navigateBtn.Top = 0
            navigateBtn.Width = 100
            navigateBtn.Height = 30


            'ボタン作成 + コントロール配置
            Dim autoExecBtn As Button = New Button()
            Me.Controls.Add(autoExecBtn)
            autoExecBtn.Text = "自動検索"
            autoExecBtn.Left = navigateBtn.Width
            autoExecBtn.Top = 0
            autoExecBtn.Width = 100
            autoExecBtn.Height = 30


            'WebViewオブジェクト作成 + コントロール配置
            Dim webview As ClsWebView = New ClsWebView()
            Me.Controls.Add(webview)
            webview.Name = "WebViewSample"
            webview.Left = 0
            webview.Top = 0 + navigateBtn.Height
            webview.Width = Me.Width
            webview.Height = Me.Height - navigateBtn.Height

            'ブラウザ右クリックで表示させるカスタムメニューを作成
            Dim menuItem As ToolStripMenuItem
            menuItem = New ToolStripMenuItem("要素タグを表示", Nothing,
                                             Sub()
                                                 MsgBox(webview.ActiveElement.tagName)
                                             End Sub)
            webview.CustomContextMenu.Items.Add(menuItem)
            menuItem = New ToolStripMenuItem("要素クラス名を表示", Nothing,
                                             Sub()
                                                 MsgBox(webview.ActiveElement.className)
                                             End Sub)
            webview.CustomContextMenu.Items.Add(menuItem)
            menuItem = New ToolStripMenuItem("要素IDを表示", Nothing,
                                             Sub()
                                                 MsgBox(webview.ActiveElement.id)
                                             End Sub)
            webview.CustomContextMenu.Items.Add(menuItem)
            menuItem = New ToolStripMenuItem("要素内部文字列を表示", Nothing,
                                             Sub()
                                                 MsgBox(webview.ActiveElement.innerText)
                                             End Sub)
            webview.CustomContextMenu.Items.Add(menuItem)


            InitializeComponent()

            'イベント設定:メインフォームのサイズ変更時にWebViewも変更
            AddHandler Me.SizeChanged, Sub()
                                           webview.Width = Me.Width
                                           webview.Height = Me.Height - navigateBtn.Height
                                       End Sub


            'イベント設定:ボタンクリック時、サイト遷移開始
            AddHandler navigateBtn.Click,
                Async Sub()

                    '初期化終えてから処理
                    If webview Is Nothing OrElse webview.CoreWebView2 Is Nothing Then
                        MsgBox("コントロール初期化中です")
                        Exit Sub
                    End If

                    '指定URLへ遷移
                    webview.CoreWebView2.Navigate("https://www.google.com")

                    '待機処理
                    Await webview.WaitNavigating

                    '遷移完了後のURLとステータスをデバッグ出力
                    Debug.Print("URL:" & webview.Source.OriginalString & " | STATUS:" & webview.NowStatus.ToString)

                End Sub



            'イベント設定:ボタンクリック時、サイト遷移開始
            AddHandler autoExecBtn.Click,
                 Sub()

                     'Google以外は無効
                     If webview.Source.OriginalString <> "https://www.google.com" Then Exit Sub

                     '検索文字はひなまつり
                     Dim setword As String = "ひなまつり"


                     '入力対象要素を指定して入力実行
                     Call webview.ElementInput(tag:="INPUT",
                                               classname:="gLFyf gsfi",
                                               id:="",
                                               innertext:="",
                                               setword:=setword)


                     'クリック対象要素を指定してClick実行
                     Call webview.ElementClick(tag:="INPUT",
                                               classname:="gNO89b",
                                               id:="",
                                               innertext:="")

                 End Sub


        Catch ex As Exception
            Debug.Print("[Error] : " & Now & " : " & ex.ToString)
        End Try

    End Sub

End Class

ClsWebView.vb

Imports Microsoft.Web.WebView2.Core
Imports Microsoft.Web.WebView2.WinForms
Imports Newtonsoft.Json

''' <summary>
''' WebView2を継承していろいろカスタマイズするクラス
''' </summary>
Public Class ClsWebView
    Inherits WebView2

    ''' <summary>
    ''' 列挙体: サイト遷移の状態判定用
    ''' </summary>
    Public Enum NAVIGATE_STATUS
        Navigating
        Complate
        NavigateError
        TimeoutError
    End Enum

    ''' <summary>
    ''' マウスイベントの戻り値を取得するためのJson構造の定義
    ''' </summary>
    Private Class JsonObject
        Public key As String
        Public point As PointF
        Public target As ElementPram
    End Class

    ''' <summary>
    ''' 要素パラメータはタグ、ID、クラス名、内部文字列
    ''' </summary>
    Public Class ElementPram
        Public tagName As String
        Public id As String
        Public className As String
        Public innerText As String
    End Class

    ''' <summary>
    ''' プロパティ: サイト遷移の状態
    ''' </summary>
    Public ReadOnly Property NowStatus As NAVIGATE_STATUS

    ''' <summary>
    ''' プロパティ: コンテキストメニュー
    ''' </summary>
    ''' <returns></returns>
    Public Property CustomContextMenu As ContextMenuStrip = New ContextMenuStrip

    ''' <summary>
    ''' プロパティ: クリック中の要素情報
    ''' </summary>
    Public Property ActiveElement As ElementPram

    ''' <summary>
    ''' 読み込み完了イベント待機用
    ''' </summary>
    Private ReadOnly condition As New System.Threading.CountdownEvent(1)

    ''' <summary>
    ''' コンストラクタ
    ''' </summary>
    Public Sub New()

        '初期化
        InitializeAsync()

        '読込み完了時イベントを設定
        AddHandler Me.NavigationCompleted, AddressOf MeNavigationCompleted

        '読込み開始時イベントを設定
        AddHandler Me.NavigationStarting, AddressOf MeNavigationStarting

    End Sub

    ''' <summary>
    ''' WebView2の初期化
    ''' </summary>
    Async Sub InitializeAsync()
        Await Me.EnsureCoreWebView2Async(Nothing)
    End Sub


    ''' <summary>
    ''' サイト表示完了までの待機処理
    ''' </summary>
    ''' <returns></returns>
    Public Async Function WaitNavigating() As Task(Of NAVIGATE_STATUS)

        '非同期実行
        Await Task.Run(
            Sub()
                '読み込み完了まで待機
                If condition.Wait(5000) Then
                    '待機完了
                Else
                    '待機未完了(タイムアウト)
                    Me._NowStatus = NAVIGATE_STATUS.TimeoutError
                End If
            End Sub
        )
        Return Me.NowStatus

    End Function


    ''' <summary>
    ''' URL読み込み開始イベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MeNavigationStarting(sender As Object, e As CoreWebView2NavigationStartingEventArgs) Handles Me.NavigationStarting

        '今回はカスタム右クリックメニューを表示するためデフォルトの右クリックメニューを禁止
        Me.CoreWebView2.Settings.AreDefaultContextMenusEnabled = False

        'ステータス更新(読み込み中)
        Me._NowStatus = NAVIGATE_STATUS.Navigating

    End Sub

    ''' <summary>
    ''' URL読み込み完了時イベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MeNavigationCompleted(sender As Object, e As CoreWebView2NavigationCompletedEventArgs) Handles Me.NavigationCompleted

        If e.IsSuccess Then
            '前回ステータス未完了から今回完了に変化した場合。
            If Me._NowStatus <> NAVIGATE_STATUS.Complate Then

                'ステータス更新(完了)
                Me._NowStatus = NAVIGATE_STATUS.Complate

                'サイト内にJavaScriptイベント追加
                Call AddJsEvent()

            End If
        Else

            'ステータス更新(エラー)
            Me._NowStatus = NAVIGATE_STATUS.NavigateError

        End If

        condition.Signal()
        System.Threading.Thread.Sleep(1)
        condition.Reset()

    End Sub


    ''' <summary>
    ''' サイト内にJavaScriptイベント追加
    ''' </summary>
    Public Async Sub AddJsEvent()

        'JavaScriptを別ファイルで管理。テキストデータで読み込み
        Dim js As String = System.IO.File.ReadAllText("Js\test.js")

        'ExecuteScriptAsyncでサイト内にJavaScriptイベント追加
        Await Me.ExecuteScriptAsync(js)

    End Sub


    ''' <summary>
    ''' JavaScriptからメッセージを受信したときに実行されるイベント
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="args"></param>
    Private Sub MessageReceived(sender As Object, args As Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs) Handles Me.WebMessageReceived
        Try
            '受信したJsonデータをデシリアライズ
            Dim JsonObject As JsonObject = JsonConvert.DeserializeObject(Of JsonObject)(args.WebMessageAsJson)

            'アクティブ要素を取得
            Me.ActiveElement = JsonObject.target

            '右クリックイベントの場合
            If JsonObject.key = "contextmenu" Then
                Me.CustomContextMenu.Show(Point.Truncate(JsonObject.point))
            Else
                Me.CustomContextMenu.Hide()
            End If

        Catch ex As Exception
            Debug.WriteLine(ex.ToString)
        End Try
    End Sub


    ''' <summary>
    ''' 指定情報(タグ、クラス名、Id、内部テキスト)より要素判定してクリック
    ''' </summary>
    ''' <param name="tag"></param>
    ''' <param name="classname"></param>
    ''' <param name="innertext"></param>
    Public Async Sub ElementClick(ByVal tag As String, ByVal classname As String, ByVal id As String, ByVal innertext As String)

        'サイト読み込み完了時以外は実行しない
        If Me.NowStatus <> NAVIGATE_STATUS.Complate Then Exit Sub

        '指定HTML要素をクリック
        Dim js As New System.Text.StringBuilder()
        js.AppendLine("ElementClick('" & tag & "','" & classname & "','" & id & "','" & innertext & "');")
        Await Me.ExecuteScriptAsync(js.ToString())

    End Sub


    ''' <summary>
    ''' 指定情報(タグ、クラス名、Id、内部テキスト)より要素判定して入力
    ''' </summary>
    ''' <param name="tag"></param>
    ''' <param name="classname"></param>
    ''' <param name="id"></param>
    ''' <param name="innertext"></param>
    ''' <param name="setword"></param>
    Public Async Sub ElementInput(ByVal tag As String, ByVal classname As String, ByVal id As String, ByVal innertext As String, ByVal setword As String)

        'サイト読み込み完了時以外は実行しない
        If Me.NowStatus <> NAVIGATE_STATUS.Complate Then Exit Sub

        '指定HTML要素をクリック
        Dim js As New System.Text.StringBuilder()
        js.AppendLine("ElementInput('" & tag & "','" & classname & "','" & id & "','" & innertext & "','" & setword & "');")
        Await Me.ExecuteScriptAsync(js.ToString())

    End Sub

End Class


マウスオーバー時の要素枠付けは成功


右クリックメニューの表示も大丈夫そう


タイトルとURLをコピーしました