[ SPECIAL ]
Visual Studio 2005 / C#
アプリケーションの二重起動を防止する
2006/09/29 Tomohiro Kumagai
□ アプリケーションの二重起動
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";
System.Threading.Mutex mutex = new System.Threading.Mutex(false,
MUTEX_NAME);
if (mutex.WaitOne(0, false))
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
mutex.ReleaseMutex();
}
else
{
MessageBox.Show("アプリケーションは既に起動しています。");
}
mutex.Close();
}
こうすることで最初に起動したアプリケーションは通常通りに動きますし、それを起動したままもうひとつ起動しようとすると、その旨を示すダイアログボックスが表示されるだけな感じになりま
した。
□ Mutex で多重起動検出時に、既存のウィンドウを最前面に移動する
Mutex
で多重起動を阻止したときに、既に起動されているアプリケーションを前面に表示させてから重複した自分自身を終了させることも、少し工夫をすることで可能です。そのようにすることで、アプリケーションが最小化されて既に実行されたりしている場面などにでも、利用者がすぐに操作できるようになるので親切です。
それを行うためには Windows API を利用する必要がありますので、まずは次のような API へアクセスするためのクラスを作成しておきます。
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
/// <summary>
/// </summary>
public static class WindowsAPI
{
/// <summary>
/// </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>
/// </summary>
/// <param name="hWnd">対象となるウィンドウハンドルです。</param>
/// <returns>正常に終了した場合に true が返ります。</returns>
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(InPtr hWnd);
/// <summary>
/// </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>
/// </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);
foreach (System.Diagnostics.Process proc in running)
{
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
{
result = false;
}
return result;
}
そして、あとはこれを Program クラス内の Main メソッドにある "WaitOne メソッドで所有権を取得できなかった場合の処理" のところで利用
します。
else
{
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 というものがあることが分かりました。
using System.Text;
/// <summary>
/// </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>
/// </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";
System.Threading.Mutex mutex = new System.Threading.Mutex(false,
MUTEX_NAME);
if (mutex.WaitOne(0, false))
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
mutex.ReleaseMutex();
}
else
{
if (!WakeupWindow(CLASS_NAME))
{
MessageBox.Show("アプリケーションは既に起動しています。");
}
}
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
{
result = false;
}
return result;
}
そして、ウィンドウが復元された際にタスクバーにアイコンが表示されていて欲しいようなアプリケーションの場合には、その辺りも気にしておく必要があります。ウィンドウの復元時には
Resize イベントが発生しますので、それをオーバーライドして、状況に応じてタスクバーのアイコン表示を調整してあげる感じにしておくのが簡単です。
private void Folder1_Resize(object sender, EventArgs e)
{
ShowInTaskbar = (WindowState != FormWindowState.Minimized);
}
これでとりあえずは完成です。個人的には自分で決めていないウィンドウハンドルを定数で持たせておくことになんだかすっきりしないのですけど、多分これでも大丈夫なのではないかなって思います。
他にも "名前付きイベント" Event クラスを活用して Mutex
のような利用をして元のプログラムとシグナルのやり取りをしたりとか、メインウィンドウハンドルを起動時に Windows
レジストリに記録しておくとか、いろいろと方法はあると思いますけど、とりあえず何か問題が見つかるまではこの方法でやっていってみようかなってところです。