dullwhaleのメモ帳

何度も同じことを調べなくてよいように...

Windows App SDK(WinUI3)でNotifyIconを持つプログラムを作成する

この記事は、Windows App SDK(WinUI3)でNotifyIconからウィンドウを開く方法について説明する。 NotifyIconの実装にはライブラリH.NotifyIconを用いる。

NotifyIconとその使われ方

Windowsにおいて、普段はデーモンのように動くが時々GUIで対話するアプリケーションを作りたい。 そのようなアプリケーションのGUIを起動する手段として、NotifyIconが使われることがある。 NotifyIconはsystem tray iconやtaskbar iconとも言われ、デフォルトでタスクバーの右に配置される。 次の図にNotifyIconの例を示す。 赤い枠で囲まれた部分に各アプリケーションや機能のNotifyIconが格納されている。

図: Windows 11におけるsystem tray中のNotifyIconの例

アイコンの右クリックメニューでいくつかの簡単な(GUIの用語としての)コマンドを実行でき、複雑な操作に備えてウィンドウを開けるようになっているものが多い。

対象

事前作業 プロジェクトを作成しUnpackagedにする

この節では事前作業としてVisual Studio 2022でソリューション・プロジェクトを作成し、アプリケーションのベース部分を作成する。 配布は行わないから、Unpackagedアプリとして作成する。

詳細は公式ドキュメントに記載されているから、ここでは要点だけ記述する。

1 . プロジェクト作成時のテンプレートでは「C#」、「Windows」、「WinUI」でフィルタし、「空のアプリ、パッケージ (WinUI 3 in Desktop)」を選択する。

2 . *.csprojファイルを開き、<PropertyGroup>の下の階層に以下の記述を追加する。

<WindowsPackageType>None</WindowsPackageType>

3 . Visual Studioからの実行の構成がデフォルトで「${プロジェクト名} (Packaged)」になっているから、「${プロジェクト名} (Unpackaged)」に変更したうえで実行してみる。

4 . ビルド、実行が行われ、起動したウィンドウ中央の「Click Me」ボタンを押して「Clicked」に変化すれば、この節手順は完了。

続く節ではサードパーティーライブラリH.NotifyIconの導入理由と手順を示す。

なぜサードパーティーのライブラリを使うのか

この記事を書いた時点においては、NotifyIconの機能はWindows App SDKで提供されていないようだ。 Win32APIを直接コールするか、Windows FormsのAPIを呼び出す必要がある。 どちらの方法を取っても、記述量が増えバグのリスクも高まる。 特にWin32APIはより低レベルのインターフェイスを提供する分、危険である。

対して、H.NotifyIconはごく少ないコード量でNotifyIconを利用でき、危険なコードになりにくい。 同様の機能を提供し、かつ開発が継続しているライブラリが他に見当たらないからこれを使う。

H.NotifyIconの導入

Windows App SDKと一緒に使うことを想定したNuGetパッケージH.NotifyIcon.WinUIをインストールする。 H.NotifyIconではないことに注意せよ。 さらに、他のGUIフレームワーク向けのよく似たパッケージが複数用意されているから混同に注意せよ。 インストールはメニューの「プロジェクト(P)」>「NuGet パッケージの管理(N)」から行うのが簡単だろう。

NotifyIconをクリックしてウィンドウを開けるようにする

この節では、常駐アプリとして稼働し、必要なときだけNotifyIconをクリックすることでウィンドウを開くアプリケーションを作成する。 記述するコードは少ないものの、コード量に対して考えるべきことが多いから、以降いくつかの節に分けて説明する。

ウィンドウが全て閉じられてもアプリケーションが動き続けるようにする

よくある常駐アプリのように、バックグラウンドで稼働し続け、必要なときだけウィンドウを表示できるよう実装する。 これは実装の視点から見ると、全てのウィンドウが閉じられてもアプリケーションが起動し続ける必要がある。 これを実現するためにClosedイベントをキャッチしてデフォルトのハンドラからイベントを隠ぺいする。 詳しくは次の記事を参照せよ。

