23
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

次期HTAとしてのPowerShell+WebView2の利用

Last updated at Posted at 2022-05-06

※本記事の後続の検証結果の記事を公開しました(2022/05/08)。
 次期HTA(HtmlApplication)としてのPowerShell+XAML+WebView2の利用 - Qiita

PowerShell + WebView2 という面白い組み合わせについて Stack Overflow に質問が投稿されていたのを見かけた(参考#1)。
使い方次第では HTA を置き換える技術として利用できそうな気もするので検証した結果についてまとめる。

■ 前書き

2022年6月、Internet Explorer が終了する(参考#2)。IE モードは OS のサポート期限に合わせてサポートされる話もあるが、IE の終了と共に段階的に終了されるという情報も出ている (参考#3)。
HTML と VBS/JScript でデスクトップアプリケーションを気軽に作れる HTA(HtmlApplication) という技術は便利だったが IE の終了に伴いこの技術も利用できなくなりそうだ。

GUI アプリケーションを簡便に作る方法はいくらかあり、HTA の代わりになりそうなものとして、下記の記事は参考になる。

しかし、開発環境やツールなど環境構築が必要になることも多く、様々な制限から環境構築自体が難しい場面では採用し辛い事もある。そんな中 PowerShell + WebView2 は OS デフォルトの環境で作成やメンテナンスもし易い。

■ WebView2 とは

まず WebView2 が何かについて Microsoft の HP より引用して紹介する。

Microsoft Edge WebView2 を使用すると、Web テクノロジ (HTML、CSS、JavaScript) をユーザーのネイティブ アプリに埋め込みできます。 WebView2 コントロールは、Microsoft Edge をレンダリング エンジンとして使用して、ネイティブ アプリに Web コンテンツを表示します。
https://docs.microsoft.com/ja-jp/microsoft-edge/webview2/

クライアントアプリの UI 内に MS Edge のレンダラーを埋め込む事ができ、クライアントアプリと WebView2 間でデータの交換や、イベントリスナの登録が可能になるそうだ。

■ 本記事で利用する技術

今回利用する技術に付いて概要の分かるページを下記に記載する。

◇ 環境

  • Windows 10 Pro (21H2)
  • PSVersion: 5.1.19041.1645
  • WebView2: 1.0.1210.30

■ 本記事の以降の流れ

いきなり複雑な実装から手掛けると私の頭のリソースが足りなくなるので、下記のように簡単な構成から少しずつ進めて行く。
なお、段階を進めていく中で重複するコードが記載され冗長となってしまうが分かりやすさのため許していただきたい。

  1. PowerShell で簡単なウィンドウを表示する
  2. WebView2 を埋め込む
  3. WebView2 に対して PowerShell から (A)スクリプトの実行、(B)イベントリスナの登録、(C)データの送受信 を行う

◇ PowerShell で簡単なウィンドウを表示する

ボタンとテキストボックスとラベルを表示する。
ボタンをクリックすると数値をインクリメントしながらテキストボックスとラベルに値を入力する。

ファイル構成

構成
winforms_webview01\
  + sample01.ps1  (1)

ファイル内容

(1) sample01.ps1
Add-Type -Assembly System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$stAbsolute   = [System.Windows.Forms.SizeType]::Absolute
$dsFill       = [System.Windows.Forms.DockStyle]::Fill
$global:count = 0

<# Window #>
$form1 = [System.Windows.Forms.Form]@{
    Width  = 400
    Height = 250
    Text   = "Hello WinForms"
    Name   = "MainWindow1"
}

<# Top level Panel #>
$tablePanel = [System.Windows.Forms.TableLayoutPanel]@{
    ColumnCount = 1
    RowCount    = 3
    Dock        = $dsFill
}

<# Header Panel #>
$headerPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "HeaderPanel"
    BackColor     = [System.Drawing.Color]::AntiqueWhite
    Dock          = $dsFill
}

$incrementButton = [System.Windows.Forms.Button]@{
    Text = "Increment"
    Name = "IncrementButton"
}
$headerPanel.Controls.Add($incrementButton)

<# Result Panel #>
$resultPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "ResultPanel"
    BackColor     = [System.Drawing.Color]::Cyan
    Dock          = $dsFill
}

