From 0ad48232b9027f1e123470b1da4e69f07a37dfd9 Mon Sep 17 00:00:00 2001 From: papanda925 Date: Wed, 2 Sep 2026 00:18:58 +0900 Subject: [PATCH] Improve WPF sample docs and path handling --- README.md | 49 ++++++++++ Tests/Test-Repository.ps1 | 26 +++++- WPF_CustomGraphicalInputBoxSample.ps1 | 92 ++++++++----------- WPF_GraphicalDatePickerSample.ps1 | 92 +++++++++---------- .../WPF_SimpleWeatherFormSample.ps1 | 71 ++++++-------- 5 files changed, 184 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index 991bf55..feee122 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,54 @@ # PowerShell WPF XAML GUI Sample Codes / PowerShell XAML Designer +PowerShell だけで WPF/XAML の画面を作成・編集するためのデザイナーと、すぐに動かせる GUI サンプル集です。Visual Studio や追加モジュールを実行時に必要としません。 + +## はじめに(日本語) + +### 動作環境 + +- Windows 10 / 11 +- Windows PowerShell 5.1 または PowerShell 7 +- WPF を表示できるデスクトップ環境 + +PowerShell 7 では、WPF のために `-STA` を付けて起動してください。 + +### XAML デザイナーを起動する + +```powershell +git clone https://github.com/papanda925/PowerShell_WPF_XAML_GUI_SampleCodes.git +Set-Location .\PowerShell_WPF_XAML_GUI_SampleCodes +pwsh.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1 +``` + +Windows PowerShell 5.1 の場合は、最後の行を次に置き換えます。 + +```powershell +powershell.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1 +``` + +基本操作は [日本語スタートガイド](./XamlDesigner/GETTING_STARTED.ja.md)、設計と制限事項は [デザイナーのREADME](./XamlDesigner/README.md) を参照してください。 + +### 収録サンプル + +| サンプル | 内容 | 実行例 | +|---|---|---| +| `WPF_CustomGraphicalInputBoxSample.ps1` | テキスト入力ダイアログ。OK時に入力文字列を返します | `.\WPF_CustomGraphicalInputBoxSample.ps1` | +| `WPF_GraphicalDatePickerSample.ps1` | 日付選択ダイアログ。OK時に `DateTime` を返します | `.\WPF_GraphicalDatePickerSample.ps1` | +| `WPF_SimpleWeatherFormSample` | Grid / StackPanel と画像のレイアウト例 | `.\WPF_SimpleWeatherFormSample\WPF_SimpleWeatherFormSample.ps1` | +| `WPF_InkCanvas.ps1` | InkCanvas の手書き入力例 | `.\WPF_InkCanvas.ps1` | +| `WPF_OCR_Sample.ps1` | Windows OCR API の利用例 | `.\WPF_OCR_Sample.ps1` | + +入力・日付サンプルは、キャンセル時には値を返しません。スクリプトと同じフォルダーの XAML を `$PSScriptRoot` から解決するため、どのカレントディレクトリからでも起動できます。 + +### トラブルシューティング + +- `The term ... is not recognized`:リポジトリのルートへ移動してから、上記の相対パスで実行してください。 +- STA に関するエラー:`pwsh.exe -STA` または `powershell.exe -STA` で起動してください。 +- 実行ポリシーで停止する:管理者の許可なくポリシーを変更せず、組織のルールに従って署名・許可された方法で実行してください。 +- XAML の読み込みエラー:`x:Name` の重複、閉じタグ、名前空間、単体 `XamlReader` で利用できないイベント属性を確認してください。 + +--- + This repository is being refocused around a **PowerShell-only WPF/XAML GUI designer** for Windows. The goal is to make it possible to create and maintain PowerShell WPF screens even in environments where Visual Studio, Blend, paid IDE add-ons, or third-party GUI designers cannot be installed or used. diff --git a/Tests/Test-Repository.ps1 b/Tests/Test-Repository.ps1 index d01945a..6817dce 100644 --- a/Tests/Test-Repository.ps1 +++ b/Tests/Test-Repository.ps1 @@ -57,6 +57,30 @@ foreach ($relativePath in $requiredFiles) { } } +$standaloneSamples = @( + 'WPF_CustomGraphicalInputBoxSample.ps1', + 'WPF_GraphicalDatePickerSample.ps1', + 'WPF_SimpleWeatherFormSample\WPF_SimpleWeatherFormSample.ps1' +) +foreach ($relativePath in $standaloneSamples) { + $samplePath = Join-Path $repositoryRoot $relativePath + if (-not (Test-Path -LiteralPath $samplePath)) { + $errorsFound.Add("Required standalone sample is missing: $relativePath") + continue + } + + $sampleCode = [System.IO.File]::ReadAllText($samplePath) + if (-not $sampleCode.Contains('$PSScriptRoot')) { + $errorsFound.Add("Standalone sample must resolve assets from PSScriptRoot: $relativePath") + } + if ($sampleCode -match 'Get-Content\s+\.\\') { + $errorsFound.Add("Standalone sample still reads XAML from the current directory: $relativePath") + } + if ($sampleCode -match '(?i)\.add_[a-z]+\.Invoke\(') { + $errorsFound.Add("Standalone sample uses indirect event registration: $relativePath") + } +} + $templateCodePath = Join-Path $repositoryRoot 'XamlDesigner\Templates\BlankWindow.ps1' if (Test-Path -LiteralPath $templateCodePath) { $templateCode = [System.IO.File]::ReadAllText($templateCodePath) @@ -114,4 +138,4 @@ if ($errorsFound.Count -gt 0) { throw "$($errorsFound.Count) repository validation error(s) found." } -Write-Host 'PowerShell/XAML syntax, required files, generated markers, beginner UI, and CI policy checks passed.' +Write-Host 'PowerShell/XAML syntax, standalone samples, required files, generated markers, beginner UI, and CI policy checks passed.' diff --git a/WPF_CustomGraphicalInputBoxSample.ps1 b/WPF_CustomGraphicalInputBoxSample.ps1 index 0628740..7063b90 100644 --- a/WPF_CustomGraphicalInputBoxSample.ps1 +++ b/WPF_CustomGraphicalInputBoxSample.ps1 @@ -1,52 +1,40 @@ -#presentationframeworkを読み込み -Add-Type -AssemblyName presentationframework - -#XAMLファイルを読み込んでxmlに変換 -[System.Xml.XmlDocument]$xaml = Get-Content .\WPF_CustomGraphicalInputBoxSample.xaml -$xaml.GetType().FullName -#visual studio でデザインした場合に付与され、Powershellで読み込む時に不要な要素を削除 -$xaml.window.RemoveAttribute("x:Class") -$xaml.window.RemoveAttribute("mc:Ignorable") - -#読み込んだxamlをXmlNodeReaderにキャスト -[System.Xml.XmlNodeReader]$xamlReader = $xaml -as "System.Xml.XmlNodeReader" -$xamlReader.GetType().FullName -#Windows.Markup.XamlReaderで読み込み -[System.Windows.Window]$mainWindow = [Windows.Markup.XamlReader]::Load($xamlReader) -$mainWindow.gettype().FullName - -#各種コントロールを取得 -[System.Windows.Controls.Button]$okButton = $mainWindow.FindName("OkButton") -$okButton.gettype().FullName -[System.Windows.Controls.Button]$CancelButton = $mainWindow.FindName("CancelButton") -$CancelButton.gettype().FullName -[System.Windows.Controls.TextBox]$TextBox = $mainWindow.FindName("TextBox") -$TextBox.gettype().FullName -[System.Windows.Controls.Label]$Label = $mainWindow.FindName("Label") -$Label.gettype().FullName - -#イベントを追加 -$okButton.add_Click.Invoke({ - Write-Output "Using $textBox.Text" | Out-Host - $mainWindow.DialogResult = $true - $mainWindow.Close() - return -}) -$CancelButton.add_Click.Invoke({ - Write-Output "Cancel Click Bye" | Out-Host - $mainWindow.DialogResult = $false - $mainWindow.Close() - return -}) - -#表示 -#$mainWindow.showDialog() | out-null -[System.Boolean]$result = $mainWindow.showDialog() -$result.GetType().FullName -Write-Output "Result is $result" | Out-Host - -if ($result -eq $true ) -{ - $x = $textBox.Text - $x -} +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Add-Type -AssemblyName PresentationFramework + +# 呼び出し元のカレントディレクトリに依存せず、スクリプトと同じ場所の XAML を読む。 +$xamlPath = Join-Path $PSScriptRoot 'WPF_CustomGraphicalInputBoxSample.xaml' +[xml]$xaml = Get-Content -LiteralPath $xamlPath -Raw + +# Visual Studio が追加する、単体の XamlReader では不要な属性を取り除く。 +$xamlRoot = $xaml.DocumentElement +[void]$xamlRoot.RemoveAttribute('Class', 'http://schemas.microsoft.com/winfx/2006/xaml') +[void]$xamlRoot.RemoveAttribute('Ignorable', 'http://schemas.openxmlformats.org/markup-compatibility/2006') + +$xamlReader = [System.Xml.XmlNodeReader]::new($xaml) +try { + [System.Windows.Window]$mainWindow = [System.Windows.Markup.XamlReader]::Load($xamlReader) +} +finally { + $xamlReader.Close() +} + +[System.Windows.Controls.Button]$okButton = $mainWindow.FindName('OkButton') +[System.Windows.Controls.Button]$cancelButton = $mainWindow.FindName('CancelButton') +[System.Windows.Controls.TextBox]$textBox = $mainWindow.FindName('TextBox') + +$okButton.Add_Click({ + $mainWindow.DialogResult = $true +}) +$cancelButton.Add_Click({ + $mainWindow.DialogResult = $false +}) + +$result = $mainWindow.ShowDialog() +if ($result -eq $true) { + $textBox.Text +} diff --git a/WPF_GraphicalDatePickerSample.ps1 b/WPF_GraphicalDatePickerSample.ps1 index 09ec156..191e6c2 100644 --- a/WPF_GraphicalDatePickerSample.ps1 +++ b/WPF_GraphicalDatePickerSample.ps1 @@ -1,57 +1,47 @@ -#presentationframeworkを読み込み -Add-Type -AssemblyName presentationframework - -#XAMLファイルを読み込んでxmlに変換 -[System.Xml.XmlDocument]$xaml = Get-Content .\WPF_GraphicalDatePickerSample.xaml -$xaml.GetType().FullName -#visual studio でデザインした場合に付与され、Powershellで読み込む時に不要な要素を削除 -$xaml.window.RemoveAttribute("x:Class") -$xaml.window.RemoveAttribute("mc:Ignorable") - -#読み込んだxamlをXmlNodeReaderにキャスト -[System.Xml.XmlNodeReader]$xamlReader = $xaml -as "System.Xml.XmlNodeReader" -$xamlReader.GetType().FullName -#Windows.Markup.XamlReaderで読み込み -[System.Windows.Window]$mainWindow = [Windows.Markup.XamlReader]::Load($xamlReader) -$mainWindow.gettype().FullName - -#各種コントロールを取得 -[System.Windows.Controls.Button]$okButton = $mainWindow.FindName("OKButton") -$okButton.gettype().FullName -[System.Windows.Controls.Button]$CancelButton = $mainWindow.FindName("CancelButton") -$CancelButton.gettype().FullName -#[System.Windows.Controls.TextBox]$TextBox = $mainWindow.FindName("TextBox") -#$TextBox.gettype().FullName -[System.Windows.Controls.Label]$Label = $mainWindow.FindName("Label") -$Label.gettype().FullName -[System.Windows.Controls.Calendar]$Calendar = $mainWindow.FindName("Calendar") -$Calendar.gettype().FullName - -$Calendar.SelectedDate = [datetime]::Now -$Label.Content = $Calendar.SelectedDate.ToString("'Today:' yyyy/MM/dd") - -#イベントを追加 -$okButton.add_Click.Invoke({ -# Write-Output "Using $textBox.Text" | Out-Host +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Add-Type -AssemblyName PresentationFramework + +$xamlPath = Join-Path $PSScriptRoot 'WPF_GraphicalDatePickerSample.xaml' +[xml]$xaml = Get-Content -LiteralPath $xamlPath -Raw + +$xamlRoot = $xaml.DocumentElement +[void]$xamlRoot.RemoveAttribute('Class', 'http://schemas.microsoft.com/winfx/2006/xaml') +[void]$xamlRoot.RemoveAttribute('Ignorable', 'http://schemas.openxmlformats.org/markup-compatibility/2006') + +$xamlReader = [System.Xml.XmlNodeReader]::new($xaml) +try { + [System.Windows.Window]$mainWindow = [System.Windows.Markup.XamlReader]::Load($xamlReader) +} +finally { + $xamlReader.Close() +} + +[System.Windows.Controls.Button]$okButton = $mainWindow.FindName('OKButton') +[System.Windows.Controls.Button]$cancelButton = $mainWindow.FindName('CancelButton') +[System.Windows.Controls.Label]$label = $mainWindow.FindName('Label') +[System.Windows.Controls.Calendar]$calendar = $mainWindow.FindName('Calendar') + +$calendar.SelectedDate = [datetime]::Today +$label.Content = ([datetime]$calendar.SelectedDate).ToString("'Selected:' yyyy/MM/dd") +$calendar.Add_SelectedDatesChanged({ + if ($null -ne $calendar.SelectedDate) { + $label.Content = ([datetime]$calendar.SelectedDate).ToString("'Selected:' yyyy/MM/dd") + } +}) + +$okButton.Add_Click({ $mainWindow.DialogResult = $true - $mainWindow.Close() - return }) -$CancelButton.add_Click.Invoke({ - Write-Output "Cancel Click Bye" | Out-Host +$cancelButton.Add_Click({ $mainWindow.DialogResult = $false - $mainWindow.Close() - return }) -#表示 -#$mainWindow.showDialog() | out-null -[System.Boolean]$result = $mainWindow.showDialog() -$result.GetType().FullName -Write-Output "Result is $result" | Out-Host - -if ($result -eq $true ) -{ - $x = $Calendar.SelectedDate.ToString() - $x +$result = $mainWindow.ShowDialog() +if (($result -eq $true) -and ($null -ne $calendar.SelectedDate)) { + [datetime]$calendar.SelectedDate } diff --git a/WPF_SimpleWeatherFormSample/WPF_SimpleWeatherFormSample.ps1 b/WPF_SimpleWeatherFormSample/WPF_SimpleWeatherFormSample.ps1 index 7b49188..f585dc0 100644 --- a/WPF_SimpleWeatherFormSample/WPF_SimpleWeatherFormSample.ps1 +++ b/WPF_SimpleWeatherFormSample/WPF_SimpleWeatherFormSample.ps1 @@ -1,43 +1,30 @@ -#参考URL -#https://docs.microsoft.com/en-us/windows/apps/design/layout/grid-tutorial -#Tutorial: Use Grid and StackPanel to create a simple weather app より - -#presentationframeworkを読み込み -Add-Type -AssemblyName presentationframework - -#動的絶対パスを作成 -$scriptPath = $MyInvocation.MyCommand.Path -Write-Output "---------scriptPath ------------" -Write-Output $scriptPath -$scriptPath2 = Split-Path -Parent $scriptPath -Write-Output "---------scriptPath2------------" -Write-Output $scriptPath2 -$ImagePath = Join-Path $scriptPath2 "partially-cloudy.png" -Write-Output "----------ImagePath-------------" -Write-Output $ImagePath -Write-Output "--------------------------------" - -#XAMLファイルを読み込んでxmlに変換 -[System.Xml.XmlDocument]$xaml = Get-Content .\WPF_SimpleWeatherFormSample.xaml - -#visual studio でデザインした場合に付与され、Powershellで読み込む時に不要な要素を削除 -$xaml.window.RemoveAttribute("x:Class") -$xaml.window.RemoveAttribute("mc:Ignorable") - -#読み込んだxamlをXmlNodeReaderにキャスト -[System.Xml.XmlNodeReader]$xamlReader = $xaml -as "System.Xml.XmlNodeReader" -$xamlReader.GetType().FullName -#Windows.Markup.XamlReaderで読み込み -[System.Windows.Window]$mainWindow = [Windows.Markup.XamlReader]::Load($xamlReader) -$mainWindow.gettype().FullName - -#各種コントロールを取得 -[System.Windows.Controls.Image]$Image = $mainWindow.FindName("imageData") -$Image.gettype().FullName -$Image.Source = $ImagePath #絶対パスでないとエラーとなるため、動的取得の絶対パスを再設定 - -#表示 -[System.Boolean]$result = $mainWindow.showDialog() -$result.GetType().FullName -Write-Output "Result is $result" | Out-Host +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# 参考: https://learn.microsoft.com/windows/apps/design/layout/grid-tutorial +Add-Type -AssemblyName PresentationFramework + +$xamlPath = Join-Path $PSScriptRoot 'WPF_SimpleWeatherFormSample.xaml' +$imagePath = Join-Path $PSScriptRoot 'partially-cloudy.png' +[xml]$xaml = Get-Content -LiteralPath $xamlPath -Raw + +$xamlRoot = $xaml.DocumentElement +[void]$xamlRoot.RemoveAttribute('Class', 'http://schemas.microsoft.com/winfx/2006/xaml') +[void]$xamlRoot.RemoveAttribute('Ignorable', 'http://schemas.openxmlformats.org/markup-compatibility/2006') + +$xamlReader = [System.Xml.XmlNodeReader]::new($xaml) +try { + [System.Windows.Window]$mainWindow = [System.Windows.Markup.XamlReader]::Load($xamlReader) +} +finally { + $xamlReader.Close() +} + +[System.Windows.Controls.Image]$image = $mainWindow.FindName('imageData') +$image.Source = $imagePath + +[void]$mainWindow.ShowDialog()