4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows標準のPowerShellだけで簡易GUIツールを作る WinForms / WPF / csc / Add-Type

4
Posted at

Windowsで社内向けの小さい確認ツールや、CSVを選んで中身を見る程度のGUIがほしいことがあります。

このくらいの用途なら、Visual Studioでプロジェクトを作らなくても、Windowsに入っているPowerShell 5.1と.NET Frameworkだけで作れます。

この記事では、次の4パターンを扱います。

  • PowerShell + WinFormsで最小GUIを作る
  • PowerShell + WPFでXAMLベースのGUIを作る
  • Windows標準のcsc.exeでC#コードをEXE化する
  • PowerShellのAdd-TypeでC#をインプロセス実行する

前提

想定環境は次の通りです。

項目 内容
OS Windows 10 / 11
PowerShell Windows PowerShell 5.1
.NET .NET Framework 4.x
追加インストール なし

PowerShell 7ではなく、Windowsに標準で入っているWindows PowerShell 5.1を前提にしています。

確認する場合は次のコマンドを使います。

$PSVersionTable.PSVersion
[System.Environment]::Version

どれを使うか

先に使い分けです。

方法 向いている用途 配布
PowerShell + WinForms ボタン、入力欄、一覧などの簡易ツール .ps1
PowerShell + WPF レイアウトを少し整えたいツール .ps1
C# + csc.exe ダブルクリックで起動するEXEにしたい .exe
PowerShell + Add-Type 一部だけC#で書きたい、.NET APIを直接使いたい .ps1

個人的には、まずWinFormsで十分かを見ます。
画面が複雑になってきたらWPF、利用者にPowerShellを意識させたくないならEXE化、処理の一部だけC#に寄せたいならAdd-Type、という判断でよいと思います。

1. PowerShell + WinForms

一番小さい例です。

Add-Type -AssemblyName System.Windows.Forms

$form = [System.Windows.Forms.Form]::new()
$form.Text = 'WinForms'
$form.Width = 300
$form.Height = 150

$button = [System.Windows.Forms.Button]::new()
$button.Text = 'Click'
$button.Dock = 'Fill'
$button.Add_Click({
    [System.Windows.Forms.MessageBox]::Show('Hello from PowerShell')
})

$form.Controls.Add($button)
$form.ShowDialog() | Out-Null

winforms.gif

WinFormsは古い技術ですが、PowerShellから触るにはかなり手軽です。

業務用の小さいGUIなら、次のような用途に向いています。

  • ファイル選択
  • 入力値の確認
  • ボタン実行
  • CSVや一覧の表示
  • 処理結果のメッセージ表示

2. PowerShell + WPF

WPFを使うと、XAMLで画面を定義できます。

Add-Type -AssemblyName PresentationFramework

[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="WPF XAML" Width="300" Height="150">
  <Grid>
    <Button Name="Button" Content="Click" Width="100" Height="32" />
  </Grid>
</Window>
"@

$reader = [System.Xml.XmlNodeReader]::new($xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)

$button = $window.FindName('Button')
$button.Add_Click({
    [System.Windows.MessageBox]::Show('Hello from WPF')
})

$window.ShowDialog() | Out-Null

wpf.gif

WinFormsよりコード量は増えますが、レイアウトをXAML側に分けられるのが利点です。

ただし、PowerShellだけでWPFを本格的に書き続けるとつらくなります。
画面が大きくなるなら、素直にC#のWPFプロジェクトにした方が保守しやすいです。

3. CSVを選んで一覧表示する

実務寄りの例として、CSVを選択してDataGridViewに表示します。

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Data

function Convert-CsvToDataTable {
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    $rows = @(Import-Csv -Path $Path)
    $table = [System.Data.DataTable]::new()

    if ($rows.Count -eq 0) {
        return $table
    }

    foreach ($name in $rows[0].PSObject.Properties.Name) {
        [void]$table.Columns.Add($name)
    }

    foreach ($row in $rows) {
        $dataRow = $table.NewRow()
        foreach ($name in $row.PSObject.Properties.Name) {
            $dataRow[$name] = $row.$name
        }
        [void]$table.Rows.Add($dataRow)
    }

    return $table
}

$form = [System.Windows.Forms.Form]::new()
$form.Text = 'CSV Viewer'
$form.Width = 800
$form.Height = 600

$button = [System.Windows.Forms.Button]::new()
$button.Text = 'CSVを開く'
$button.Dock = 'Top'

$grid = [System.Windows.Forms.DataGridView]::new()
$grid.Dock = 'Fill'
$grid.AutoSizeColumnsMode = 'Fill'

$button.Add_Click({
    $dialog = [System.Windows.Forms.OpenFileDialog]::new()
    $dialog.Filter = 'CSV Files (*.csv)|*.csv|All Files (*.*)|*.*'

    if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
        $grid.DataSource = Convert-CsvToDataTable -Path $dialog.FileName
    }
})

