この記事では、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であり、パブリックドメインと同じである。システムトレイアプリケーションを作るときには、このコードを気軽に使ってほしい。