$resultBox = [System.Windows.Forms.TextBox]@{
    Name     = "resultBox"
    ReadOnly = $true
    Text     = "Default Text"
}
$resultPanel.Controls.Add($resultBox)

<# View Panel #>
$viewPanel= [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "ViewPanel"
    BackColor     = [System.Drawing.Color]::Ivory
    Dock          = $dsFill
}

$labelText = [System.Windows.Forms.Label]@{
    Name = "LabelText"
    Text = "Default Text"
}
$viewPanel.Controls.Add($labelText)

<# Events #>
$incrementButton.add_Click({
    $script:count++
    $resultBox.Text = $script:count
    $labelText.Text = $script:count
})

<# Add Panels #>
$form1.Controls.Add($tablePanel)

$tablePanel.Controls.Add($headerPanel, 0, 0)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($resultPanel, 0, 1)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($viewPanel, 0, 2)

<# Show #>
[void]$form1.showDialog()
$form1 = $null

実行結果

画像のうち左上のウィンドウが起動時、右下のウィンドウが Increment ボタンを何度か押した後のキャプチャ。
起動時には Default Text という値が表示されているが、Increment ボタンをクリックする事で繰り上げた値をそれぞれ表示させている。
スクリーンショット 2022-05-05 232810.png

◇ WebView2 を埋め込む

先のコードのラベルの部分を WebView2 に置き換える。WebView2 に関するコードの主な部分は参考にも記載の下記リンク先より利用している。

ファイル構成

構成
winforms_webview02\
  + sample02.ps1  (1)
  + lib\
    + Microsoft.Web.WebView2.Core.dll
    + Microsoft.Web.WebView2.WinForms.dll
    + Microsoft.Web.WebView2.Wpf.dll
    + WebView2Loader.dll
  + data\  … スクリプト側で自動生成

lib 配下のファイルは Nuget (参考#5) よりパッケージをダウンロードして、実行環境のアーキテクチャのDLLファイルを配置する。

ファイル内容

(1) sample02.ps1
Add-Type -Assembly System.Windows.Forms
Add-Type -AssemblyName System.Drawing

[void][reflection.assembly]::LoadFile((Join-Path $PSScriptRoot "lib\Microsoft.Web.WebView2.WinForms.dll"))
[void][reflection.assembly]::LoadFile((Join-Path $PSScriptRoot "lib\Microsoft.Web.WebView2.Core.dll"))
[void][reflection.assembly]::Load('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
[void][reflection.assembly]::Load('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')

$stAbsolute   = [System.Windows.Forms.SizeType]::Absolute
$dsFill       = [System.Windows.Forms.DockStyle]::Fill
$global:count = 0

[System.Windows.Forms.Application]::EnableVisualStyles()
$form1 = [System.Windows.Forms.Form]@{
    Width  = 400
    Height = 250
    Text   = "Hello WinForms"
    Name   = "MainWindow1"
}

$tablePanel = [System.Windows.Forms.TableLayoutPanel]@{
    ColumnCount = 1
    RowCount    = 3
    Dock        = $dsFill
}

<# Header Panel #>
$headerPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "HeaderPanel"
    BackColor     = [System.Drawing.Color]::AntiqueWhite
    Dock          = $dsFill
}

$incrementButton = [System.Windows.Forms.Button]@{
    Text = "Increment"
    Name = "IncrementButton"
}
$headerPanel.Controls.Add($incrementButton)

<# Result Panel #>
$resultPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "ResultPanel"
    BackColor     = [System.Drawing.Color]::Cyan
    Dock          = $dsFill
}

$resultBox = [System.Windows.Forms.TextBox]@{
    Name     = "resultBox"
    ReadOnly = $true
    Text     = "Default Text"
}
$resultPanel.Controls.Add($resultBox)