$form.Controls.Add($grid)
$form.Controls.Add($button)
$form.ShowDialog() | Out-Null

winforms_list.gif

CSVのパースは自前で,分割せず、Import-Csvを使う方が安全です。
引用符付きの値やカンマを含む値を考えると、PowerShell標準のCSVパーサに任せる方がよいです。

4. csc.exeでEXE化する

Windowsには.NET Framework付属のC#コンパイラcsc.exeがあります。
これを使うと、Visual Studioなしで小さいEXEを作れます。

$framework = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319'
if (!(Test-Path $framework)) {
    $framework = Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319'
}

$csc = Join-Path $framework 'csc.exe'

$source = @'
using System;
using System.Windows.Forms;

namespace DemoWinForms
{
    internal static class Program
    {
        [STAThread]
        private static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var button = new Button { Text = "Click", Dock = DockStyle.Fill };
            button.Click += (sender, e) => MessageBox.Show("Hello from C#");

            var form = new Form { Text = "WinForms EXE", Width = 300, Height = 150 };
            form.Controls.Add(button);

            Application.Run(form);
        }
    }
}
'@

$cs = Join-Path $env:TEMP 'demo_winforms.cs'
$exe = Join-Path $PSScriptRoot 'demo_winforms.exe'

$source | Set-Content -Path $cs -Encoding UTF8

& $csc /nologo /target:winexe `
    /r:System.Windows.Forms.dll `
    /r:System.Drawing.dll `
    /out:$exe `
    $cs

make_exe.gif

/target:winexeにすると、起動時にコンソールウィンドウを出さないEXEになります。

この方法は、利用者に.ps1を実行させたくない場合に便利です。
ただし、大きいアプリをこれで作るより、普通にC#プロジェクトを作った方がよいです。

5. Add-TypeでC#をPowerShell内に読み込む

Add-Typeを使うと、PowerShellの中でC#コードをコンパイルして、そのまま呼び出せます。

Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Windows.Forms;

namespace Demo
{
    public static class UI
    {
        public static void Show()
        {
            var form = new Form { Text = "Add-Type", Width = 300, Height = 150 };

            var button = new Button { Text = "Click", Dock = DockStyle.Fill };
            button.Click += (sender, e) => MessageBox.Show("Hello from Add-Type");

            form.Controls.Add(button);
            form.ShowDialog();
        }
    }
}
'@ -Language CSharp

[Demo.UI]::Show()

in_proc.gif

Add-Typeは、次のようなときに使えます。

  • PowerShellだけでは書きにくい処理をC#で書く
  • .NETの型をまとめて扱う
  • GUIはPowerShell、重い処理だけC#に寄せる

注意点として、同じPowerShellセッション内で同じ名前空間・同じクラス名を再定義するとエラーになります。
試行錯誤するときは、PowerShellを起動し直すか、クラス名を変えると楽です。

6. GUIスクリプトでハマりやすい点

実行ポリシー

.ps1を実行するとき、環境によっては実行ポリシーで止まります。

一時的に実行するなら、次のようにプロセス単位で緩める方法があります。

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

恒久的に変更する必要があるかは、環境の運用ルールに合わせて判断してください。

STAThread

クリップボードや一部のダイアログ、WPFまわりではSTAが必要になることがあります。

PowerShellから別プロセスで起動する場合は、-STAを付けて実行できます。

powershell.exe -STA -File .\tool.ps1

C#でEXE化する場合は、Main[STAThread]を付けます。

[STAThread]
private static void Main()
{
    // ...
}

PowerShell 5.1前提にする

この記事の内容は、Windows PowerShell 5.1と.NET Frameworkを前提にしています。

PowerShell 7は.NETの実行基盤が違うため、WinForms/WPFやAdd-Typeまわりの挙動が変わることがあります。
「Windows標準だけ」で配る小ツールなら、まずはPowerShell 5.1前提で書く方が説明しやすいです。

本格アプリにしない

PowerShell GUIは便利ですが、本格的なアプリ開発環境ではありません。

次のような状態になったら、C#のWPF/WinForms/WinUIなどに移した方がよいです。

  • 画面数が増えた
  • 状態管理が複雑になった
  • 非同期処理が多い
  • エラー処理やログが重要になった
  • 複数人で保守する

まとめ

Windows標準だけでも、PowerShellからWinForms/WPFを呼べば簡易GUIツールは作れます。

使い分けは次の感覚です。

やりたいこと 選択肢
とにかく小さくGUIを作る PowerShell + WinForms
XAMLで少し整えたい PowerShell + WPF
ダブルクリックで配りたい csc.exeでEXE化
一部だけC#で書きたい Add-Type

業務用の小さい確認ツールや、自分用の作業補助ツールなら十分使えます。
一方で、長く保守するアプリに育ちそうなら、早めに通常のC#プロジェクトへ移すのが無難です。

4
7
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
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?