📌

PowershellでもGUIが使えるんやで ~Windows FormsとPowershellでつくるGUIアプリケーション~

2024/11/28に公開

はじめに

Windowsでは、「Windows PowerShell」というシェルがプリインストールされており、Windowsにおけるシステム管理などのバッチ処理に、幅広く使えるようになっています。
ただし、Powershell単体で、GUIアプリケーションを作れるということは詳しく知らない方もいらっしゃると思います。
本記事では、私がGUIフレームワーク「Windows Forms」を用いて、PowerShellでGUIアプリケーションを作ってみるにあたって調べたり実践した結果、得られた方法を書いていこうと思います。
Windows Formsを使うこと自体が全くの初めてなので、もし不適切な実装などありましたらご指摘ください🙏

PowershellでWindows Formsを扱える原理

Windows PowerShellは、.NET Framework上で動作しています。そのため、.NET Framework上で利用可能な機能をすべて使用することができます。
.NET Frameworkでは、C#、F#、Visual Basicのうちのどれかを使ってアプリケーション開発することが想定されていますが、ランタイム(実行環境)は共通で、クラスライブラリ(様々な機能のためのAPIと型)も全く同じものを使っています。
つまり、Powershellでも、.NETで使える同じ機能を、同じ呼び出し方で使うことができるはずです。
このことは、開発にあたってもうひとつのメリットをもたらしてくれます。それは「C#などの他言語の、コード例をそのまま参考にできる」ということです。どんな手続きが必要かや、メソッドの呼び方・順番など、PowerShellの情報がなくても、C#やVisual Basicの作例や入門ページを参考にしながら、方法を模索することができるのです。
...というわけで、今回は.NET Framework環境用のGUIフレームワーク「Windows Forms」を使って、GUIアプリケーションの作成に挑戦してみました。

メッセージボックスの表示

まずは、GUIのなかでも使うのが簡単な「メッセージボックス」を表示してみます。


こういうヤツらです

メッセージボックスは、System.Windows.Forms名前空間のなかにあるMessageBoxというクラスを使用します。このクラスの中にある「show」というメソッドを呼び出すことで、メッセージボックスを表示することができます。引数は、メッセージ、タイトルバーのテキスト、表示するボタンのタイプ、の3つを指定できます。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.messagebox.show?view=windowsdesktop-9.0#system-windows-forms-messagebox-show(system-string-system-string-system-windows-forms-messageboxbuttons)
ただし、この名前空間をPowerShellで使用するためには、まずSystem.Windows.Formsをロードする必要があります。そのために、Add-Typeコマンドレットを用いて、System.Windows.Formsを含むアセンブリ(.NETにおけるライブラリのような役割のもの)を読み込む必要があります。

Add-Type -AssemblyName System.Windows.Forms

以下は、Add-TypeMessageBox::Showを使うコードの例です。このコードでメッセージボックスを表示することができます。

psgui_00_dialog.ps1
# アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms

# メッセージボックスの表示
$result = [System.Windows.Forms.MessageBox]::Show("どうも、メッセージボックスです。`nダイアログとも呼ばれますね", "第2引数はタイトルテキストです",[System.Windows.Forms.MessageBoxButtons]::OK)

# 押されたボタンをコンソールに出力
Write-Host $result


実行結果

ウィンドウの作成

ウィンドウの表示

次に普通のウィンドウを作成・使用してみます。
ウィンドウは、System.Windows.Forms.Formクラスを用いて作成できます。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form
New-Objectコマンドレットを用いてFormのインスタンスを作成し、プロパティを設定してから、FormオブジェクトのShow()メソッドで表示、Activate()メソッドで前面に出すようにしています。ちなみにFormTextプロパティでは、ウィンドウのタイトルバーに表示されるテキストを設定します。

psgui_01_window.ps1m
# ウィンドウの作成・表示
$screen = New-Object System.Windows.Forms.Form
$screen.Width = 640
$screen.Height = 480
$screen.Text = "New Window"
$screen.Show()
$screen.Activate()