<# WebView2 #>
$webview = [Microsoft.Web.WebView2.WinForms.WebView2]@{
    Location   = New-Object System.Drawing.Point(0, 0)
    Name       = 'webview'
    TabIndex   = 0
    ZoomFactor = 1
    Dock       = $dsFill
    CreationProperties = New-Object 'Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties'
}
$webview.CreationProperties.UserDataFolder = (Join-Path $PSScriptRoot "data")

<# for Events ScriptBlock #>
$clickIncrementButton = {
    $script:count++
    $resultBox.Text = $script:count
}

$webview_SourceChanged = {
    $form1.Text = $webview.Source.AbsoluteUri;
}

$form1Loaded = {
    $webview.Source = ([uri]"https://www.google.co.jp/")
    $webview.Visible = $true
}

$form1Unloaded = {
    $tablePanel.Controls.Remove($webview)
    $incrementButton.remove_Click($clickIncrementButton)
    $form1.remove_Load($form1Loaded)
    $form1.remove_FormClosed($form1Unloaded)
}

<# add Event Listeners #>
$incrementButton.add_Click($clickIncrementButton)
$webview.add_SourceChanged($webview_SourceChanged)
$form1.add_Load($form1Loaded)
$form1.add_FormClosed($form1Unloaded)

<# Add Panels #>
$form1.SuspendLayout()
$form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$form1.AutoScaleMode = 'Font'
$form1.ClientSize = New-Object System.Drawing.Size(619, 413)

$form1.Controls.Add($tablePanel)

$tablePanel.Controls.Add($headerPanel, 0, 0)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($resultPanel, 0, 1)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($webview, 0, 2)

$form1.ResumeLayout()
<# Show #>

$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
$InitialFormWindowState = $form1.WindowState
$form1.add_Load({
    $form1.WindowState = $InitialFormWindowState
})

[void]$form1.showDialog()

実行結果

スクリーンショット 2022-05-06 090349.png
無事表示することができた。

◇ WebView2 に対して PowerShell から (A)スクリプトの実行、(B)イベントリスナの登録、(C)データの送受信 を行う

検証は以下の概要のように実施する。

  1. (A) スクリプトの実行
    WebView の ExecuteScriptAsync で JavaScript の alert(メッセージボックス) を起動する。
  2. (B) イベントリスナの登録
    WebView の ExecuteScriptAsync で JavaScript の addEventListener を設定する。
  3. (C) データの送受信
    WebView から PowerShell:
     → JS の window.chrome.webview.postMessage(...) で PowerShell にデータを送信する。
      PowerShell 側は WebView2 オブジェクトの add_WebMessageReceived で受信時の処理を記載する。
    PowerShell から WebView:
     → PowerShell の $webview.CoreWebView2.PostWebMessageAsString でデータを送信する。
      JS 側は window.chrome.webview.addEventListener で受信時の処理を記載する。   

ファイル構成

構成
winforms_webview03\
  + sample03.ps1  (1)
  + sample03.html (2)
  + lib\
    + Microsoft.Web.WebView2.Core.dll
    + Microsoft.Web.WebView2.WinForms.dll
    + Microsoft.Web.WebView2.Wpf.dll
    + WebView2Loader.dll
  + data\  … スクリプト側で自動生成

