WindowsフォームとWPFアプリではタスクバーのアイコンに処理の進捗状況を表示することがよくある。本稿ではこの機能を実装する方法を解説する。
対象:.NET 4.0以降、Windows 7以降
デスクトップ用のプログラムで長い処理を行うとき、Windows 7以降ではタスクバーのアイコンに進捗(しんちょく)状態を表示できる。これを実装するのは難しそうだと思っていないだろうか? 長い処理の途中で画面上に進捗を表示できているのなら、タスクバーのアイコンに表示するのは意外と簡単なのである。本稿では、.NET 4.0以降のWindowsフォームとWPFでタスクバーのアイコンに進捗表示を出す方法を解説する。
タスクバーのアイコンに表示される進捗表示は、正式には「プログレスインジケーター」と呼ばれる。「進行状況バー」と呼ぶこともあるようだ。プログレスインジケーターには5種類の状態がある(次の画像)。その種類を表す列挙体のメンバー名と意味は、次のようになっている。
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 API Code PackをNuGetから導入しておく(次の画像)。
上の画像のようにしてWindows API Code Packをインストールすると、プロジェクトの参照設定に次の三つが追加されているはずだ。確認しておいてほしい(C#のプロジェクトではソリューションエクスプローラーの[参照設定]フォルダー、VBのプロジェクトではプロジェクトのプロパティの[参照]タブ)。
準備が整ったら、メインのフォーム(=プログラムの起動時に表示されるフォーム)のコードビハインドの冒頭部分に名前空間の指定を追加する(次のコード)。
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)
// プログレスインジケーターを元に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
' プログレスインジケーターを元に戻す
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress)
*2 それ以外のフォーム(=メインのフォームから表示した別のフォーム)からプログレスインジケーターを表示することも可能だが、そのときはウィンドウハンドルを引数として渡す必要がある。また、(どのフォームにおいても)フォームが完全に表示される前に呼び出すと、例外が発生する。例えば、フォームのLoadイベントでは例外となり、Shownイベントならば正しく表示される。
ただし、上のコードは、UIスレッドで呼び出さねばならない(そうしないと例外が発生する)。時間のかかる処理は別スレッドで非同期に動作させるのが普通であるから、UIの表示を更新する部分をUIスレッドに戻して実行するようなコーディングが必要だ。
簡単な例を紹介しておこう。次の画像のようなUIを作る。カウントを表示しているラベルの名前は「label1」、[START]ボタンの名前は「button1」とする。
「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
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
そうしたら、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
// プログレスインジケーターを元に戻す
this.TaskbarItemInfo.ProgressState
= System.Windows.Shell.TaskbarItemProgressState.None;
' プログレスインジケーターを元に戻す
Me.TaskbarItemInfo.ProgressState _
= System.Windows.Shell.TaskbarItemProgressState.None
ただし、上のコードは、UIスレッドで呼び出さねばならない(そうしないと例外が発生する)。時間のかかる処理は別スレッドで非同期に動作させるのが普通であるから、UIの表示を更新する部分をUIスレッドに戻して実行するようなコーディングが必要だ。
簡単な例を紹介しておこう。次の画像のようなUIを作る。カウントを表示しているテキストブロックの名前は「textBlock1」、[START]ボタンの名前は「button1」とする。
「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
プログレスインジケーターを表示すること自体は簡単である。実際には、非同期処理の途中で行うことになるので、そこがちょっと難しい*3。とはいえ、非同期処理の途中で画面を更新するコードが書けるならば、プログレスインジケーターの表示を更新するコードも同様である。
*3 非同期処理、特にTaskクラスを使う方法については、次の記事などで学んでいただきたい。筆者も「C#によるマルチコアのための非同期/並列処理プログラミング」という書籍を出しているので、参考にしていただければ幸いである。
利用可能バージョン:.NET Framework 4.0以降
カテゴリ:オープンソース・ライブラリ 処理対象:タスクバー
カテゴリ:Windowsフォーム 処理対象:ウィンドウ
カテゴリ:WPF/XAML 処理対象:タスクバー
使用ライブラリ:TaskbarItemInfoクラス(System.Windows.Shell名前空間)
関連TIPS:タスクバー上のボタンやウィンドウのタイトルバーを点滅させるには?[C#、VB]
関連TIPS:タスクバーにアイコンを表示させないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.