イベントループ

「まだ実行してはいけない」と書きましたが、それには理由があります。試しに実行すると、ウィンドウが存在するのにスクリプトの実行が終了してしまっています。しかも、このウィンドウはウィンドウを閉じたり、移動したりの操作を一切受け付けないという、ゾンビのようなウィンドウです。


このようなウィンドウが表示されますが...一切の操作ができません

WindowsのGUIアプリケーションでは、アプリケーションの外部から来る「ウィンドウメッセージ」の処理を行う必要があります。そして、それらウィンドウメッセージを、何も来ていなければ待機、来ていれば適切に処理するためのループ処理(イベントループ)が必要です。
ウィンドウメッセージには膨大な種類があり、それぞれに適切な処理を記述するのは大変です。しかし、Windows Formsでは、各ウィンドウメッセージに対して標準的な動作を勝手にしてくれるSystem.Windows.Forms.Application.Run()というメソッドが用意されています。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.application.run
このメソッドでは、ウィンドウメッセージを処理するためのイベントループを開始し、そのなかを無限ループしてくれます。言い換えれば、その後の行は一切実行されません。ただし、第一引数にFormオブジェクトを指定すると、そのフォームが閉じられた(System.Windows.Forms.Form.FormClosedイベントが発生した)場合にイベントループから脱出し、スクリプトの実行を再開してくれます。

イベントループを追加したコードが以下です。

psgui_02_eventloop.ps1
# アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms

# ウィンドウの作成・表示
$screen = New-Object System.Windows.Forms.Form
$screen.Width = 640
$screen.Height = 480
$screen.Text = "New Window"
$screen.Show()
$screen.Activate()

#イベントループ
[System.Windows.Forms.Application]::Run($screen)
#フォームが閉じられた(FormClosed)ときに、Runから抜ける
Write-Host "イベントループから脱出しました"
exit
トラブルシューティング: PowerShellスクリプトで日本語テキストが文字化けする場合は...

PowerShellスクリプトでは、文字のエンコードには注意が必要です。
だいたいどのエディタでもデフォルトなUTF-8は文字化けします

これだとダメ
「Shift-JIS (ANSI)」または「BOM付きUTF-8」で保存する必要があります。

この2つのどちらかなら上手くいく

イベントハンドラを追加

`System.Windows.Forms.Application.Run()`メソッドを使ってイベントをうまく処理できることがわかりましたが、特定の種類のイベントに対する処理をカスタムすることはできるのでしょうか?

もちろんできます。特定のイベントを処理するための関数(イベントハンドラ)を設定することによって実現できます。
ここでは、ウィンドウを閉じる(「X」ボタンや、Alt + F4)ときの動作を定義してみます。
ウィンドウを閉じるときに発生するイベントは、System.Windows.Forms.Form.FormClosingイベントです。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form.formclosing
このイベントでは、System.Windows.Forms.FormClosingEventHandlerという型で、引数2つでイベントハンドラが呼び出されます。そのため、イベントハンドラとなる関数も、引数を2つ受け取るような関数にします。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.formclosingeventhandler
今回は、ウィンドウが閉じられたときに、メッセージボックスが表示されるようにしてみました。

# フォームが閉じられたときに実行される関数
function screen_formclosing(){
    param(
        [System.Windows.Forms.Form]$send_from,
        [System.Windows.Forms.FormClosingEventArgs]$eh
    )
    [void] [System.Windows.Forms.MessageBox]::show("バイバイ", "",[System.Windows.Forms.MessageBoxButtons]::OK)
    Write-Host "フォームが閉じられました"
}

PowerShell上で、Windows Formsオブジェクトに対してイベントハンドラを設定するのは、(インスタンス).Add_(イベント名)で設定できます。パラメータには、イベントが発生したときに実行されるスクリプトを記述します。ここでは、System.Windows.Forms.FormClosingEventHandlerの引数をparam構文で受け取り、イベントハンドラとなる関数screen_formclosingに投げています。

