WPF/Windowsフォーム:タスクバーのアイコンに進捗表示を出すには?[C#、VB]:.NET TIPS
WindowsフォームとWPFアプリではタスクバーのアイコンに処理の進捗状況を表示することがよくある。本稿ではこの機能を実装する方法を解説する。
対象:.NET 4.0以降、Windows 7以降
デスクトップ用のプログラムで長い処理を行うとき、Windows 7以降ではタスクバーのアイコンに進捗(しんちょく)状態を表示できる。これを実装するのは難しそうだと思っていないだろうか? 長い処理の途中で画面上に進捗を表示できているのなら、タスクバーのアイコンに表示するのは意外と簡単なのである。本稿では、.NET 4.0以降のWindowsフォームとWPFでタスクバーのアイコンに進捗表示を出す方法を解説する。
進捗表示の種類
タスクバーのアイコンに表示される進捗表示は、正式には「プログレスインジケーター」と呼ばれる。「進行状況バー」と呼ぶこともあるようだ。プログレスインジケーターには5種類の状態がある(次の画像)。その種類を表す列挙体のメンバー名と意味は、次のようになっている。
- None: 進捗表示なし
- Normal: 緑色(正常に処理中)
- Paused: 黄色(中断状態、再開待ち)
- Error: 赤色(エラーで中断、再開不能)
- Indeterminate: 緑色(正常に処理中、ただし進捗割合は不明)
利用するAPI
Windowsフォームには標準の機能としては備わっていない。「Windows API Code Pack」を利用する*1。
WPFでは、.NET Framework 4.0で追加されたTaskbarItemInfoクラス(System.Windows.Shell名前空間)を利用する。
*1 Windows API Code Packは、以前はcode.msdn.microsoft.comやarchive.msdn.microsoft.comでソースコードと共に公開されていた。現在は、マイクロソフトからの公開は終了している。しかし、有志の手によってソースコードは引き続きメンテされており、そのバイナリはNuGetから入手できる。なお、当初のWindows API Code Packは.NET Framework 3.0から利用できたが、現在NuGetで公開されているバイナリを利用するには.NET Framework 4.0以上が必要なようである。
Windowsフォームで進捗表示を出すには?
まず、進捗表示を出したいWindowsフォームのプロジェクトごとに、Windows API Code PackをNuGetから導入しておく(次の画像)。
NuGetからWindows API Code Packをインストールする(Visual Studio 2012)
ソリューションエクスプローラーでプロジェクトを選択し、その右クリックメニューから[NuGet パッケージの管理]を選ぶと、この画像のような[NuGet パッケージの管理]ダイアログが出てくる。
[NuGet パッケージの管理]ダイアログで、左側の[オンライン]を選び((1))、右上の検索ボックスに「Windows 7 Api Code Pack w/ xml documentation」と入力して検索する((2))。見つかったパッケージを選ぶと[インストール]ボタンが表示されるので((3))、それをクリックしてインストールする。プログレスインジケーターを表示するには、「Windows 7 Api Code Pack w/ xml documentation - Core」と「Windows 7 Api Code Pack w/ xml documentation - Shell」の二つのパッケージを順に入れる必要がある。
なお、右側の[ライセンス条項の表示]リンク((4))でライセンス条件を確認できるので、利用に先立って確認しておいてほしい。
上の画像のようにしてWindows API Code Packをインストールすると、プロジェクトの参照設定に次の三つが追加されているはずだ。確認しておいてほしい(C#のプロジェクトではソリューションエクスプローラーの[参照設定]フォルダー、VBのプロジェクトではプロジェクトのプロパティの[参照]タブ)。
- Microsoft.WindowsAPICodePack
- Microsoft.WindowsAPICodePack.Shell
- Microsoft.WindowsAPICodePack.ShellExtensions
準備が整ったら、メインのフォーム(=プログラムの起動時に表示されるフォーム)のコードビハインドの冒頭部分に名前空間の指定を追加する(次のコード)。
using Microsoft.WindowsAPICodePack.Taskbar;
Imports Microsoft.WindowsAPICodePack.Taskbar
そうしたら、メインのフォームが完全に表示された後の好きなタイミングで*2、次に示すようなコードを記述するだけである。
// プログレスインジケーターを緑色にする
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal);
// プログレスインジケーターの進捗割合を30%にする
TaskbarManager.Instance.SetProgressValue(30, 100);
' プログレスインジケーターを緑色にする
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal)
' プログレスインジケーターの進捗割合を30%にする
TaskbarManager.Instance.SetProgressValue(30, 100)
SetProgressStateメソッドとSetProgressValueメソッドを呼び出すだけである。
SetProgressStateメソッドには、前述したプログレスインジケーターの5種類の状態のいずれかを指定する。
SetProgressValueメソッドは、進捗率の更新があるときだけ呼び出せばよい。この例の引数は、100分の30(=30%)を表している。
// プログレスインジケーターを元に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
' プログレスインジケーターを元に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress)
プログレスインジケーターを元に戻すには、SetProgressStateメソッドを呼び出すだけである。
*2 それ以外のフォーム(=メインのフォームから表示した別のフォーム)からプログレスインジケーターを表示することも可能だが、そのときはウィンドウハンドルを引数として渡す必要がある。また、(どのフォームにおいても)フォームが完全に表示される前に呼び出すと、例外が発生する。例えば、フォームのLoadイベントでは例外となり、Shownイベントならば正しく表示される。
ただし、上のコードは、UIスレッドで呼び出さねばならない(そうしないと例外が発生する)。時間のかかる処理は別スレッドで非同期に動作させるのが普通であるから、UIの表示を更新する部分をUIスレッドに戻して実行するようなコーディングが必要だ。
簡単な例を紹介しておこう。次の画像のようなUIを作る。カウントを表示しているラベルの名前は「label1」、[START]ボタンの名前は「button1」とする。
非同期処理からプログレスインジケーターを更新するフォームの例
Windows 7で実行しているところである。非同期処理で10から0までカウントダウンしていく。カウントダウンに合わせて、フォーム上のラベルとプログレスインジケーターの表示を更新する。
「button1」ボタンのクリックイベントで、10から0までカウントダウンする処理を非同期に実行させよう。そして、カウントが進むたびに、「label1」ラベルの表示とプログレスインジケーターを更新しよう。.NET 4.0のTaskクラス(System.Threading.Tasks名前空間)を使って書くと、次のコードのようになる。
private void button1_Click(object sender, EventArgs e)
{
// ボタンをディスエーブルに
this.button1.Enabled = false;
// プログレスインジケーターを緑色にする
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal);
// 進捗を表示するためのデリゲートを定義する(カウントダウン中に呼び出される)
Action<Object> callUpdateProgress = (o) =>
{
int number = (int)o;
// ラベルの表示を更新する
this.label1.Text = number.ToString();
// プログレスインジケーターに値をセットする
TaskbarManager.Instance.SetProgressValue(10 - number, 10);
};
// 別スレッドで実行するタスクを定義し、非同期実行を開始する
Task.Factory.StartNew(() =>
{
// 別スレッドで行う処理の本体
for (int i = 10; i >= 0; i--)
{
// UIスレッドで進捗表示を呼び出す
this.BeginInvoke(callUpdateProgress, i);
// 1秒間待機
System.Threading.Thread.Sleep(1000);
}
})
.ContinueWith((t) =>
// 非同期実行が完了した後の処理
{
// プログレスインジケーターを通常表示に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
// ボタンをイネーブルに戻す
this.button1.Enabled = true;
},
// 完了時の処理をUIスレッドで実行するための指定
TaskScheduler.FromCurrentSynchronizationContext()
);
}
Private Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
' ボタンをディスエーブルに
Me.button1.Enabled = False
' プログレスインジケーターを緑色にする
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Normal)
' 進捗を表示するためのデリゲートを定義する(カウントダウン中に呼び出される)
Dim callUpdateProgress As Action(Of Object) _
= Sub(o)
Dim number As Integer = CInt(o)
' ラベルの表示を更新する
Me.label1.Text = number.ToString()
' プログレスインジケーターに値をセットする
TaskbarManager.Instance.SetProgressValue(10 - number, 10)
End Sub
' 別スレッドで実行するタスクを定義し、非同期実行を開始する
Task.Factory.StartNew(
Sub()
' 別スレッドで行う処理の本体
For i As Integer = 10 To 0 Step -1
' UIスレッドで進捗表示を呼び出す
Me.BeginInvoke(callUpdateProgress, i)
' 1秒間待機
System.Threading.Thread.Sleep(1000)
Next
End Sub) _
.ContinueWith(
Sub(t)
' 非同期実行が完了した後の処理
' プログレスインジケーターを通常表示に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress)
' ボタンをイネーブルに戻す
Me.button1.Enabled = True
End Sub,
TaskScheduler.FromCurrentSynchronizationContext()
)
' TaskScheduler.〜 は、完了時の処理をUIスレッドで実行するための指定
End Sub
これはフォームのコードビハインドに記述している。そのため、UIスレッドで処理を呼び出すには「this.BeginInvoke」メソッドが使える。フォームと切り離して非同期処理を書く場合には、非同期処理側でイベントを発生させたり、.NET 4.5で導入されたIProgressインターフェース(System名前空間)を利用したりなどと、さまざまな工夫をすることになる。
WPFで進捗表示を出すには?
Windowsフォームと同様にWindows API Code Packを使ってもよいが、.NET 4.0のWPFからは標準でサポートされている。本稿では、WPFの機能を使う。
WPF(.NET 4.0以降)のWindowオブジェクト(System.Windows名前空間)にはTaskbarItemInfoプロパティがあって、これを通じてプログレスインジケーターを操作できる。ただし、このプロパティはデフォルトではnullになっている(利用しないときに無用な負荷を与えないためであろう)。
そこで、プログレスインジケーターを利用するには、メインのウィンドウが作られたときに、TaskbarItemInfoオブジェクト(System.Windows.Shell名前空間)を生成してウィンドウに設定してやる(次のコード)。
public MainWindow()
{
InitializeComponent();
// WindowのTaskbarItemInfoプロパティにTaskbarItemInfoインスタンスを設定する
this.TaskbarItemInfo = new System.Windows.Shell.TaskbarItemInfo();
// これはXAMLで次のように書いてもよい
// <Window.TaskbarItemInfo>
// <TaskbarItemInfo />
// </Window.TaskbarItemInfo>
}
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' InitializeComponent() 呼び出しの後で初期化を追加します。
' WindowのTaskbarItemInfoプロパティにTaskbarItemInfoインスタンスを設定する
Me.TaskbarItemInfo = New System.Windows.Shell.TaskbarItemInfo()
' これはXAMLで次のように書いてもよい
' <Window.TaskbarItemInfo>
' <TaskbarItemInfo />
' </Window.TaskbarItemInfo>
End Sub
太字の部分を追加する。
なお、コメントに書いたように、XAMLコードで記述することもできる。
そうしたら、TaskbarItemInfoプロパティにオブジェクトをセットした後の好きなタイミングで、次に示すようなコードを記述するだけである。
// プログレスインジケーターを緑色にする
this.TaskbarItemInfo.ProgressState
= System.Windows.Shell.TaskbarItemProgressState.Normal;
// プログレスインジケーターの進捗割合を30%にする
this.TaskbarItemInfo.ProgressValue = 0.3;
' プログレスインジケーターを緑色にする
Me.TaskbarItemInfo.ProgressState _
= System.Windows.Shell.TaskbarItemProgressState.Normal
' プログレスインジケーターの進捗割合を30%にする
Me.TaskbarItemInfo.ProgressValue = 0.3
ProgressStateプロパティとProgressValueプロパティをセットするだけである。
ProgressStateプロパティには、前述したプログレスインジケーターの5種類の状態のいずれかを指定する。
ProgressValueプロパティは、進捗率の更新があるときだけセットすればよい。
// プログレスインジケーターを元に戻す
this.TaskbarItemInfo.ProgressState
= System.Windows.Shell.TaskbarItemProgressState.None;
' プログレスインジケーターを元に戻す
Me.TaskbarItemInfo.ProgressState _
= System.Windows.Shell.TaskbarItemProgressState.None
ProgressStateプロパティにセットするだけである。
ただし、上のコードは、UIスレッドで呼び出さねばならない(そうしないと例外が発生する)。時間のかかる処理は別スレッドで非同期に動作させるのが普通であるから、UIの表示を更新する部分をUIスレッドに戻して実行するようなコーディングが必要だ。
簡単な例を紹介しておこう。次の画像のようなUIを作る。カウントを表示しているテキストブロックの名前は「textBlock1」、[START]ボタンの名前は「button1」とする。
非同期処理からプログレスインジケーターを更新するウィンドウの例
Windows 10で実行しているところである。非同期処理で10から0までカウントダウンしていく。カウントダウンに合わせて、ウィンドウ上のテキストブロックとプログレスインジケーターの表示を更新する。
「button1」ボタンのクリックイベントで、10から0までカウントダウンする処理を非同期に実行させよう。そして、カウントが進むたびに、「textBlock1」テキストブロックの表示とプログレスインジケーターを更新しよう。.NET 4.5のTaskクラス(System.Threading.Tasks名前空間)とVisual Studio 2012で導入されたasync/awaitキーワードを使って書くと、次のコードのようになる。
private async void Button_Click(object sender, RoutedEventArgs e)
{
// ボタンをディスエーブルに
this.button1.IsEnabled = false;
// プログレスインジケーターを緑色にする
this.TaskbarItemInfo.ProgressState
= System.Windows.Shell.TaskbarItemProgressState.Normal;
// 進捗を表示するためのデリゲートを定義する(カウントダウン中に呼び出される)
Action<Object> callUpdateProgress = (o) =>
{
int number = (int)o;
// テキストブロックの表示を更新する
this.textBlock1.Text = number.ToString();
// プログレスインジケーターに値をセットする
this.TaskbarItemInfo.ProgressValue = (10 - number) / 10.0;
};
// 別スレッドで実行するタスクを定義し、非同期実行を開始する(実行開始と同時に制御が戻る)
await Task.Run(() =>
{
// 別スレッドで行う処理の本体
for (int i = 10; i >= 0; i--)
{
// UIスレッドで進捗表示を呼び出す
this.Dispatcher.BeginInvoke(callUpdateProgress, i);
// 1秒間待機
System.Threading.Thread.Sleep(1000);
}
});
// 非同期実行が完了した後の処理(上のタスクが終了した後にUIスレッドで呼び出される)
// プログレスインジケーターを通常表示に戻す
this.TaskbarItemInfo.ProgressState
= System.Windows.Shell.TaskbarItemProgressState.None;
// ボタンをイネーブルに戻す
this.button1.IsEnabled = true;
}
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
' ボタンをディスエーブルに
Me.button1.IsEnabled = False
' プログレスインジケーターを緑色にする
Me.TaskbarItemInfo.ProgressState _
= System.Windows.Shell.TaskbarItemProgressState.Normal
' 進捗を表示するためのデリゲートを定義する(カウントダウン中に呼び出される)
Dim callUpdateProgress As Action(Of Object) _
= Sub(o)
Dim number As Integer = CInt(o)
' テキストブロックの表示を更新する
Me.textBlock1.Text = number.ToString()
' プログレスインジケーターに値をセットする
Me.TaskbarItemInfo.ProgressValue = (10 - number) / 10.0
End Sub
' 別スレッドで実行するタスクを定義し、非同期実行を開始する(実行開始と同時に制御が戻る)
Await Task.Run(
Sub()
' 別スレッドで行う処理の本体
For i As Integer = 10 To 0 Step -1
' UIスレッドで進捗表示を呼び出す
Me.Dispatcher.BeginInvoke(callUpdateProgress, i)
' 1秒間待機
System.Threading.Thread.Sleep(1000)
Next
End Sub)
' 非同期実行が完了した後の処理(上のタスクが終了した後にUIスレッドで呼び出される)
' プログレスインジケーターを通常表示に戻す
Me.TaskbarItemInfo.ProgressState _
= System.Windows.Shell.TaskbarItemProgressState.None
' ボタンをイネーブルに戻す
Me.button1.IsEnabled = True
End Sub
これはウィンドウのコードビハインドに記述している。そのため、UIスレッドで処理を呼び出すには「this.Dispatcher.BeginInvoke」メソッドが使える。ウィンドウと切り離して非同期処理を書く場合には、非同期処理側でイベントを発生させたり、.NET 4.5で導入されたIProgressインターフェース(System名前空間)を利用したりなどと、さまざまな工夫をすることになる。
まとめ
プログレスインジケーターを表示すること自体は簡単である。実際には、非同期処理の途中で行うことになるので、そこがちょっと難しい*3。とはいえ、非同期処理の途中で画面を更新するコードが書けるならば、プログレスインジケーターの表示を更新するコードも同様である。
*3 非同期処理、特にTaskクラスを使う方法については、次の記事などで学んでいただきたい。筆者も「C#によるマルチコアのための非同期/並列処理プログラミング」という書籍を出しているので、参考にしていただければ幸いである。
- 特集:.NET開発者のための非同期入門:フリーズしないアプリケーションの作り方
- 連載:次世代技術につながるSilverlight入門:避けて通れない「非同期処理」を克服しよう
- 連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
利用可能バージョン:.NET Framework 4.0以降
カテゴリ:オープンソース・ライブラリ 処理対象:タスクバー
カテゴリ:Windowsフォーム 処理対象:ウィンドウ
カテゴリ:WPF/XAML 処理対象:タスクバー
使用ライブラリ:TaskbarItemInfoクラス(System.Windows.Shell名前空間)
関連TIPS:タスクバー上のボタンやウィンドウのタイトルバーを点滅させるには?[C#、VB]
関連TIPS:タスクバーにアイコンを表示させないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.