PowershellでもGUIが使えるんやで ~Windows FormsとPowershellでつくるGUIアプリケーション~
はじめに
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つを指定できます。
ただし、この名前空間をPowerShellで使用するためには、まずSystem.Windows.Forms
をロードする必要があります。そのために、Add-Type
コマンドレットを用いて、System.Windows.Forms
を含むアセンブリ(.NETにおけるライブラリのような役割のもの)を読み込む必要があります。
Add-Type -AssemblyName System.Windows.Forms
以下は、Add-Type
とMessageBox::Show
を使うコードの例です。このコードでメッセージボックスを表示することができます。
# アセンブリのロード
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
クラスを用いて作成できます。
New-Object
コマンドレットを用いてForm
のインスタンスを作成し、プロパティを設定してから、Form
オブジェクトのShow()
メソッドで表示、Activate()
メソッドで前面に出すようにしています。ちなみにForm
のText
プロパティでは、ウィンドウのタイトルバーに表示されるテキストを設定します。
# ウィンドウの作成・表示
$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()
というメソッドが用意されています。
このメソッドでは、ウィンドウメッセージを処理するためのイベントループを開始し、そのなかを無限ループしてくれます。言い換えれば、その後の行は一切実行されません。ただし、第一引数にFormオブジェクトを指定すると、そのフォームが閉じられた(System.Windows.Forms.Form.FormClosed
イベントが発生した)場合にイベントループから脱出し、スクリプトの実行を再開してくれます。
イベントループを追加したコードが以下です。
# アセンブリのロード
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
イベントです。
このイベントでは、System.Windows.Forms.FormClosingEventHandler
という型で、引数2つでイベントハンドラが呼び出されます。そのため、イベントハンドラとなる関数も、引数を2つ受け取るような関数にします。
今回は、ウィンドウが閉じられたときに、メッセージボックスが表示されるようにしてみました。
# フォームが閉じられたときに実行される関数
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})
これらを実装したコードが以下です。
# アセンブリのロード
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
クラスを用いて作成できます。
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
クラスを用いて作成できます。
ボタンが押されたときに発生するイベントは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
クラスを用いて作成できます。
テキストが入力されたり、消されたときに発生するイベントは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
メソッド)していくことで、メニューを作成します。
メニューバーの各項目は、System.Windows.Forms.ToolStripMenuItem
クラスを用いて作成できます。項目内にさらに項目を追加する場合、例えば「ファイル」→「新規作成」では、「ファイル」のToolStripMenuItem
に、「新規作成」のToolStripMenuItem
をDropDownItems.Add
メソッドを用いて追加します。
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に戻します。
# アセンブリのロード
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アプリケーションを作ることができました。今後はこれで、テキストエディタのようなもう少し実用性のあるアプリケーションを作成してみたいです。
ありがとうございました。
Discussion