$screen.Add_FormClosing({param($s,$e) screen_formclosing $s $e})

これらを実装したコードが以下です。

psgui_03_formclosing.ps1
# アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms

# フォームが閉じられたときに実行される関数
function screen_formclosing(){
    param(
        [System.Windows.Forms.Form]$send_from,
        [System.Windows.Forms.FormClosingEventArgs]$eh
    )
    [void] [System.Windows.Forms.MessageBox]::show("バイバイ", "",[System.Windows.Forms.MessageBoxButtons]::OK)
    Write-Host "フォームが閉じられました"
}

# ウィンドウの作成・表示
$screen = New-Object System.Windows.Forms.Form
$screen.Width = 640
$screen.Height = 480
$screen.Text = "New Window"
$screen.Show()
$screen.Activate()
$screen.Add_FormClosing({param($s,$e) screen_formclosing $s $e})

#イベントループ
[System.Windows.Forms.Application]::Run($screen)
#フォームが閉じられた(FormClosed)ときに、Runから抜ける
Write-Host "イベントループから脱出しました"
exit


「X」ボタン または Alt+F4 で、メッセージボックスが出てきてから終了する

GUIオブジェクトの作成と配置

GUIアプリケーションでは、GUIの部品となるオブジェクトを画面上に配置して作られます。いくつかのオブジェクトを実際に配置してみます。

ラベル

ラベルは、System.Windows.Forms.Labelクラスを用いて作成できます。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.label
New-Objectでラベルを作成したあと、表示テキストやサイズなど、各プロパティを設定していきます。設定ができたら最後に、System.Windows.Forms.FormオブジェクトにあるControlCollection.Addメソッドを使用してウィンドウにラベルを追加し、ラベルが表示されるようにします。
またここでは、ラベルのサイズやフォントの指定に、System.Drawing.~という名前空間にあるクラスを使っているので、Add-Type -AssemblyName System.Drawingしておきます。

#アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# ---------
#  中略...
# ---------

# ラベルの作成・表示
$hellolabel = New-Object System.Windows.Forms.Label
$hellolabel.Text = "Hello, World!"
$hellolabel.Location = New-Object System.Drawing.Point(0, 0)
$hellolabel.Font = New-Object System.Drawing.Font("MS ゴシック", 20)
$hellolabel.Size = New-Object System.Drawing.Point(320, 32)
$hellolabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$screen.Controls.Add($hellolabel)


ラベルの表示例。中央揃えで表示されている

ボタン

ボタンは、System.Windows.Forms.Buttonクラスを用いて作成できます。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.button

ボタンが押されたときに発生するイベントはClickイベントです。なので、Add_Clickメソッドを呼び出してイベントハンドラを設定します。
それ以外は、ラベルと同じような使い方ができます。

# ボタンが押されたときに実行される関数
function pushme_click(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    Write-Host "ボタンが押されました"
}

# ---------
#  中略...
# ---------

# ボタンの作成・表示
$pushme = New-Object System.Windows.Forms.Button
$pushme.Text = "押してね"
$pushme.Location = New-Object System.Drawing.Point(50, 32)
$pushme.Size = New-Object System.Drawing.Point(120, 24)
$pushme.Add_Click({param($s,$e) pushme_click $s $e})
$screen.Controls.Add($pushme)


ボタン。押してね

入力ボックス

入力ボックスは、System.Windows.Forms.TextBoxクラスを用いて作成できます。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.textbox
テキストが入力されたり、消されたときに発生するイベントはTextChangedイベントです。なので、Add_TextChangedイベントを呼び出してイベントハンドラを設定します。
なお、ここでは使用していませんが、MultilineというプロパティをTrueに設定すると、複数行のテキストを入力できるボックスになります。

# 入力ボックスに入力されたときに実行される関数
function inputhere_textchanged(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    Write-Host $inputhere.Text
}

# ---------
#  中略...
# ---------

