アプリケーションの二重起動を防止する
SPECIAL
アプリケーションの二重起動
Microsoft Visual Studio 2005 の Visual C# 8.0 で普通に Windows アプリケーションを作成すると、出来上がったプログラムは、それを実行するたびに 2 つ、3 つと起動させることが出来ます。
例えば文書を編集するようなソフトウェアだったりするとそれは便利だったりするのですけど、システム状態を監視しているソフトのように 1 つだけ動けば十分なものや、中には複数動くと困るような場面が出てくることもあって。そんなときに気にする必要が出てくるのが、今回の二重起動のお話しです。
二重起動を阻止する方法としてはいろいろな方法があるのでしょうけど、とにかく 2 番目に起動することとなったアプリケーションが、既に起動されているものを検出することが出来ればいい感じになります。そして既に起動されていることが分かったら自分自身は終了してあげれば、二重起動を防止することが可能です。
それを実現する仕組みはいくつか用意されていて、簡単に利用することも出来るようになってはいるのですが、理想を形にするためには機能不足なところもあったりして、いろいろなことを試してみることとなりましたので、それについて記してみようと思います。
Mutex クラスを利用する
二重起動を阻止する上でいちばん基本的なのが Mutex という機能を利用する方法です。
Mutex とは "Mutual Exclusion" の略で、複数のスレッドが共有リソースへアクセスするような場合に排他的にそれらを利用する手助けをするための機能です。これは単純に、指定した名前を要求すると既にそれが要求されているかを知ることができるといった程度の仕組みではありますけど、プロセスを越えて状況判断を行うためには非常に手軽で便利です。
.NET Framework には System.Threading.Mutex という Mutex を管理するためのクラスが備わっていますので、それを使って C# による Windows アプリケーションの多重起動防止のプログラムを組んでみようと思います。
Mutex クラスを利用して二重起動を阻止する場合、次のメソッドを使って制御する感じになるようです。
Mutex | コンストラクタを使ってインスタンスを生成します。 |
---|---|
Mutex.WaitOne | Mutex の所有権を取得したり、他で所有権が取得されていることを知るために使用します。所有権を取得してある Mutex の場合、複数回呼び出してその数だけ所有権を所持することも可能だそうです。 |
Mutex.ReleaseMutex | 取得した Mutex の所有権を解放します。複数の所有権が取得されている場合は、取得した数だけ ReleaseMutex を呼び出す必要があるそうです。 |
Mutex.Close | インスタンスが保持している所有権を全て解放します。 |
Visual Studio 2005 で作成した Windows アプリケーションの Program クラス内にて、これらを組み込んでゆくことで二重起動の阻止が図れます。実際に Main 関数を調整してみると、次のような感じになります。
static void Main()
{
const string MUTEX_NAME = "MutexTestApplication";
// Mutex インスタンスを生成します。二番目の引数 MUTEX_NAME は生成する Mutex の識別名です。
System.Threading.Mutex mutex = new System.Threading.Mutex(false, MUTEX_NAME);
// WaitOne メソッドを使って、所有権を得ます。
// 最初の引数で 0 ミリ秒を指定して、所有権が取得できなくても待ちません。
if (mutex.WaitOne(0, false))
{
// 所有権を得られた場合には、アプリケーションの実行手続きに入ります。
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
// アプリケーションの実行が終わったら Mutex の所有権を解放します。
mutex.ReleaseMutex();
}
else
{
// WaitOne メソッドで所有権を取得できなかった場合の処理です。
MessageBox.Show("アプリケーションは既に起動しています。");
}
// 最後に念のため Mutex インスタンスを完全に解放しておきます。
mutex.Close();
}
こうすることで最初に起動したアプリケーションは通常通りに動きますし、それを起動したままもうひとつ起動しようとすると、その旨を示すダイアログボックスが表示されるだけな感じになりま した。
Mutex で多重起動検出時に、既存のウィンドウを最前面に移動する
Mutex で多重起動を阻止したときに、既に起動されているアプリケーションを前面に表示させてから重複した自分自身を終了させることも、少し工夫をすることで可能です。そのようにすることで、アプリケーションが最小化されて既に実行されたりしている場面などにでも、利用者がすぐに操作できるようになるので親切です。
それを行うためには Windows API を利用する必要がありますので、まずは次のような API へアクセスするためのクラスを作成しておきます。
using System;
using System.Collections.Generic;
using System.Text;
// DllImport を利用するために必要です。
using System.Runtime.InteropServices;
/// <summary>
/// Windows API にアクセスするための静的クラスです。
/// </summary>
public static class WindowsAPI
{
/// <summary>
/// ShowWindow Commands 定数を列挙型にしたものです。
/// VC\PratformSDK\Includes\winuser.h を参考にしました。
/// </summary>
public enum ShowWindowEnum : int
{
SW_HIDE = 0,
SW_NORMAL = 1,
SW_SHOWMINIMIZED = 2,
SW_MAXIMIZE = 3,
SW_SHOWNOACTIVATE = 4,
SW_SHOW = 5,
SW_MINIMIZE = 6,
SW_SHOWMINNOACTIVE = 7,
SW_SHOWNA = 8,
SW_RESTORE = 9,
SW_SHOWDEFAULT = 10,
SW_MAX = 11
}
/// <summary>
/// 最前面に表示されるウィンドウを指定する Windows API です。
/// </summary>
/// <param name="hWnd">対象となるウィンドウハンドルです。</param>
/// <returns>正常に終了した場合に true が返ります。</returns>
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(InPtr hWnd);
/// <summary>
/// ウィンドウの状態を変更する Windows API です。
/// </summary>
/// <param name="hWnd">対象となるウィンドウハンドルです。</param>
/// <param name="nCmdShow">ウィンドウの状態を示す ShowWindowEnum 列挙子です。</param>
/// <returns>設定前にウィンドウが可視状態だった場合に true が返ります。</returns>
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(InPtr hWnd, ShowWindowEnum nCmdShow);
/// <summary>
/// 指定したウィンドウが最小化されているかどうかを判定する Windows API です。
/// </summary>
/// <param name="hWnd">対象となるウィンドウハンドルです。</param>
/// <returns>ウィンドウが最小化に設定されている場合に true が返ります。</returns>
[DllImport("user32.dll")]
public static extern bool IsIconic(InPtr hWnd);
}
続いて、実行中のプロセスから自分と同じプログラムを使って起動されているプロセスがないかを 判定し、発見した場合はそれを前面に表示させるメソッドを実装して行きます。これは Windows アプリケーションの Program クラス内に追記する感じが良いでしょう。
/// <summary>
/// プロセス名とプログラム名が同一のプロセスを見つけ、
/// それが所持しているメインウィンドウを前面に表示します。
/// </summary>
/// <returns>正常にウィンドウを前面に表示できた場合に true を返します。</returns>
protected bool WakeupWindow()
{
bool result;
System.Diagnostics.Process current;
System.Diagnostics.Process[] running;
System.Diagnostics.Process target = null;
// 現在アクティブなプロセスを取得します。
current = System.Diagnostics.Process.GetCurrentProcess();
// 稼働中のプロセスから、アクティブなプロセスと同一のプロセス名を持つプロセスを取得します。
running = System.Diagnostics.Process.GetProcessByName(current.ProcessName);
// 自分自身を除外しつつ、自分以外の同一プログラムのプロセスを target に取得します。
foreach (System.Diagnostics.Process proc in running)
{
// プロセス ID が自分自身とは異なるものを探します。
if (proc.Id != current.Id)
{
// 捜査中のプロセスが、アクティブなプロセスと同一ファイル名であるかどうかを調べます。
if (proc.MainModule.FileName == current.MainModule.FileName)
{
// ファイル名が一致した場合は、それが目的のプロセスとなります。
target = proc;
break;
}
}
}
// 該当するプロセスが見つかった場合の操作です。
if (target != null)
{
// ウィンドウが最小化されていた場合には、それを元に戻します。
if (WindowsAPI.IsIconic(target.MainWindowHandle)) WindowsAPI.ShowWindowAsync(target.MainWindowHandle, WindowsAPI.ShowWindowEnum.SW_RESTORE);
// ウィンドウを前面に移動します。
result = WindowsAPI.SetForegroundWindow(target.MainWindowHandle);
}
else
{
// 該当するプロセスが見つからなかった場合には false を返させます。
result = false;
}
return result;
}
そして、あとはこれを Program クラス内の Main メソッドにある "WaitOne メソッドで所有権を取得できなかった場合の処理" のところで利用 します。
else
{
// WaitOne メソッドで所有権を取得できなかった場合の処理です。
if (!WakeupWindow())
{
// ウィンドウを前面に出すのに失敗した場合は、メッセージで通知しておくことにします。
MessageBox.Show("アプリケーションは既に起動しています。");
}
}
このようにすることで、Mutex によって二重起動が検出されたとき、既に起動されていたウィンドウを前面に表示させることが可能となります。
ShowInTaskbar が False の場合にウィンドウを最前面に移動できない
ウィンドウハンドルが取得できない場面
通常ならこれで問題なくウィンドウを操作することができる感じなんですけど、Windows フォームの ShowInTaskbar プロパティが false に設定されてタスクバーに情報が表示されてなかったりすると、これでは上手く行かなくなってしまうことが分かりました。
どうしてそうなるのかと原因を調べてみたところ、System.Diagnostics.Process.GetProcessByName ではしっかりとプロセス情報を入手できているのですけど、その Process が持つ MainWindowHandle の値が IntPtr.Zero を示していることがわかりました。操作対象となるメインウィンドウハンドルを取得することが出来ないため、その影響で SetForegroundWindow API も然ることながら、IsIconic API なども false を返してしまう感じです。
ところで ShowInTaskbar プロパティの値によって MainWindowHandle でウィンドウハンドルを取得できるかどうかが違ってくることから、もしかするとタスクバーに残すか残さないかで待機中のメモリ使用量に差が出てくるのかと思ってタスクマネージャでメモリ使用量を見てみたんですけど、ざっと見てみる感じでは、最小化すればどちらであっても同じくらいメモリ使用量が減少するようでした ので、そのような現象はリソースをリリースするかどうかというよりは、何かの管理体制に依るものなのかもしれないです。
ともあれそんなところから、たとえば普段はタスクトレイに常駐してタスクバーには表示されていないような場合は、プロセスから探し出す方法ではなくてウィンドウそのものを見つけてあげる必要が出てくるのですけど、それを行うためには FindWindow API を利用する感じになるようです。
ただ、この API は、取得したいウィンドウの "ウィンドウクラス名" というものを引数で指定する必要があるようで、今度はそれを知る必要が出てきました。
ウィンドウクラス名について調べてみる
ウィンドウクラス名は、ウィンドウを作成する際に CreateWindow API でウィンドウを生成する時にあわせて、RegisterClassEx API を使って生成しておく必要があるとのことです。ただし Visual C# 8.0 で Windows アプリケーションを作成している限りでは、これらを気にする必要がないため、今回の場合は逆に難しくなってきました。
ウィンドウクラス名に関して調べてみると、プログラムを起動中に Visual Studio 2005 に付属している "Spy++" というソフトウェアを利用することで、そのプログラムのウィンドウに設定されているウィンドウクラス名を確認することが出来るとのことだったので、調べてみたところ、実際に当てられていたウィンドウクラス名は "WindowsForms10.Window.8.app.0.b7ab7b" という感じで、なかなか分かりにくい名前になっていました。
その都度に自動生成されてもおかしくない感じの印象も受けた名前だったので、それについても調べてみたりしましたけど、ビルドによっては変更されることがあるかもしれないけれど、実行ファイルになってしまえば不変であることが定説となっているようです。
自分自身で調べてみた限りでは、同一のプログラムであれば複数起動させてみても、どれも同じウィンドウクラス名を保持しているようでした。ただ、Visual Studio 2005 から起動させる場合とバイナリを直接実行させる場合とでは、間に挟まれるプログラムの影響もあってか、ウィンドウクラス名がそれぞれ異なってくる感じでしたので、その辺りは注意して扱う必要がありそうです。
ウィンドウクラスに関する API
このことから、自分自身のウィンドウのウィンドウハンドルを知ることが出来れば、相手のウィンドウハンドルを知ったと同じになる気がします。そして、そのウィンドウハンドル名を知るための関数として GetClassName API というものがあることが分かりました。
// StringBuilder クラスを利用する上で必要になってきます。
using System.Text;
/// <summary>
/// ウィンドウクラス名を取得する Windows API です。
/// </summary>
/// <param name="hWnd">対象となるウィンドウハンドルです。</param>
/// <param name="lpClassName">ウィンドウクラス名を取得するための StringBuilder 変数です。</param>
/// <param name="nMaxCount">lpClassName が保持できる最大文字数を指定します。</param>
/// <returns>取得に成功した場合は lpClassName で使用した文字数が返ります。失敗した場合は 0 です。</returns>
[DllImport("user32.dll")]
public static extern int GetClassName(IntPtr hWnd, [Out] StringBuilder lpClassName, int nMaxCount);
あとはこれで取得したウィンドウクラス名を利用してウィンドウハンドルを取得するための FindWindow API も併せて準備しておけば、取得から検索までが出来るようになるのかなって感じです。
/// <summary>
/// ウィンドウクラス名やウィンドウタイトルから、該当するウィンドウハンドルを取得する Windows API です。
/// </summary>
/// <param name="lpClassName">ウィンドウクラス名を指定します。null を指定した場合には、あらゆるウィンドウクラス名が該当するものとします。</param>
/// <param name="lpWindowName">ウィンドウタイトルを指定します。null を指定した場合には、あらゆるウィンドウタイトルが該当するものとします。</param>
/// <returns>該当したウィンドウハンドルです。取得できなかった場合は IntPtr.Zero が返ります。</returns>
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
これらを使って、二重起動検出時にウィンドウを復元するプログラムを調整していってみようと思います。
ウィンドウクラス名を用いて、既存のウィンドウを最前面に移動する
ウィンドウを操作するために、まずはウィンドウクラス名を取得する必要があります。
このウィンドウクラスが生成されるのはどうやらフォームクラスのインスタンスを生成したときのようなので、もしもビルドしなおした時にウィンドウクラス名が変わってしまった ことがあったとしてもそれを取得すれば大丈夫かなと考えてみたんですけど、そうしてしまうと同一のウィンドウクラス名を持ったウィンドウがもうひとつ登録されてしまって、FindWindow を実行したときに自分自身を拾ってしまうことになってしまいました。
なので、あまり賢い感じもしないのですけど、ウィンドウクラス名は文字列定数 CLASS_NAME で扱う方針で進めておくことにします。
まず、Program クラスの Main メソッドの変更点としては、既存のウィンドウを前面に出す手続きの WakeupWindow メソッドの引数としてウィンドウクラス名を与えてあげる感じです。
static void Main()
{
const String MUTEX_NAME = @"MutexTestApplication";
const String CLASS_NAME = @"WindowsForms10.Window.8.app.0.33c0d9d";
/ Mutex インスタンスを生成します。二番目の引数 MUTEX_NAME は生成する Mutex の識別名です。
System.Threading.Mutex mutex = new System.Threading.Mutex(false, MUTEX_NAME);
// WaitOne メソッドを使って、所有権を得ます。
// 最初の引数で 0 ミリ秒を指定して、所有権が取得できなくても待ちません。
if (mutex.WaitOne(0, false))
{
// 所有権を得られた場合には、アプリケーションの実行手続きに入ります。
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
// アプリケーションの実行が終わったら Mutex の所有権を解放します。
mutex.ReleaseMutex();
}
else
{
// WaitOne メソッドで所有権を取得できなかった場合は、既存のウィンドウを前面にだす手続きです。
if (!WakeupWindow(CLASS_NAME))
{
// ウィンドウを前面に出すのに失敗した場合は、メッセージで通知しておくことにします。
MessageBox.Show("アプリケーションは既に起動しています。");
}
}
// 最後に念のため Mutex インスタンスを完全に解放しておきます。
mutex.Close();
}
そして、プロセスを検索して該当ウィンドウを見つけてウィンドウを前面に出す処理をしていた WakeupWindow メソッドも、ウィンドウクラス名を受け取ってその情報に見合うウィンドウを見つけて処理をするように変更します。
/// <summary>
/// 渡されたウィンドウクラス名と同じウィンドウクラス名を持つウィンドウを見つけ、それを前面に表示します。
/// </summary>
/// <param name="class_name">検索するウィンドウクラス名です。</param>
/// <returns>正常にウィンドウを前面に表示できた場合に true を返します。</returns>
protected bool WakeupWindow(string class_name)
{
bool result;
// ウィンドウクラス名からウィンドウハンドルを取得します。
IntPtr hWnd = WindowsAPI.FindWindow(class_name, null);
// ウィンドウハンドルを取得できたかどうかの判定です。
if (hWnd != IntPtr.Zero)
{
// ウィンドウが最小化されていた場合には、それを元に戻します。
if (WindowsAPI.IsIconic(hWnd)) WindowsAPI.ShowWindowAsync(hWnd, WindowsAPI.ShowWindowEnum.SW_RESTORE);
// ウィンドウを前面に移動します。
result = WindowsAPI.SetForegroundWindow(hWnd);
}
else
{
// ウィンドウハンドルが取得できなかった場合には false を返させます。
result = false;
}
return result;
}
そして、ウィンドウが復元された際にタスクバーにアイコンが表示されていて欲しいようなアプリケーションの場合には、その辺りも気にしておく必要があります。ウィンドウの復元時には Resize イベントが発生しますので、それをオーバーライドして、状況に応じてタスクバーのアイコン表示を調整してあげる感じにしておくのが簡単です。
private void Folder1_Resize(object sender, EventArgs e)
{
// サイズ変更が行われた後で、最小化状態でなければ場合はタスクバーにアイコンを表示します。
ShowInTaskbar = (WindowState != FormWindowState.Minimized);
}
これでとりあえずは完成です。個人的には自分で決めていないウィンドウハンドルを定数で持たせておくことになんだかすっきりしないのですけど、多分これでも大丈夫なのではないかなって思います。
他にも "名前付きイベント" Event クラスを活用して Mutex のような利用をして元のプログラムとシグナルのやり取りをしたりとか、メインウィンドウハンドルを起動時に Windows レジストリに記録しておくとか、いろいろと方法はあると思いますけど、とりあえず何か問題が見つかるまではこの方法でやっていってみようかなってところです。