Windows App SDK(WinUI3)でウィンドウを閉じられてもアプリケーションを動かし続ける - dullwhaleのメモ帳

ここでは、Closedイベントの隠ぺいに加えてウィンドウを非表示にする処理を加える。 デフォルトで作成されるMainWindowのコンストラクタ内でイベントハンドラを追加する。 MainWindow.xaml.cs

public MainWindow()
{
    // 中略
    // ウィンドウが閉じられたイベントに対するイベントハンドラを追加
    Closed += (sender, args) =>
    {
        // イベントを処理済みとしてマークして、デフォルトのハンドラへの伝播を阻止する。
        // デフォルトのハンドラはアクティブなウィンドウが1つもないとアプリケーションを終了させる。
        args.Handled = true;
        // ウィンドウを閉じる代わりに隠す。
        this.Hide();
    };
}

NotifyIconをXAMLで作成する

H.NotifyIconのREADMEにあるサンプルを流用して、最低限のNotifyIconを実装する。 左クリック時のイベントハンドラを指定している部分だけが元のサンプルとの違いである。

LeftClickCommand="{x:Bind IconClicked}"

MainWindow.xaml

<!-- 中略 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    <tb:TaskbarIcon LeftClickCommand="{x:Bind IconClicked}">
        <tb:TaskbarIcon.IconSource>
            <tb:GeneratedIconSource
        Text="❤️"
        Foreground="Red"
        />
        </tb:TaskbarIcon.IconSource>
    </tb:TaskbarIcon>
</StackPanel>
<!-- 中略 -->

非表示をウィンドウを表示するための下準備をする

テンプレートのコードを少し修正し、続く節でイベントハンドラからウィンドウを表示できるよう準備をする。 既存のAppクラスの一部を以下のように書き換え、staticメンバを作成して簡易的なシングルトンとして機能させる。

App.xaml.cs

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    MainWindow = new MainWindow();
    MainWindow.Activate();
}

public static Window MainWindow;

NotifyIcon左クリック時にウィンドウを表示する

上で指定したイベントハンドラIconClickedを実装する。 このイベントハンドラの指定には癖がある。 H.NotifyIconのコントロールではメソッドを直接指定できず、System.Windows.Input.ICommand型の変数をx:Bindする必要がある。 この原因は恐らく、H.NotifyIconがWPFwindows presentation foundation)をターゲットとして作成され、後からWindows App SDKに対応させたからだと思われる。

まずはinterface ICommandを実装するclassを実装する。 ICommandの設計思想は置いておき、定義されている3つのものを次のように実装すればよい。

MainWindow.xaml.cs

using System.Windows.Input;

// 中略

public class IconClickedCommand : ICommand
{
    // 使わないからnullにしておく
    public event EventHandler CanExecuteChanged = null;
    public bool CanExecute(object parameter)
    {
        // この単純な実装では常にtrueを返せばよい。
        return true;
    }

    // 処理の実体をここに記述する
    public void Execute(object parameter)
    {
        App.MainWindow.Activate();
    }
}

そして、このインスタンスをメンバに登録する。

MainWindow.xaml.cs

public sealed partial class MainWindow : Window
{
    private readonly IconClickedCommand IconClicked;
    public MainWindow()
    {
        this.InitializeComponent();

        IconClicked = new IconClickedCommand();
        // 中略 イベントハンドラ追加の処理
    }
}

これで、先のXAMLでの指定が機能する。

LeftClickCommand="{x:Bind IconClicked}"

プログラムの実行と停止

Unpackagedであることを確認してVisual Studioからデバッグでビルドと実行を行う。 最初に起動したウィンドウを閉じた後、NotifyIconを左クリックして再びウィンドウを表示できれば動作テストは完了。

Closedイベントに対するデフォルトの動作変えたから、アプリを停止させるにはVisual Studioから「デバッグの停止」を押すか、プロセスを直接killする必要がある。

成果物

https://github.com/dullwhale/winui3-notifyicon-test