# 入力ボックスの作成・表示
$inputhere = New-Object System.Windows.Forms.TextBox
$inputhere.Location = New-Object System.Drawing.Point(40, 0)
$inputhere.Size = New-Object System.Drawing.Point(240, 26)
$inputhere.Add_TextChanged({param($s,$e) inputhere_textchanged $s $e})
$inputhere.Text = "ここにテキストを入力"
$screen.Controls.Add($inputhere)


入力ボックス。MultilineがFalseのままなので、1行テキストだけ入力できる

入力した場合のコンソール出力(Add_TextChangedイベントの度にテキストを出力する)

メニューバー

メニューバーは、複数のオブジェクトを組み合わせて作成する必要があり、複雑です。
メニューバーの本体オブジェクトは、System.Windows.Forms.MenuStripクラスを用いて作成できます。MenuStripクラスに、メニュー内の各項目オブジェクトを追加(Items.Addメソッド)していくことで、メニューを作成します。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.menustrip
メニューバーの各項目は、System.Windows.Forms.ToolStripMenuItemクラスを用いて作成できます。項目内にさらに項目を追加する場合、例えば「ファイル」→「新規作成」では、「ファイル」のToolStripMenuItemに、「新規作成」のToolStripMenuItemDropDownItems.Addメソッドを用いて追加します。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.toolstripmenuitem

ToolStripMenuItemには、ShortCutKeysプロパティでショートカットキーを設定することもできます。

# メニューバーの作成・表示
$menu_top = New-Object System.Windows.Forms.MenuStrip

# ファイル
$menu_file = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file.Text = "ファイル(&F)"
[void]$menu_top.Items.Add($menu_file)