lib 配下のファイルは Nuget (参考#5) よりパッケージをダウンロードして、実行環境のアーキテクチャのDLLファイルを配置する。

ファイル内容

(1) sample03.ps1
Add-Type -Assembly System.Windows.Forms
Add-Type -AssemblyName System.Drawing

[void][reflection.assembly]::LoadFile((Join-Path $PSScriptRoot "lib\Microsoft.Web.WebView2.WinForms.dll"))
[void][reflection.assembly]::LoadFile((Join-Path $PSScriptRoot "lib\Microsoft.Web.WebView2.Core.dll"))
[void][reflection.assembly]::Load('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
[void][reflection.assembly]::Load('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')

$stAbsolute   = [System.Windows.Forms.SizeType]::Absolute
$dsFill       = [System.Windows.Forms.DockStyle]::Fill

[System.Windows.Forms.Application]::EnableVisualStyles()
$form1 = [System.Windows.Forms.Form]@{
    Width  = 400
    Height = 250
    Text   = "Hello WinForms"
    Name   = "MainWindow1"
}

$tablePanel = [System.Windows.Forms.TableLayoutPanel]@{
    ColumnCount = 1
    RowCount    = 3
    Dock        = $dsFill
}

<# Header Panel #>
$headerPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "HeaderPanel"
    BackColor     = [System.Drawing.Color]::AntiqueWhite
    Dock          = $dsFill
}

$alertButton = [System.Windows.Forms.Button]@{
    Text = "alert(in WebView2)"
    Name = "alertButton"
    Width = 120
}
$headerPanel.Controls.Add($alertButton)

$changeHeaderButton = [System.Windows.Forms.Button]@{
    Text = "Change Header"
    Name = "getTitleButton"
    Width = 130
}
$headerPanel.Controls.Add($changeHeaderButton)

<# Result Panel #>
$resultPanel = [System.Windows.Forms.FlowLayoutPanel]@{
    FlowDirection = [System.Windows.Forms.FlowDirection]::LeftToRight
    Name          = "ResultPanel"
    BackColor     = [System.Drawing.Color]::Cyan
    Dock          = $dsFill
}

$resultBox = [System.Windows.Forms.TextBox]@{
    Name     = "resultBox"
    ReadOnly = $true
    Text     = "Default Text"
}
$resultPanel.Controls.Add($resultBox)

<# WebView2 #>
$webview = [Microsoft.Web.WebView2.WinForms.WebView2]@{
    Location   = New-Object System.Drawing.Point(0, 0)
    Name       = 'webview'
    TabIndex   = 0
    ZoomFactor = 1
    Dock       = $dsFill
    CreationProperties = New-Object 'Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties'
}
$webview.CreationProperties.UserDataFolder = (Join-Path $PSScriptRoot "data")

<# for Events ScriptBlock #>
$clickAlertButton = {
    $webview.ExecuteScriptAsync("alert('Hello, World!');")
}

$clickChangeHeaderButton = {
    $webview.CoreWebView2.PostWebMessageAsString('that is the question.');
    # $webview.CoreWebView2.PostWebMessageAsJson((@{a=10} | ConvertTo-Json))
}

$webview_NavigateCompleted = {
    $webview.ExecuteScriptAsync(
@"
document.querySelector('#sendToPS').addEventListener('click', () => {
    window.chrome.webview.postMessage({PageTitle: document.querySelector('#sendText').value});
});
"@)
}

$webview_MessageReceived = {
    param($p1, $p2)
    $json = ($p2.WebMessageAsJson | ConvertFrom-Json)
    $resultBox.Text = $json.PageTitle
}

$form1Loaded = {
    $webview.Source = ([uri]("file:///" + $PSScriptRoot + "/sample03.html"))
    $webview.Visible = $true
}

$form1Unloaded = {
    $tablePanel.Controls.Remove($webview)
    $alertButton.remove_Click($clickAlertButton)
    $form1.remove_Load($form1Loaded)
    $form1.remove_FormClosed($form1Unloaded)
}

<# add Event Listeners #>
$alertButton.add_Click($clickAlertButton)
$changeHeaderButton.add_Click($clickChangeHeaderButton)
$webview.add_NavigationCompleted($webview_NavigateCompleted);
$webview.add_WebMessageReceived($webview_MessageReceived)
$form1.add_Load($form1Loaded)
$form1.add_FormClosed($form1Unloaded)

<# Add Panels #>
$form1.SuspendLayout()
$form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$form1.AutoScaleMode = 'Font'
$form1.ClientSize = New-Object System.Drawing.Size(480, 270)

$form1.Controls.Add($tablePanel)

$tablePanel.Controls.Add($headerPanel, 0, 0)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($resultPanel, 0, 1)
[void]$tablePanel.RowStyles.Add(
	(New-Object System.Windows.Forms.RowStyle($stAbsolute, 35))
)

$tablePanel.Controls.Add($webview, 0, 2)

$form1.ResumeLayout()
<# Show #>

$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
$InitialFormWindowState = $form1.WindowState
$form1.add_Load({
    $form1.WindowState = $InitialFormWindowState
})

[void]$form1.showDialog()

(2) sample03.html
<!DOCTYPE html>
<html>
  <head>
    <title>Hogeタイトル</title>
    <script>
    window.chrome.webview.addEventListener('message', function(event) {
        document.querySelector('h1').innerText = event.data;
    });
    </script>
  </head>
  <body>
    <h1 id="forTitleChange">To be or not to be,</h1>
    <hr />
    <input id="sendText" type="text" value="sample Text" />
    <button id="sendToPS">テキストの内容をPowerShellに送信</button>
  </body>
</html>

実行結果

・起動時の画面
image.png

・(A) スクリプトの実行
alert ボタンをクリックして、WebView2 内で alert の呼び出しに成功している。
image.png

・(B) イベントリスナの登録、(C) データの送受信(WebView2->PowerShell)
ページ内のボタンをクリックすると PowerShell にデータの送信を行うようにイベントリスナを登録している。
ボタン横のテキストの内容を青色の帯のテキストボックスに入力する。
image.png

・(C) データの送受信(PowerShell->WebView2)
PowerShell から PostWebMessageAsString で文字列を送信し、ページのH1要素の内容を書き換えている。
image.png

■ 結論と後書き

HTA ほどのノリでは流石に書けないが巨大なアプリでも手掛けないならば用を足せそうだ。
例えば、レイアウトに関わる処理は WebView2 内に完結させて、外部との通信やファイル操作は PowerShell で実装するなど、それぞれ得意な処理を棲み分けて使うのが良さそうには感じる。
どのように使うにしてもブラウザを取り扱うだけにサニタイズ不備のないように注意したい。

元々は PowerShell + XAML + WebView2 に関する記事を書こうと検証を進めていたが、WPF ベースである XAML に WebView2 のオブジェクトを上手く追加できなかった。知識不足もあるが、現状上手く結果を残せる範囲で記事にまとめた。
もし PowerShell 上で WPF に WebView2 を追加する方法が分かる方がいましたら、コメント欄で教えていただけると幸いです。
→ WinForms と WPF 用で異なる DLL が提供されており、目的と異なる DLL を利用していたために使えませんでした。適切なDLLの読み込みにより利用可能な事を確認済み(2022/05/08追記)。

参考

記事内で参照しているもの、参照していないが参考とさせていただいたもの。

  1. WebView2 in PowerShell Winform GUI - Stack Overflow
    https://stackoverflow.com/questions/66106927/webview2-in-powershell-winform-gui
  2. Microsoft 社 Internet Explorer のサポート終了について:IPA 独立行政法人 情報処理推進機構
    https://www.ipa.go.jp/security/announce/ie_eos.html
  3. Internet Explorer 11 デスクトップ アプリケーションのサポート終了 – 発表に関連する FAQ のアップデート - Windows Blog for Japan
    https://blogs.windows.com/japan/2022/02/21/internet-explorer-11-desktop-app-retirement-faq/
  4. powershellでTableLayoutPanelを使ってフォームをレイアウト : morituriのブログ
    http://blog.livedoor.jp/morituri/archives/54179696.html
  5. NuGet Gallery | Microsoft.Web.WebView2 1.0.1210.30
    https://www.nuget.org/packages/Microsoft.Web.WebView2

更新履歴

  • 2022/05/08:
    • 後続の検証に関する記事を最上部に追記
    • 参考へのアンカーリンクが外れていたため修正
    • 一部誤字の修正
    • 後書きを修正
  • 2022/05/11:
    • タグに PS-Edge を追加
  • 2022/05/22:
    • [前書き]のIE モード終了次期に関する誤情報を訂正
    • バージョン等の環境情報を記載
23
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?