Blog

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

UnityアプリケーションをNamedPipe経由で制御する

以下のようなWindowsアプリケーションから、UnityアプリケーションをNamedPipeを介して制御する方法を紹介する。なおソースコード全体はGitHubのリポジトリに置いてある。

"UnityアプリケーションをNamedPipe経由で制御する" の続きを読む

C#でロックフリーなメモリープールを自作する

基本的にシングルスレッドでしか使わないライブラリを、速度を落とさずにスレッドセーフにするために、ロックフリーなメモリープールを自作した話を紹介する。この記事はC# その2 Advent Calendar 2020の18日目の記事である。

"C#でロックフリーなメモリープールを自作する" の続きを読む

[Unity] Physics.OverlapCapsuleの使い方

Capsule and Cube Collider

このようにCapsule Colliderに重なってる2つのColliderを取得したいときは、Physics.OverlapCapsuleを使うとよい。このメソッドにCapsule Colliderの位置と大きさを与えると、重なっているColliderがすべて返る。RigidbodyをKinematic TriggerにしてOnTriggerEnterをハンドルして取得することもできるが、こちらのほうが手軽である。

"[Unity] Physics.OverlapCapsuleの使い方" の続きを読む

厳格なJSONパーザーの作り方

僕の作ったJSONパーザーのDynaJsonはとても厳格にできている。RFC 8259に準拠しているものはすべて受理するし、そうでないものは二つの例外を除きすべて受理しない。

受理する例外は、ケツカンマと02-02のような0で始まる数字である。前者を受理するのは実用性のためであり、後者を受理するのはDynamicJsonとの互換性のためである。

JSONの文法はとても単純だが、不用意にパーザーを実装するとRFCに準拠したJSONを受理しなかったり、非準拠のものをエラーにできなかったりする。パーザーを実装する際の地雷ポイントはParsing JSON is a Minefieldによくまとまっている。

"厳格なJSONパーザーの作り方" の続きを読む

Docker for WindowsでWindowsの任意のフォルダをコンテナにマウントする

普通にdockerを使っている場合は、docker run -v /source:/destinationとするとコンテナホストの/sourceをコンテナの/destinationにマウントして、コンテナからアクセスできる。

では、Docker for WindowsでC:\Sourceをコンテナの/destinationにマウントしたいときはどうするか。VMホストのC:\Sourceをコンテナの中に直接マウントすることは当然できない。

まず最初に、 docker-machineでVMを作成するときにC:\Sourceを共有フォルダとして設定する。共有フォルダはVirtualBox deriverの--virtualbox-share-folderオプションで以下のように指定できる。

docker-machine create --driver virtualbox --virtualbox-share-folder=C:\Source:Source

"Docker for WindowsでWindowsの任意のフォルダをコンテナにマウントする" の続きを読む