# ファイル > 新規作成
$menu_file_new = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_new.Text = "新規作成(&N)"
$menu_file_new.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::N
$menu_file_new.Add_Click({param($s,$e) new_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_new)

# ファイル > 開く...
$menu_file_open = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_open.Text = "開く(&O)..."
$menu_file_open.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::O
$menu_file_open.Add_Click({param($s,$e) open_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_open)

# ファイル > 上書き保存
$menu_file_save = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_save.Text = "上書き保存(&S)"
$menu_file_save.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::S
$menu_file_save.Add_Click({param($s,$e) save_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_save)

# ファイル > 別名で保存...
$menu_file_saveas = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_saveas.Text = "別名で保存..."
$menu_file_saveas.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::Shift -bor [System.Windows.Forms.Keys]::S
$menu_file_saveas.Add_Click({param($s,$e) saveas_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_saveas)

# ファイル > 終了
$menu_file_quit = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_quit.Text = "終了(&X)"
$menu_file_quit.Add_Click({param($s,$e) quit_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_quit)

# 編集
$menu_edit = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_edit.Text = "編集(&E)"
[void]$menu_top.Items.Add($menu_edit)

# ---------
#  中略...
# ---------

# ヘルプ
$menu_help = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_help.Text = "ヘルプ(&H)"
[void]$menu_top.Items.Add($menu_help)

# ---------
#  中略...
# ---------

$screen.Controls.Add($menu_top)


メニューバー。

作例(全部載せデモ)

ここまで説明したオブジェクトを組み合わせて、簡易なGUIアプリケーションを作ってみました。
単純なカウンターアプリです。ボタンを押したり、ショートカットキーを押したり、またはメニューから「カウント」を選ぶことでカウントアップできます。「リセット」を押すと0に戻します。

psgui_1x_menu.ps1
# アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# フォームが閉じられたときに実行される関数
function screen_formclosing(){
    param(
        [System.Windows.Forms.Form]$send_from,
        [System.Windows.Forms.FormClosingEventArgs]$eh
    )
    [void] [System.Windows.Forms.MessageBox]::show("バイバイ", "",[System.Windows.Forms.MessageBoxButtons]::OK)
    Write-Host "フォームが閉じられました"
}

# ボタンが押されたときに実行される関数
function pushme_click(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    $script:push_count = $script:push_count + 1
    update_hellolabel
    Write-Host "ボタンが押されました"
}

# 入力ボックスに入力されたときに実行される関数
function inputhere_textchanged(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    update_hellolabel
}

# メニューの「カウント」が選択されたときに実行される関数
function count_click(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    $script:push_count = $script:push_count + 1
    update_hellolabel
    Write-Host "メニュー「カウント」"
}

# メニューの「リセット」が選択されたときに実行される関数
function reset_click(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    $script:push_count = 0
    update_hellolabel
    Write-Host "メニュー「リセット」"
}

# メニューの「終了」が選択されたときに実行される関数
function quit_click(){
    param(
        [System.Object]$send_from,
        [System.EventArgs]$eh
    )
    $screen.Close()
    Write-Host "メニュ「終了」"
}

# ラベルの更新
function update_hellolabel(){
    $hellolabel.Text = $inputhere.Text + ": " + $push_count
}

# ウィンドウの作成・表示
$screen = New-Object System.Windows.Forms.Form
$screen.Width = 640
$screen.Height = 480
$screen.Text = "New Window"
$screen.Show()
$screen.Activate()
$screen.Add_FormClosing({param($s,$e) screen_formclosing $s $e})

# メニューバーの作成・表示
$menu_top = New-Object System.Windows.Forms.MenuStrip

# ファイル
$menu_file = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file.Text = "ファイル(&F)"
[void]$menu_top.Items.Add($menu_file)

# ファイル > カウント
$menu_file_count = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_count.Text = "カウント(&C)..."
$menu_file_count.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::C
$menu_file_count.Add_Click({param($s,$e) count_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_count)

# ファイル > リセット
$menu_file_reset = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_reset.Text = "リセット(&R)..."
$menu_file_reset.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::R
$menu_file_reset.Add_Click({param($s,$e) reset_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_reset)

# ファイル > 終了
$menu_file_quit = New-Object System.Windows.Forms.ToolStripMenuItem
$menu_file_quit.Text = "終了(&X)..."
$menu_file_quit.Add_Click({param($s,$e) quit_click $s $e})
[void]$menu_file.DropDownItems.Add($menu_file_quit)

$screen.Controls.Add($menu_top)

# ラベルの作成・表示
$push_count = 0

$hellolabel = New-Object System.Windows.Forms.Label
$hellolabel.Text = "押: " + $push_count
$hellolabel.Location = New-Object System.Drawing.Point(0, ($menu_top.Height))
$hellolabel.Font = New-Object System.Drawing.Font("MS ゴシック", 20)
$hellolabel.Size = New-Object System.Drawing.Point(640, 32)
$hellolabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$screen.Controls.Add($hellolabel)

# ボタンの作成・表示
$pushme = New-Object System.Windows.Forms.Button
$pushme.Text = "押してね"
$pushme.Location = New-Object System.Drawing.Point(260, ($menu_top.Height + 32))
$pushme.Size = New-Object System.Drawing.Point(120, 24)
$pushme.Add_Click({param($s,$e) pushme_click $s $e})
$screen.Controls.Add($pushme)

# 入力ボックスの作成・表示
$inputhere = New-Object System.Windows.Forms.TextBox
$inputhere.Location = New-Object System.Drawing.Point(260, ($menu_top.Height + 56))
$inputhere.Size = New-Object System.Drawing.Point(120, 24)
$inputhere.Add_TextChanged({param($s,$e) inputhere_textchanged $s $e})
$inputhere.Text = "押"
$screen.Controls.Add($inputhere)

#イベントループ
[System.Windows.Forms.Application]::Run($screen)
#フォームが閉じられた(FormClosed)ときに、Runから抜ける
Write-Host "イベントループから脱出しました"
exit


動作例

さいごに

PowerShellとWindows Formsを使えば、追加の開発ツールなどをインストールすることなく、.NET上で動作するGUIアプリケーションを作ることができました。今後はこれで、テキストエディタのようなもう少し実用性のあるアプリケーションを作成してみたいです。
ありがとうございました。

TryAngle@大阪公立大学

Discussion