この記事では、WPFとMVVMとによるシステムトレイアプリケーションの一実装について解説する。ソースコード全体はGitHubリポジトリにある。

この実装は二つの特徴がある。まず、よく使われているWPF NotifyIconを使っていない。ライセンスのCPOLが、どのOSSライセンスとも互換性がないからだ。また、MVVMパターンを採用していてコードビハインドがない。
NotifyIconのラッパー
実装の中心となるのは、WinFormsのNotifyIconクラスのラッパーの NotifyIconWrapperである。このラッパーは、ShowBaloonTipメソッドを呼び出すための依存プロパティNotifyRequestを持っている。
private static readonly DependencyProperty NotifyRequestProperty =
DependencyProperty.Register("NotifyRequest", typeof(NotifyRequestRecord), typeof(NotifyIconWrapper),
new PropertyMetadata(
(d, e) =>
{
var r = (NotifyRequestRecord)e.NewValue;
((NotifyIconWrapper)d)._notifyIcon?.
ShowBalloonTip(r.Duration, r.Title, r.Text, r.Icon);
}));
以下のように、このNotifyRequestをViewModelにバインドしてNotifyRequestRecordを設定すると、上のPropertyMetadataで定義したコールバックがShowBaloonTipをレコードの中身を引数にして呼び出す。
public NotifyIconWrapper.NotifyRequestRecord? NotifyRequest
{
get => _notifyRequest;
set => SetProperty(ref _notifyRequest, value);
}
private void Notify(string message)
{
NotifyRequest = new NotifyIconWrapper.NotifyRequestRecord
{
Title = "Notify",
Text = message,
Duration = 1000
};
}
以下は、NotifyIconWrapperのコンストラクタである。コンストラクタを呼び出したのがXAMLエディタではない場合に、イベントハンドラを設定済みのNotifyIconとコンテキストメニューを作成する。
上のイベントハンドラーOpenItemOnClickとExitItemOnClickは、NotifyIconWrapperの定義するルーティングイベントOpenSelectedとExitSelectedを発生させる。
以下のXAMLはNotifyIconWrapperの使い方を示している。依存プロパティNotifyRequestをVewModelのプロパティにバインドし、各ルーティングイベントをXaml.Behaviros.WPFによってルーティングコマンドにバインドしている。
<Window x:Class="SystemTrayApp.WPF.MainWindow"
xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
...
<Grid>
<local:NotifyIconWrapper NotifyRequest="{Binding NotifyRequest}">
<bh:Interaction.Triggers>
<bh:EventTrigger EventName="OpenSelected">
<bh:InvokeCommandAction Command="{Binding NotifyIconOpenCommand}">
</bh:EventTrigger>
ウィンドウの非表示と復元
ウィンドウの表示と復元はデータバインディングを通じて行う。以下のXAMLはWindowStateとShowInTaskbarをViewModelのプロパティにバインドしている。
<Window x:Class="SystemTrayApp.WPF.MainWindow"
...
ShowInTaskbar="{Binding ShowInTaskbar}"
WindowState="{Binding WindowState}"
Title="SystemTrayApp" Height="200" Width="300">
ウィンドウが最小化するとバインディングプロパティWindowStateが変化するので、以下のようにsetアクセサーでShowInTaskbarをfalseにしてアプリケーションをタスクバーから消す。ウィンドウを復元するときには、ViewModelでWindowStateにWindowState.Normalを設定する。
public WindowState WindowState
{
get => _windowState;
set
{
ShowInTaskbar = true;
SetProperty(ref _windowState, value);
ShowInTaskbar = value != WindowState.Minimized;
}
}
奇妙なワークアラウンドShowInTaskbar = trueは、最小化時にデスクトップの最下部に以下のようなタイトルだけのウィンドウが残るのを防ぐためである。

LoadedとClosingイベント
アプリケーションの起動時にウインドウを隠す処理は、ウィンドウのLoadedイベントをハンドルして行う。以下のリストでは、Xaml.Behavior.WPFでLoadedイベントをルーティングコマンドLoadedCommandにバインドし、そこでWindowStateにWindowState.Minimizedを設定してウィンドウを隠している。この方法では、起動時に一瞬ウィンドウが表示されるのは避けられない。
ユーザーがタイトルバーのクローズボタンをクリックしたときには、ウィンドウのClosingイベントをキャンセルして、アプリケーションが終了してしまうのを避けなければならない。これを実現するには、イベント引数のCancelプロパティをtrueに設定すればよい。Xaml.Behavior.WPFはPassEventArgsToCommandがtrueのときにルーティングコマンドにイベント引数を渡すので、ルーティングコマンドでそれを行える。
まとめ
この記事では、このGitHubリポジトリにあるシステムトレイアプリケーションの実装について解説した。この実装はMicrosoft.Toolkit.Mvvmに依存しているが、その他のMVVMフレームワークに移植するのは容易なはずだ。コードのライセンスは0BSDであり、パブリックドメインと同じである。システムトレイアプリケーションを作るときには、このコードを気軽に使ってほしい。
