💻

Windows標準だけでGUIを作成 ― PowerShell+.NET Framework

に公開

はじめに

Windows環境では、Visual Studioなど特別な開発環境を用意しなくても、標準搭載されている PowerShell 5.x.NET Framework 4.x を使って簡単にGUIアプリケーションを作ることができる。
例えば、業務ツールにちょっとした入力フォームやボタンを追加したい場合、PowerShellでWinFormsやWPFを利用すれば数行のコードで実現可能。
さらに、同梱されている csc.exe(C#コンパイラ) を使えば、C#のコードを書く必要はあるが EXE化 することも可能。

本記事では、

  1. PowerShell + WinForms
  2. PowerShell + WPF
  3. C#をcsc.exeでコンパイル(WinForms版)
  4. C#をPowerShellでインプロセス実行(Add-Type)

の順で、「Windows標準だけ」で完結するGUI作成方法を紹介する。

No. 方法 概要
1 PowerShell + WinForms PowerShellでWinFormsのUIを作成。実装・学習コスト低。デザイン自由度は低。
2 PowerShell + WPF PowerShellでWPFのUIを作成。高機能レイアウト。XAMLなしでも可。
3 C# csc.exeコンパイル C#のコードを標準ツールでEXE化。1,2より処理速度に期待できる。ユーザーはダブルクリックで簡単に実行可能。
4 C#をPowerShellでインプロセス実行 C#のコードをPowerShellのプロセス内で実行。1,2より処理速度に期待できる。処理の一部だけC#化することも可能。

最小コード例

1. PowerShell + WinForms

  • Add-Type -AssemblyName System.Windows.Formsが必須。
  • .Add_Click({ ... })のようにイベントを追加する。
  • Controls.Add()に追加する順で描画される。
Add-Type -AssemblyName System.Windows.Forms
$form = [Windows.Forms.Form]::new()
$form.Text='WinForms'; $form.Width=300; $form.Height=150
$btn = [Windows.Forms.Button]::new()
$btn.Text='WinForms'; $btn.Dock='Fill'
$btn.Add_Click({ [System.Windows.Forms.MessageBox]::Show('Hello!') })
$form.Controls.Add($btn)
$form.ShowDialog() | Out-Null

2. PowerShell + WPF

  • Add-Type -AssemblyName PresentationFrameworkが必須。
  • XAMLは別ファイルにすることも可能。
  • .Add_Click({ ... })のようにイベントを追加する。
Add-Type -AssemblyName PresentationFramework
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="WPF XAML" Width="300" Height="150">
  <Button Name="btn" Content="Wpf" Width="80" Height="30" />
</Window>
"@
$win = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
$btn = $win.FindName("btn")
$btn.Add_Click({ [System.Windows.MessageBox]::Show('Hello!') })
$win.ShowDialog() | Out-Null

3. C#をcsc.exeでコンパイル

  • Windows標準の C# コンパイラ csc.exe を使用。
  • 以下の例では$srcにC#のコードを記述しているが、別ファイルにすることも可能。
# x64優先。無ければx86のcsc.exeを使用
$fx = Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319'
if (!(Test-Path $fx)) { $fx = Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319' }
$csc = Join-Path $fx 'csc.exe'

# --- WinForms最小コード(PowerShell版と同じUI/動作)をEXE化 ---
$src = @'
using System;
using System.Windows.Forms;

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

            var btn = new Button { Text = "WinForms", Dock = DockStyle.Fill };
            btn.Click += (s, e) => MessageBox.Show("Hello!");

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

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

$cs  = Join-Path $env:TEMP 'demo_winforms.cs'
$exe = Join-Path $PSScriptRoot 'demo_winforms.exe'
$src | Set-Content $cs -Encoding UTF8

# /target:winexe でコンソール非表示
# 参照DLLを明示
& $csc /nologo /target:winexe `
  /r:System.Windows.Forms.dll /r:System.Drawing.dll `
  /out:$exe $cs


4. C# をインプロセス実行(Add-Type)

Add-TypePowerShellプロセス内でC#コードをその場コンパイルして読み込むコマンドである。
外部EXEを作らずに .NET の機能を直接呼べるため、配布はps1だけで済み、処理の重い部分だけC#に寄せて高速化といった使い方もできる。

  • Add-Type -TypeDefinition '<C#コード>' -Language CSharp で型を生成(プロセス内にロード)。
  • 必要に応じて -ReferencedAssemblies で参照DLLを追加(WinFormsなら System.Windows.Forms / System.Drawing など)。
  • 生成された型は [名前空間.クラス名]::メソッド() の形で PowerShell から呼べる。
  • 同じ型名を再定義(Add-Type)するとエラーになるため、試行錯誤時は名前空間/クラス名を変えるか、セッションを再起動する。

https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/add-type

# WinFormsを使うので参照を追加してから型を定義
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Windows.Forms;

namespace Demo
{
    public static class UI
    {
        public static void ShowForm()
        {
            var form = new Form { Text = "WinForms (in-proc)", Width = 300, Height = 150 };

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

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

# PowerShellからC#の処理を実行
[Demo.UI]::ShowForm()

実践例

(1)CSVを読込んで一覧表示

PowerShell + WinFormsの場合と、PowerShell + WPFの場合で下記の機能を実装する。

  • ボタンクリックでファイル選択ダイアログをオープン
  • CSVファイルを選択すると一覧表示

PowerShell + WinFormsの場合

読み込んだCSVをDataTableに変換し、WinFormsのDataGridViewにバインド。

Add-Type -AssemblyName System.Windows.Forms

# CSVファイルを読み込むためのヘルパークラス
class CsvHelper {
    [string[]] ReadCsv($filePath) {
        if (-Not (Test-Path $filePath)) {
            throw "File not found: $filePath"
        }
        return Get-Content $filePath
    }
    # CSVファイルの内容をDataTableに変換する
    [System.Data.DataTable] ConvertToDataTable($csvData) {
        $dataTable = [System.Data.DataTable]::new()
        if ($csvData.Length -gt 0) {
            # ヘッダー行を分割してDataTableの列を作成
            $headers = $csvData[0].Split(',')
            foreach ($header in $headers) {
                $dataTable.Columns.Add($header)
            }
            # データ行をDataTableに追加
            for ($i = 1; $i -lt $csvData.Length; $i++) {
                $row = $dataTable.NewRow()
                $values = $csvData[$i].Split(',')
                for ($j = 0; $j -lt $values.Length; $j++) {
                    $row[$j] = $values[$j]
                }
                $dataTable.Rows.Add($row)
            }
        }
        return $dataTable
    }
}

# WinFormsのフォームを作成
$form = [System.Windows.Forms.Form]::new()
$form.Text = "CSV Viewer"
$form.Size = [System.Drawing.Size]::new(800, 600)

# DataGridViewを作成
$dataGridView = [System.Windows.Forms.DataGridView]::new()
$dataGridView.Dock = 'Fill'
$dataGridView.AutoSizeColumnsMode = 'Fill'

# ボタンを作成
$button = [System.Windows.Forms.Button]::new()
$button.Text = "Select CSV File"
$button.Dock = 'Top'
$button.Add_Click({
    # CSVファイルを選択するダイアログを表示
    $openFileDialog = [System.Windows.Forms.OpenFileDialog]::new()
    $openFileDialog.Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*"
    $openFileDialog.Title = "Select a CSV File"
    if ($openFileDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
        # CSVファイルを読み込み、DataGridViewに表示
        $csvHelper = [CsvHelper]::new()
        $csvData = $csvHelper.ReadCsv($openFileDialog.FileName)
        $dataTable = $csvHelper.ConvertToDataTable($csvData)
        $dataGridView.DataSource = $dataTable
    }
})

# フォームにコントロールを追加
$form.Controls.Add($dataGridView)
$form.Controls.Add($button)

# フォームを表示
$form.Add_Shown({$form.Activate()})
[void]$form.ShowDialog()

PowerShell + WPFの場合

読み込んだCSVをDataTableに変換し、WPFのDataGridにバインド。

Add-Type -AssemblyName PresentationFramework

# CSVファイルを読み込むためのヘルパークラス
class CsvHelper {
    [string[]] ReadCsv($filePath) {
        if (-Not (Test-Path $filePath)) {
            throw "File not found: $filePath"
        }
        return Get-Content $filePath
    }

    [System.Data.DataTable] ConvertToDataTable($csvData) {
        $dataTable = [System.Data.DataTable]::new()
        if ($csvData.Length -gt 0) {
            # ヘッダー行を分割してDataTableの列を作成
            $headers = $csvData[0].Split(',')
            foreach ($header in $headers) {
                $dataTable.Columns.Add($header)
            }
            # データ行をDataTableに追加
            for ($i = 1; $i -lt $csvData.Length; $i++) {
                $row = $dataTable.NewRow()
                $values = $csvData[$i].Split(',')
                for ($j = 0; $j -lt $values.Length; $j++) {
                    $row[$j] = $values[$j]
                }
                $dataTable.Rows.Add($row)
            }
        }
        return $dataTable
    }
}

# XAMLコードを定義
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CsvViewer" Width="800" Height="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Name="selectCsvButton" Grid.Row="0" Content="Select CSV File" Margin="0,0,0,0"/>
        <DataGrid Name="dataGrid" Grid.Row="1" AutoGenerateColumns="True" />
    </Grid>
</Window>
"@

# XAMLを読み込んでウィンドウを作成
$reader = ([System.Xml.XmlNodeReader]::new($xaml))
$window = [Windows.Markup.XamlReader]::Load($reader)
$selectCsvButton = $window.FindName("selectCsvButton")
$dataGrid = $window.FindName("dataGrid")
$selectCsvButton.Add_Click({
    # CSVファイルを選択するダイアログを表示
    $openFileDialog = [Microsoft.Win32.OpenFileDialog]::new()
    $openFileDialog.Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*"
    $openFileDialog.Title = "Select a CSV File"
    if ($openFileDialog.ShowDialog() -eq $true) {
        # CSVファイルを読み込み、DataGridに表示
        $csvHelper = [CsvHelper]::new()
        $csvData = $csvHelper.ReadCsv($openFileDialog.FileName)
        $dataTable = $csvHelper.ConvertToDataTable($csvData)
        $dataGrid.ItemsSource = $dataTable.DefaultView
    }
})

# ウィンドウを表示
$window.ShowDialog() | Out-Null

(2)PowerShellで一部の処理をC#化

  • 下記のサンプルはベースはPowerShellで一部の重い処理だけC#化するサンプル。
  • PowerShellで実装した処理をAction型でC#に渡し、C#内でコールバック実行することも可能。下記のサンプルではUI更新のPowerShell処理をC#に渡し、C#コード内で実行している。
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase

# 重い処理を行うクラスをC#で定義
Add-Type -TypeDefinition @"
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;

public class Processor
{
    public Task StartProcessingAsync(Action<string> updateStatus, Dispatcher dispatcher)
    {
        return Task.Run(() => {
            try {
                // 処理タスクの定義
                var tasks = new[] {
                    new { TaskNo = 1, Duration = 1000 },
                    new { TaskNo = 2, Duration = 2000 },
                    new { TaskNo = 3, Duration = 1500 },
                    new { TaskNo = 4, Duration = 1000 }
                };

                // 処理開始の通知
                // Task.Runはバックグラウンドスレッドで実行されるため、
                // Dispatcherを使用してUIスレッドに通知する必要がある
                dispatcher.Invoke(() => updateStatus("Starting Processing..."));

                for (int i = 0; i < tasks.Length; i++)
                {
                    // 現在のタスクを取得
                    var currentTask = tasks[i];

                    // タスクの実行
                    Thread.Sleep(currentTask.Duration);

                    // UIスレッドで進捗通知
                    dispatcher.Invoke(() => {
                        // 進捗の計算
                        double progress = (double)(i + 1) / tasks.Length * 100;
                        updateStatus(string.Format("Processing... ({0:0.0}%)", progress));
                    });
                }

                // 全てのタスクが完了したことを通知
                dispatcher.Invoke(() => updateStatus("Processing complete!"));
            }
            catch (Exception ex)
            {
                dispatcher.Invoke(() => updateStatus("Error: " + ex.Message));
            }
        });
    }
}
"@ -ReferencedAssemblies @("WindowsBase")

# XAMLでUIを定義
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Processing Demo" Height="200" Width="300">
    <StackPanel>
        <Button Name="ProcessButton" Width="100" Height="50" Margin="10">Start Process</Button>
        <TextBlock Name="StatusText" FontSize="16" Margin="10">Status: Ready</TextBlock>
    </StackPanel>
</Window>
"@

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

$processButton = $window.FindName("ProcessButton")
$statusText = $window.FindName("StatusText")

$processor = New-Object Processor
# クリックイベントハンドラを定義
$processButton.Add_Click({
    $processButton.IsEnabled = $false

    # 更新用のActionを定義(C#のクラスに渡す)
    $updateStatusAction = [Action[string]]{
        param($status)
        $statusText.Text = "Status: $status"
    }

    # Taskの開始
    $task = $processor.StartProcessingAsync($updateStatusAction, $window.Dispatcher)

    # タスク完了後の処理
    $task.ContinueWith({
        $processButton.IsEnabled = $true
    }, [System.Threading.Tasks.TaskScheduler]::FromCurrentSynchronizationContext())
})

$window.ShowDialog() | Out-Null

【補足】csc.exe / PowerShell Add-Type の C# & .NET バージョンについて

1. csc.exe (C# コンパイラ)

  • 公式: csc.exe (C# Compiler)
  • 概要: Windows に同梱される C# コンパイラ。
    Visual Studio / .NET Framework に付属するものは「.NET Framework のバージョンに対応した C#」が使われます。
.NET Framework C# バージョン
2.0 / 3.0 C# 2.0
3.5 C# 3.0
4.0 C# 4.0
4.5 / 4.6 C# 5.0
4.6.2 / 4.7 C# 6.0
4.7.2 / 4.8 C# 7.0 / 7.3

csc -version で手元のコンパイラのバージョンを確認可能。


2. PowerShell Add-Type

PowerShell 実行基盤 利用される C# コンパイラ C# バージョン目安
5.1 .NET Framework 4.x csc.exe (Framework 付属) 7.0 / 7.3

PowerShell 5.1 の Add-Type は「.NET Framework 4.8 + C# 7.3」まで。
それ以降の最新 C# 機能を使いたい場合は PowerShell 7 系を使う必要がある。


3. バージョン確認コマンド例

# csc.exe のバージョン確認
& (Get-Command csc.exe).Source -version

# Add-Type が利用する C# コンパイラ (Roslyn / Framework) の確認
Add-Type -TypeDefinition "public class V { }"
[Microsoft.CSharp.CSharpCodeProvider].Assembly.GetName().Version

Discussion