C#で二重起動を禁止するコード、起動済みプロセスに処理を移管するコード
アプリケーションは通常、プロセスとして複数起動できるものですが、何かの理由で1つのみ起動できるようにしたい場合があります。例えば、唯一リソースにアクセスするものや、インスタンス間の連携のためにスレッドで実現したい場合などです。後者はWindowsでは良く見る仕様で、Officeがそうですし、Adobe Readerなどもそうです。また、Macでは複数のアプリを上げようとしても、既に起動しているインスタンスに切り替わります。
ここでは、単純に2つの仕様の実装を紹介します。
- 二重起動を単に阻止する。2つ目を上げようとしたときに警告を出して終了する。
- 2つ目を上げようとしたとき、既に起動しているプロセスが(あたかも起動したかのように)表示される。
二重起動を阻止
プロセスの始めのほうの行に、以下のコードを入れます。Program.cs内で充分です。
// check not to run same processes simultaneously.
if (Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).Length > 1) {
MessageBox.Show(Properties.Resources.isAlreadyRunning);
return;
}
要するに、起動プロセスと同じ名前のプロセスがあったら、警告ダイアログを出して終了します。
起動済みプロセスに処理を任せる
前述のMacのような仕様です。起動しようとするユーザは、そのタスクを実行したいのですから、「できない」とか言われても困ってしまいます。できる限り、ユーザがやりたいことを実現するべきでしょう。2つ目を上げようとすると、起動できたように見えて、実は1つ目のプロセスのウィンドウがアクティブ状態で表示されます。
なのですが、こちらのほうは実装がすっきりしません。本来であれば、起動済みプロセスにお知らせして、実行を代行してもらうように実装すべき(UNIXであれば、このように実装するでしょう)なのですが、手軽ではありません。そこで、WinAPIを使って、表示を制御してしまいます。
// check not to run same processes simultaneously.
if (ActivateRunningProcess()) {
return;
}
----------
// Activate the process, if a instance is already running.
private static bool ActivateRunningProcess() {
string pnm = Process.GetCurrentProcess().ProcessName.ToString();
string cls = "WindowsForms10.Window.8.app.0.33c0d9d";
IntPtr hWnd = FindWindow(cls, pnm);
if (hWnd == IntPtr.Zero) {
//-- With debugger .vshost must be added after the processname.
string pnmr = pnm.Replace(".vshost", "");
if (!pnmr.Equals(pnm)) {
hWnd = FindWindow(cls, pnmr);
}
}
if (hWnd == IntPtr.Zero) {
return false;
}
bool rts = ShowWindowAsync(hWnd, SW_RESTORE);
bool rtf = SetForegroundWindow(hWnd);
if (!rts || !rtf) {
MessageBox.Show(Properties.Resources.isAlreadyRunning + "("
+ (rts?"1":"0") + "," + (rtf?"1":"0") + ")");
}
return true;
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
private const int SW_RESTORE = 9; // Restore the window size
起動済みのアプリプロセスを捜すのに、FindWindowを使っています。本当は、GetProcessByNameで捜すべきところなのですが、これでやると、プロセスは見つかりますが、MainWindowHandleが取れない場合があります。デバッグした範囲では、殆どの場合でOKなのですが、ステータス表示のみ(アイコンボックスに表示されているだけ)の常駐状態の場合には無効なWindowHandleが戻ってきます。仕様(実際にMainWindowが作成されていない/破棄されているのかも)なのか、バグなのか分りません。
FindWindowでは、アプリ名のフォルダーを開いているExplorerのプロセス(というかウィンドウ)も掴まえてしまうため、実際上、クラスの指定が必須になります。これは Spy+ で確認できますが、呪文を含む文字列で中身は良く分りません。とりあえず、ビルドし直しても、VisualStudioのバージョンを変えても大丈夫なようです。
また、Visual Studio 2010から、デバッグ時のプロセス名に ".vshost" が付くようになっているようです。この部分のコードは、デバッグ時以外は必要ありません。
まとめ
これで、2重起動への対応ができました。上記以外で、通常必要になるかもしれない機能は、起動パラメタを渡すことくらいでしょうか。
WinAPIは既に使うべきではないようですが、便利なので使い続けられて行くでしょう。なんと言うか反則じみていますし、結構危険に思われます。例えば、今回のコードでも、他のプロセスから勝手に表示制御などされてしまえば、最悪プロセスが落ちても文句言えませんよね。稼働中のプロセスの任意のタイミングでスクリーンにマップされてしまう訳で、表示(show)のコールバックなどもアプリが予期せぬタイミングで呼ばれてしまいます。普通に実装していれば動くでしょうが、何らかの状態制御や排他制御などをしていれば必ずしも安全ではありません。