4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[備忘録] PowerShell WPFでモダンなプログレスバーを作成してみた

Last updated at Posted at 2025-06-23

はじめに

image.png

前回はPowerShellとWindows Formsでカスタムプログレスバーを作成しました。
今回はその続編として、WPFを使って、よりモダンで柔軟なUIにアップデートしてみます。

WPFに移行するメリット

レイアウトが自由:GridやStackPanelで柔軟に配置可能

デザインが豊か:XAMLで細かくスタイル設定

アニメーション対応:動きのあるUIが作れる

データ連携が強力:ViewModelとのバインディングが簡単

高解像度に強い:自動スケーリングで見た目が崩れにくい

今回のゴール

Forms版と同様の機能を、WPFでどれだけ見栄えよく・扱いやすく作れるかを試します。
「PowerShellでもここまでできる」ことを実感してもらえる内容です。

動作環境

以下の環境で動作確認しました。
Windows 11
Powershell 5.1

基本的なWPFプログレスバー

image.png

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()

画面例
image.png

高度なカスタマイズ例

image.png

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()

画面例
image.png

複数プログレスバーのWPF版

image.png

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()

画面例
image.png

実用的なファイル処理関数

image.png

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 "バックアップ"

画面例
image.png

WPF版の主な利点

image.png

WPFを使うことで、より洗練されたUIが実現できます。たとえば、グラデーションや影、角丸といった細かなスタイリングが可能で、視覚的にも見栄えの良いデザインが作れます。また、GridやStackPanelを使った柔軟なレイアウトにより、画面サイズに応じたレスポンシブな配置が簡単に行えます。

さらに、スムーズなアニメーション効果や高DPI対応によって、動きのある表現や高解像度環境での見やすさも確保できます。XAMLを用いることで、UIデザインと処理ロジックを分離できる点も、開発効率や保守性の向上につながります。

注意点

WPFを使うことで、よりプロフェッショナルで洗練されたプログレスバーを作成できます。特に複雑な処理や長時間の作業を伴う場面では、ユーザーの操作感や視認性が大きく向上します。

ただし、いくつか注意点もあります。まず、PresentationFramework など必要なアセンブリを適切に読み込む必要があります。UIの更新を行う際には、Dispatcher.Invoke を用いてUIスレッド上での処理を行う必要があり、XAMLの記述も正確であることが求められます。また、大量のUI更新が発生するケースでは、適切なタイミングでの更新制御を行うことで、パフォーマンスの低下を防ぐことが重要です。

参考情報

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?