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