はじめに
前回はPowerShellとWindows Formsでカスタムプログレスバーを作成しました。
今回はその続編として、WPFを使って、よりモダンで柔軟なUIにアップデートしてみます。
WPFに移行するメリット
レイアウトが自由:GridやStackPanelで柔軟に配置可能
デザインが豊か:XAMLで細かくスタイル設定
アニメーション対応:動きのあるUIが作れる
データ連携が強力:ViewModelとのバインディングが簡単
高解像度に強い:自動スケーリングで見た目が崩れにくい
今回のゴール
Forms版と同様の機能を、WPFでどれだけ見栄えよく・扱いやすく作れるかを試します。
「PowerShellでもここまでできる」ことを実感してもらえる内容です。
動作環境
以下の環境で動作確認しました。
Windows 11
Powershell 5.1
基本的なWPFプログレスバー
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
# XAMLの定義
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF カスタムプログレスバー"
Width="500" Height="200"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="StatusLabel"
Text="処理中..."
FontSize="16" FontWeight="Bold"
HorizontalAlignment="Center"/>
<ProgressBar Grid.Row="2" Name="MainProgressBar"
Height="25"
Minimum="0" Maximum="100" Value="0"
Background="LightGray"
Foreground="DodgerBlue"/>
<TextBlock Grid.Row="4" Name="PercentLabel"
Text="0%"
FontSize="12"
HorizontalAlignment="Center"/>
</Grid>
</Window>
"@
# XAMLをロード
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
# コントロールの取得
$statusLabel = $window.FindName("StatusLabel")
$progressBar = $window.FindName("MainProgressBar")
$percentLabel = $window.FindName("PercentLabel")
# ウィンドウを表示
$window.Show()
# プログレス処理のシミュレーション
for ($i = 1; $i -le 100; $i++) {
$progressBar.Value = $i
$percentLabel.Text = "$i%"
$statusLabel.Text = "処理中... ステップ $i/100"
# UIの更新を強制
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [System.Action]{})
Start-Sleep -Milliseconds 50
}
$statusLabel.Text = "完了!"
Start-Sleep -Seconds 2
$window.Close()
高度なカスタマイズ例
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="高度なWPFプログレスバー"
Width="600" Height="350"
WindowStartupLocation="CenterScreen"
Background="White">
<Grid Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- タイトル -->
<TextBlock Grid.Row="0"
Text="ファイル処理進捗"
FontSize="20" FontWeight="Bold"
Foreground="DarkBlue"
HorizontalAlignment="Center"/>
<!-- 現在のファイル名 -->
<Border Grid.Row="2"
Background="AliceBlue"
BorderBrush="SteelBlue"
BorderThickness="1"
CornerRadius="5"
Padding="10">
<TextBlock Name="FileLabel"
Text="準備中..."
FontSize="14"
TextWrapping="Wrap"/>
</Border>
<!-- メインプログレスバー -->
<Grid Grid.Row="4">
<ProgressBar Name="MainProgressBar"
Height="30"
Minimum="0" Maximum="100" Value="0"
Background="LightGray"
BorderBrush="Gray"
BorderThickness="1">
<ProgressBar.Style>
<Style TargetType="ProgressBar">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="15">
<Grid>
<Rectangle Name="PART_Track"
Fill="{TemplateBinding Background}"/>
<Rectangle Name="PART_Indicator"
HorizontalAlignment="Left">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="DodgerBlue" Offset="0"/>
<GradientStop Color="LightSkyBlue" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ProgressBar.Style>
</ProgressBar>
<TextBlock Name="ProgressText"
Text="0%"
FontSize="12" FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"/>
</Grid>
<!-- 詳細情報 -->
<Border Grid.Row="6"
Background="WhiteSmoke"
BorderBrush="Gray"
BorderThickness="1"
CornerRadius="3"
Padding="10">
<TextBlock Name="InfoLabel"
Text=""
FontSize="12"
TextWrapping="Wrap"/>
</Border>
<!-- 時間情報 -->
<StackPanel Grid.Row="8" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="経過時間: " FontSize="10" Foreground="Gray"/>
<TextBlock Name="ElapsedLabel" Text="00:00:00" FontSize="10" FontWeight="Bold" Foreground="Gray"/>
<TextBlock Text=" | 推定残り時間: " FontSize="10" Foreground="Gray" Margin="20,0,0,0"/>
<TextBlock Name="RemainingLabel" Text="--:--:--" FontSize="10" FontWeight="Bold" Foreground="Gray"/>
</StackPanel>
</Grid>
</Window>
"@
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
# コントロールの取得
$fileLabel = $window.FindName("FileLabel")
$progressBar = $window.FindName("MainProgressBar")
$progressText = $window.FindName("ProgressText")
$infoLabel = $window.FindName("InfoLabel")
$elapsedLabel = $window.FindName("ElapsedLabel")
$remainingLabel = $window.FindName("RemainingLabel")
$window.Show()
# 時間計測開始
$startTime = Get-Date
$totalItems = 100
for ($i = 1; $i -le $totalItems; $i++) {
$progress = [math]::Round(($i / $totalItems) * 100, 1)
# プログレスバーとテキストの更新
$progressBar.Value = $progress
$progressText.Text = "$progress%"
# ファイル情報の更新
$fileLabel.Text = "処理中: サンプルファイル_$i.txt"
# 詳細情報の更新
$infoLabel.Text = "進捗: $i/$totalItems アイテム`n完了率: $progress%`n現在の処理: データ変換中..."
# 時間計算
$elapsed = (Get-Date) - $startTime
$elapsedLabel.Text = $elapsed.ToString("hh\:mm\:ss")
if ($i -gt 1) {
$avgTimePerItem = $elapsed.TotalSeconds / $i
$remainingItems = $totalItems - $i
$estimatedRemaining = [TimeSpan]::FromSeconds($avgTimePerItem * $remainingItems)
$remainingLabel.Text = $estimatedRemaining.ToString("hh\:mm\:ss")
}
# UIの更新
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [System.Action]{})
Start-Sleep -Milliseconds 100
}
$fileLabel.Text = "処理完了!"
$infoLabel.Text = "すべてのアイテムの処理が正常に完了しました。"
$progressText.Text = "完了"
Start-Sleep -Seconds 3
$window.Close()
複数プログレスバーのWPF版
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="複数プログレスバー - WPF版"
Width="650" Height="450"
WindowStartupLocation="CenterScreen"
Background="White">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="30"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- タイトル -->
<TextBlock Grid.Row="0"
Text="並列処理進捗モニター"
FontSize="18" FontWeight="Bold"
HorizontalAlignment="Center"
Foreground="DarkSlateGray"/>
<!-- タスク1 -->
<TextBlock Grid.Row="2" Name="Task1Label"
Text="タスク1: データベース処理"
FontSize="12" FontWeight="SemiBold"/>
<ProgressBar Grid.Row="3" Name="ProgressBar1"
Height="20" Minimum="0" Maximum="50" Value="0"
Background="LightGray" Foreground="ForestGreen"/>
<!-- タスク2 -->
<TextBlock Grid.Row="5" Name="Task2Label"
Text="タスク2: ファイル変換"
FontSize="12" FontWeight="SemiBold"/>
<ProgressBar Grid.Row="6" Name="ProgressBar2"
Height="20" Minimum="0" Maximum="75" Value="0"
Background="LightGray" Foreground="RoyalBlue"/>
<!-- タスク3 -->
<TextBlock Grid.Row="8" Name="Task3Label"
Text="タスク3: レポート生成"
FontSize="12" FontWeight="SemiBold"/>
<ProgressBar Grid.Row="9" Name="ProgressBar3"
Height="20" Minimum="0" Maximum="30" Value="0"
Background="LightGray" Foreground="OrangeRed"/>
<!-- 全体進捗 -->
<Border Grid.Row="11"
Background="AliceBlue"
BorderBrush="SteelBlue"
BorderThickness="2"
CornerRadius="5"
Padding="15">
<StackPanel>
<TextBlock Name="TotalLabel"
Text="全体進捗"
FontSize="14" FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
<Grid>
<ProgressBar Name="ProgressBarTotal"
Height="25" Minimum="0" Maximum="155" Value="0"
Background="LightGray" Foreground="Purple"/>
<TextBlock Name="TotalPercentLabel"
Text="0%"
FontSize="12" FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"/>
</Grid>
</StackPanel>
</Border>
</Grid>
</Window>
"@
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
# コントロールの取得
$task1Label = $window.FindName("Task1Label")
$task2Label = $window.FindName("Task2Label")
$task3Label = $window.FindName("Task3Label")
$progressBar1 = $window.FindName("ProgressBar1")
$progressBar2 = $window.FindName("ProgressBar2")
$progressBar3 = $window.FindName("ProgressBar3")
$progressBarTotal = $window.FindName("ProgressBarTotal")
$totalPercentLabel = $window.FindName("TotalPercentLabel")
$window.Show()
# 処理状態の管理
$task1Complete = $false
$task2Complete = $false
$task3Complete = $false
# 並行処理のシミュレーション
while (-not ($task1Complete -and $task2Complete -and $task3Complete)) {
# タスク1の処理
if (-not $task1Complete -and $progressBar1.Value -lt $progressBar1.Maximum) {
$progressBar1.Value++
$progressBarTotal.Value++
if ($progressBar1.Value -eq $progressBar1.Maximum) {
$task1Complete = $true
$task1Label.Text = "タスク1: データベース処理 - ✓ 完了"
$task1Label.Foreground = "ForestGreen"
}
}
# タスク2の処理(タスク1より少し遅い)
if (-not $task2Complete -and $progressBar2.Value -lt $progressBar2.Maximum -and $progressBar1.Value -gt 10) {
$progressBar2.Value++
$progressBarTotal.Value++
if ($progressBar2.Value -eq $progressBar2.Maximum) {
$task2Complete = $true
$task2Label.Text = "タスク2: ファイル変換 - ✓ 完了"
$task2Label.Foreground = "RoyalBlue"
}
}
# タスク3の処理(タスク1, 2がある程度進んでから開始)
if (-not $task3Complete -and $progressBar3.Value -lt $progressBar3.Maximum -and $progressBar1.Value -gt 25 -and $progressBar2.Value -gt 35) {
$progressBar3.Value++
$progressBarTotal.Value++
if ($progressBar3.Value -eq $progressBar3.Maximum) {
$task3Complete = $true
$task3Label.Text = "タスク3: レポート生成 - ✓ 完了"
$task3Label.Foreground = "OrangeRed"
}
}
# 全体進捗率の計算と表示
$totalProgress = [math]::Round(($progressBarTotal.Value / $progressBarTotal.Maximum) * 100, 1)
$totalPercentLabel.Text = "$totalProgress%"
# UIの更新
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [System.Action]{})
Start-Sleep -Milliseconds 150
}
# 完了メッセージ
$totalPercentLabel.Text = "100% - 全完了!"
Start-Sleep -Seconds 3
$window.Close()
実用的なファイル処理関数
function Show-WPFFileProcessProgress {
param(
[string[]]$FilePaths,
[string]$Operation = "処理"
)
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ファイル$Operation進捗"
Width="600" Height="300"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="15"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 現在のファイル -->
<Border Grid.Row="0" Background="LightBlue" BorderBrush="Blue" BorderThickness="1" CornerRadius="3" Padding="10">
<TextBlock Name="FileLabel" Text="準備中..." FontSize="14" TextWrapping="Wrap"/>
</Border>
<!-- プログレスバー -->
<Grid Grid.Row="2">
<ProgressBar Name="ProgressBar" Height="25" Minimum="0" Value="0" Background="LightGray" Foreground="Green"/>
<TextBlock Name="ProgressText" Text="0%" FontSize="12" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White"/>
</Grid>
<!-- 統計情報 -->
<Border Grid.Row="4" Background="WhiteSmoke" BorderBrush="Gray" BorderThickness="1" CornerRadius="3" Padding="10">
<TextBlock Name="StatsLabel" Text="" FontSize="12"/>
</Border>
<!-- ログエリア -->
<Border Grid.Row="6" BorderBrush="Gray" BorderThickness="1" CornerRadius="3">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock Name="LogText" Margin="10" FontFamily="Consolas" FontSize="10" TextWrapping="Wrap"/>
</ScrollViewer>
</Border>
</Grid>
</Window>
"@
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]$xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$fileLabel = $window.FindName("FileLabel")
$progressBar = $window.FindName("ProgressBar")
$progressText = $window.FindName("ProgressText")
$statsLabel = $window.FindName("StatsLabel")
$logText = $window.FindName("LogText")
$progressBar.Maximum = $FilePaths.Count
$window.Show()
$startTime = Get-Date
$processedSize = 0
for ($i = 0; $i -lt $FilePaths.Count; $i++) {
$currentFile = $FilePaths[$i]
$fileName = Split-Path $currentFile -Leaf
# UI更新
$fileLabel.Text = "${Operation}中: $fileName"
$progressBar.Value = $i + 1
$progress = [math]::Round((($i + 1) / $FilePaths.Count) * 100, 1)
$progressText.Text = "$progress%"
# ファイルサイズの取得
try {
$fileSize = (Get-Item $currentFile -ErrorAction Stop).Length
$processedSize += $fileSize
} catch {
$fileSize = 0
}
# 統計情報の更新
$elapsed = (Get-Date) - $startTime
$avgTime = if ($i -gt 0) { $elapsed.TotalSeconds / ($i + 1) } else { 0 }
$remaining = if ($avgTime -gt 0) { [TimeSpan]::FromSeconds($avgTime * ($FilePaths.Count - $i - 1)) } else { [TimeSpan]::Zero }
$statsLabel.Text = @"
進捗: $($i + 1)/$($FilePaths.Count) ファイル | 完了率: $progress%
処理済みサイズ: $([math]::Round($processedSize / 1MB, 2)) MB
経過時間: $($elapsed.ToString("hh\:mm\:ss")) | 推定残り: $($remaining.ToString("hh\:mm\:ss"))
"@
# ログ追加
$timestamp = (Get-Date).ToString("HH:mm:ss")
$logText.Text += "[$timestamp] $Operation完了: $fileName`n"
# UIの更新
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [System.Action]{})
# 実際の処理をここに記述
Start-Sleep -Milliseconds 200 # 処理時間のシミュレーション
}
$fileLabel.Text = "${Operation}完了!"
$logText.Text += "[$(Get-Date -Format "HH:mm:ss")] 全ての${Operation}が完了しました。`n"
Start-Sleep -Seconds 3
$window.Close()
}
# 使用例
# $files = Get-ChildItem "C:\temp\*.txt"
# Show-WPFFileProcessProgress -FilePaths $files.FullName -Operation "バックアップ"
WPF版の主な利点
WPFを使うことで、より洗練されたUIが実現できます。たとえば、グラデーションや影、角丸といった細かなスタイリングが可能で、視覚的にも見栄えの良いデザインが作れます。また、GridやStackPanelを使った柔軟なレイアウトにより、画面サイズに応じたレスポンシブな配置が簡単に行えます。
さらに、スムーズなアニメーション効果や高DPI対応によって、動きのある表現や高解像度環境での見やすさも確保できます。XAMLを用いることで、UIデザインと処理ロジックを分離できる点も、開発効率や保守性の向上につながります。
注意点
WPFを使うことで、よりプロフェッショナルで洗練されたプログレスバーを作成できます。特に複雑な処理や長時間の作業を伴う場面では、ユーザーの操作感や視認性が大きく向上します。
ただし、いくつか注意点もあります。まず、PresentationFramework など必要なアセンブリを適切に読み込む必要があります。UIの更新を行う際には、Dispatcher.Invoke を用いてUIスレッド上での処理を行う必要があり、XAMLの記述も正確であることが求められます。また、大量のUI更新が発生するケースでは、適切なタイミングでの更新制御を行うことで、パフォーマンスの低下を防ぐことが重要です。
参考情報