概要
Windowsでデスクトップアプリを作ると、必ずついて回るのが「多重起動されたらどうするか」です。エディタなどの自由度の高いアプリを除けば、たいていはユーザーの混乱を避けるために多重起動禁止、できれば多重起動時に「すでに起動しているウインドウをアクティブにする」といった要求になると思います。ウインドウメッセージやイベントやミューテックスを使えばまあ自力でも作れますが、少々面倒です。
Windows App SDKを組み込むと、WPFでもこれを容易に実現できます。これ1つ使うだけでも十分に便利だと思うので、最小限の組み込み方を紹介します。
最初に結論まとめ
こんな感じで、既存のWPFの処理に足す形で実現できます。
- NuGetでWindows App SDKを組み込む※1
- プロジェクトの設定をいくつか変更する※1
- アプリ起動時のイベントハンドラ(Application.Startupなど)に、多重起動判定処理を入れる
- 「Microsoft.Windows.AppLifecycle.AppInstance.GetInstances()」を使ってインスタンスを取得し、「インスタンスが2つ以上なら、終了する」と判定
- 多重起動時に処理が必要なら、最初に起動したプロセスで自分のインスタンス(AppInstance.GetCurrent())にOnActivateイベントを追加して、ウインドウのアクティブ化などを行う
※1の例
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.240923002" />
</ItemGroup>
説明
今年春のBuildでWPFとWin UI 3の併用の方針がはっきりと打ち出されたためか、Win UI 3前提だった機能がWPFでも使えるようになってきています。(最新の体験を実装したいならWinUI、既存資産を生かすならWPF、という使い分け方針が語られていました)新顔のWindows App SDKも、WPFへの組み込みが可能になってきました。
Windows App SDKの機能はいくつもありますが、ここではアプリケーションライフサイクルのMicrosoft.Windows.AppLifecycle.AppInstanceを使います。
これは同一のexeから起動したプロセスを「インスタンス」として、まとめて管理することができる。という機能のようです。使い道は色々ありそうですが、ここでは多重起動対策の用途に絞って使ってみます。
ちなみにサンプルはオフィシャルのGitHubにもありますが、多重起動対策以外にも色々まとめて実装されているので、最小限のポイントが分かりづらい感じです。
ここで紹介している最小限実装のものを、GitHubのこのリポジトリに入れておきました。今後もっと処理を足すかもしれませんので、最小限のタグはこちらです。
WPFプロジェクトへのWindows App SDKの組み込み
WPF アプリでWindows App SDKを使用する - Windows apps | Microsoft Learnの説明通りに進めれば問題ないと思います。実際にやったことを以下に書きます。
前提として、開発環境にWindows App SDKをインストールします。 Windows App SDK 用の最新のダウンロード - Windows apps | Microsoft Learn
次に、既存のWPFプロジェクトへWindows App SDKを組み込みます。
Windows App SDK自体は、NuGetで組み込めます。プロジェクトにこんな風に追加します。
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.240923002" />
</ItemGroup>
次にプロジェクトファイルを少し変更します。
TargetFrameworkにWindowsのバージョンを追加します。これはWindows向けにWPFアプリを作っている場合は、たいてい元々入っていると思います。
RuntimeIdentifiersを追加して、対象を明示します。このケースではArm対応もしないので、win-x64を指定しています。
WindowsPackageTypeに値Noneを追加して、普通のWPFの配布方法(UnPackaged)かつWindows App SDKの初期化を自動で行うようにします。ここは用途に応じてカスタマイズできますが、今回の狙いではこれが一番手軽です。
以上を全てまとめると、次のようになります。
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.240923002" />
</ItemGroup>
このように変更してからビルドすると、Windows App SDKの初期化のコードが自動生成されます。これでプロジェクト側の準備は完了なので、続けて実装に移ります。
多重起動禁止の実装
まず、今起動しているインスタンスの数をこのように取得します。
※以降、usingの記載は省略します
using Microsoft.Windows.AppLifecycle;
var instances = AppInstance.GetInstances();
WindowsAppSDKを初期化したプロセスの数だけインスタンスが返ってくるようです。つまり、その数が1より上かどうかの判定で、多重起動かどうかを判断できることになります。1より上の場合は終了するだけで、多重起動禁止のできあがりです。とても簡単ですね。
if (1 < instances.Count){
Shutdown();
return;
}
図で描くと、多分こんな感じの動きです。
多重起動時のイベント処理の実装
さらに、すでに起動しているウインドウをアクティブにするようなことも簡単にできます。
取得したインスタンスに対してイベントを発生させるメソッドがあるので、それを呼び出すだけです。インスタンスは配列で返ってきますが、この場合は多重起動禁止なのでインスタンスは1つしかありません。(連続実行した時の厳密な排他を問わなければ)なのでFirstを対象にすると、こうなります。
var mainInstance = instances.First();
mainInstance.RedirectActivationToAsync(args).GetAwaiter().GetResult();
ここではアプリケーション起動時の同期処理なので、awaitを使わずに完了を待機しています。
これによって、メインの(1つ目の)インスタンスに対してイベントが発生します。メインのインスタンスで事前にイベントハンドラを登録しておけば、それが呼び出されることになります。次のように実装しておけば、イベント発生時にWPFのメインウインドウのActivateが呼び出され、アクティブになります。
AppInstance.GetCurrent().Activated += OnActivated;
private void OnActivated(object? sender, AppActivationArguments e)
{
Current.Dispatcher.Invoke(() => Current.MainWindow?.Activate());
}
次の図の8番の矢印の動きです。
このようにプロセスを跨いだ通信は、イベントやウインドウメッセージなどを使って組む必要があり、少々手間でした。それが、単にAppInstanceというものを列挙してメソッドを呼び出すだけで実現できています。これは手軽ですね。
まとめ
「多重起動を禁止し、多重起動した場合は起動済みのウインドウをアクティブにして欲しい」という、ありがちだが意外に実装が面倒な要求を、Window App SDKを使うことで手軽に実現できました。Win UIではないとダメなどということもなく、WPFでも使えています。
Windows App SDKは、まだ新しくて使い道が分からないところも有ると思いますが、このように便利なところだけでもどんどん採用していき、Windowsアプリ開発を便利にしていきましょう!