システムトレイアプリケーションをWPFとMVVMで実装する

この記事では、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とコンテキストメニューを作成する。

NotifyIconWrapperは、上のイベントハンドラーOpenItemOnClickExitItemOnClickで発生させるルーティングイベントOpenSelectedExitSelectedをそれぞれ定義している。

以下のXAMLはNotifyIconWrapperの使い方を示している。依存プロパティNotifyRequestは前述のプロパティにバインドされる。各ルーティングイベントは対応するルーティングコマンドに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はWindowStateShowInTaskbarをViewModelのプロパティにバインドしている。

<Window x:Class="SystemTrayApp.WPF.MainWindow"
...
        ShowInTaskbar="{Binding ShowInTaskbar}"
        WindowState="{Binding WindowState}"
        Title="SystemTrayApp" Height="200" Width="300">

ウィンドウが最小化するとバインディングプロパティWindowStateが変化するので、以下のようにsetアクセサーでShowInTaskbarをfalseにしてアプリケーションをタスクバーから消す。ウィンドウを復元するときには、ViewModelでWindowStateWindowState.Normalを設定する。

public WindowState WindowState
{
    get => _windowState;
    set
    {
        ShowInTaskbar = true;
        SetProperty(ref _windowState, value);
        ShowInTaskbar = value != WindowState.Minimized;
    }
}

奇妙なワークアラウンドShowInTaskbar = trueは、最小化時にデスクトップの最下部に以下のようなタイトルだけのウィンドウが残るのを防ぐためである。

A window consisting of only the titlebar leaves at the bottom of the screen.

LoadedとClosingイベント

アプリケーションの起動時にウインドウを隠す処理は、ウィンドウのLoadedイベントをハンドルして行う。以下のリストでは、Xaml.Behavior.WPFでLoadedイベントをルーティングコマンドLoadedCommandにバインドし、そこでWindowStateWindowState.Minimizedを設定してウィンドウを隠している。この方法では、起動時に一瞬ウィンドウが表示されるのは避けられない。

XAML ViewModel

ユーザーがタイトルバーのクローズボタンをクリックしたときには、ウィンドウのClosingイベントをキャンセルして、アプリケーションが終了してしまうのを避けなければならない。これを実現するには、イベント引数のCancelプロパティをtrueに設定すればよい。Xaml.Behavior.WPFはPassEventArgsToCommandがtrueのときにルーティングコマンドにイベント引数を渡すので、ルーティングコマンドでそれを行える。

XAML ViewModel

まとめ

この記事では、このGitHubリポジトリにあるシステムトレイアプリケーションの実装について解説した。この実装はMicrosoft.Toolkit.Mvvmに依存しているが、その他のMVVMフレームワークに移植するのは容易なはずだ。コードのライセンスは0BSDであり、パブリックドメインと同じである。システムトレイアプリケーションを作るときには、このコードを気軽に使ってほしい。