アプリケーションを二重に起動させたくない場合の対処法も、FAQ の類であろう。いろいろな方法があると思うが、ここでは上記の参照元を参考にして、Mutex オブジェクトを使ってみよう。Mutex はシステムグローバルなオブジェクトとして作成され、スレッド間の同期を取るときなどに使うのが本来の目的である。ここでは、名前付きの Mutex が同名でただ一つしか同時には作成できないことを利用して、アプリケーションの二重起動を防ぐのに使うことになる。Mutex は、以下の関数で作られる。
function CreateMutex(lpMutexAttributes: PSecurityAttributes; bInitialOwner: BOOL; lpName: PChar): THandle; stdcall;
第一パラメータはセキュリティー属性を設定するレコード型
PSecurityAttributes = ^TSecurityAttributes; _SECURITY_ATTRIBUTES = record nLength: DWORD; lpSecurityDescriptor: Pointer; bInheritHandle: BOOL; end; TSecurityAttributes = _SECURITY_ATTRIBUTES; SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES;
へのポインタである。ここでは、nil を設定する。第二パラメータを true にすると、作成と同時にこの関数を実行しているスレッドがこの Mutex のオーナーになる。一つの Mutex に対して、同時にオーナーになれるのは、一つのスレッドだけである。オーナー権を手放すには、ReleaseMutex() を実行する。第三パラメータには、文字列を設定してこの Mutex に名前をつける。これを nil にすると名前なしになる。CreateMutex() は、成功すると Mutex へのハンドルを返す。また、この関数が実行される前に、すでに同名の Mutex が作成済みであった場合は、直後の GetLastError は、ERROR_ALREADY_EXISTS を返す。
ERROR_ALREADY_EXISTS = 183;
このことを利用して、すでに同じアプリが起動済みかどうかを判断できる。この場合でもハンドルは取得できるが、オーナーにはなれない。ハンドルは、取得したスレッドのプロセスが終了するとシステムによって自動的に閉じられる。最後のハンドルが閉じられたとき、Mutex オブジェクトは破棄される。それでは、簡単な例から始めよう。プロジェクトファイルを以下のようにすると二重起動は防止できる。
List 1
program OneInstance01; uses Forms,Windows, Main in 'Main.pas' {Form1}; {$R *.RES} function IsPrevAppExist(UniqueName:string):Boolean; begin result := false; CreateMutex(nil,true,PChar(UniqueName)); if GetLastError = ERROR_ALREADY_EXISTS then result := true; end; begin if IsPrevAppExist('HogeHogeApp') then exit; Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
uses 節に Windows ユニットを加えることを忘れないように。IsPrevAppExist() は、上記の説明のとおり、すでに起動中のアプリがあるとき true を返す。
さて、これだけでは、ユーザはなぜ起動しないのか不思議に思うかもしれない。そこで、メッセージを表示してみよう。
List 2
program OneInstance02; uses Forms,Windows,Dialogs, Main in 'Main.pas' {Form1}; {$R *.RES} function IsPrevAppExist(UniqueName:string):Boolean; begin result := false; CreateMutex(nil,true,PChar(UniqueName)); if GetLastError = ERROR_ALREADY_EXISTS then result := true; end; begin if IsPrevAppExist('HogeHogeApp') then begin ShowMessage('このアプリは二重起動できません'); exit; end; Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
ここでは、uses に Dialogs を加えて、右図のように ShowMessage() を実行している。このような表示をするアプリケーションはかなりたくさんある。
さらにユーザに親切にするには、すでに起動しているアプリを前面に持ってくるとよいであろう。そのためには、そのアプリの Application ウィンドウのハンドルを取得しなければならない。これは、あまり簡単ではない。Delphi で作られたアプリの最上位のトップレベルウィンドウのクラス名は常に TApplication であるから、検索にクラス名を使うことはできない。参照元では、検索すべきトップレベルウィンドウに、ユニークなプロパティーを設定することでこれを解決している。ウィンドウには、OS から見える「プロパティー」を設定できる。すでに API の稿で説明しているが、プロパティーを設定するには、
function SetProp(hWnd: HWND; lpString: PChar; hData: THandle): BOOL; stdcall;
を実行すると、文字列 lpString で識別される32ビットの数値 hData が hWnd に設定される。hData は、一応 THandle 型になっているが、単なる数値でも、ポインタでも、オブジェクトのインスタンス参照もキャストして設定できる。プロパティーを削除するには、
function RemoveProp(hWnd: HWND; lpString: PChar): THandle; stdcall;
を実行する。プロパティーを取得するには、
function GetProp(hWnd: HWND; lpString: PChar): THandle; stdcall;
を実行する。そのほかに特定のウィンドウのプロパティーを列挙することもできるがここでは説明しない。ここの目的は、Application ウィンドウに、ユニークな名前のプロパティーを設定して、EnumWindows() ですでに起動済みの Application ウィンドウを識別することである。そして、それを SetForegroundWindow() で前面に持ってくる。起動時の特権で起動アプリは必ず Foreground になるので、他のウィンドウに対しての SetForegroundWindow() は必ず期待通りに動作する。それではやってみよう。
List 3
program OneInstance03; uses Forms, Windows, Dialogs, Main in 'Main.pas' {Form1}, Unit2 in 'Unit2.pas' {Form2}; {$R *.RES} const UniqueName:string = 'HogeHogeApp'; function IsPrevAppExist(Name:string):Boolean; begin result := false; CreateMutex(nil,true,PChar(Name)); if GetLastError = ERROR_ALREADY_EXISTS then result := true; end; function EnumWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; begin result := true; if GetProp(hWindow,PChar(lData)) = 1111 then begin if IsIconic(hWindow) then ShowWindow(hWindow,SW_RESTORE); SetForegroundWindow(hWindow); result := false; end; end; procedure ActivatePrevApp(Name:string); begin EnumWindows(@EnumWndProc,LPARAM(PChar(Name))); end; begin if IsPrevAppExist(UniqueName) then begin ShowMessage('このアプリは二重起動できません'); ActivatePrevApp(UniqueName); exit; end; SetProp(Application.Handle,PChar(UniqueName),1111); Application.Initialize; Application.CreateForm(TForm1, Form1); Application.CreateForm(TForm2, Form2); Application.Run; RemoveProp(Application.Handle,PChar(UniqueName)); end.
これでうまくいく。最初に起動されたアプリだけが、PChar(UniqueName) で識別されるプロパティーを Application ウィンドウに設定されるので、EnumWindows() のコールバック関数では、これをつかってハンドルを取得している。
それでは、最後にこの機能をコンポーネントにしてみよう。
List 4
unit OneInstanceUnit; interface uses Windows,Messages,Forms,Classes; type TOneInstance = class(TComponent) private FUniqueName:string; FOnPrevInstanceExist:TNotifyEvent; FAllocWnd:HWND; protected procedure AllocWnd(var Message: TMessage); function IsPrevAppExist:Boolean;virtual; public constructor Create(AOwner: TComponent);override; destructor Destroy; override; procedure ActivatePrevApp; published property UniqueName:string read FUniqueName write FUniqueName; property OnPrevInstanceExist:TNotifyEvent read FOnPrevInstanceExist write FOnPrevInstanceExist; end; procedure Register; implementation procedure Register; begin RegisterComponents('Samples',[TOneInstance]); end; { TOneInstance } constructor TOneInstance.Create(AOwner: TComponent); begin inherited; FUniqueName := 'HogeHogeApp123'; if csDesigning in ComponentState then exit; FAllocWnd := AllocateHWnd(AllocWnd); PostMessage(FAllocWnd,WM_USER+200,0,0); end; destructor TOneInstance.Destroy; begin DeallocateHWnd(FAllocWnd); RemoveProp(Application.Handle,PChar(FUniqueName)); inherited; end; function TOneInstance.IsPrevAppExist: Boolean; begin result := false; CreateMutex(nil,true,PChar(FUniqueName)); if GetLastError = ERROR_ALREADY_EXISTS then result := true; end; function EnumWndProc(hWindow:HWND;lData:LPARAM):BOOL;stdcall; begin result := true; if GetProp(hWindow,PChar(lData)) = 1111 then begin if IsIconic(hWindow) then ShowWindow(hWindow,SW_RESTORE); SetForegroundWindow(hWindow); result := false; end; end; procedure TOneInstance.ActivatePrevApp; begin EnumWindows(@EnumWndProc,LPARAM(PChar(FUniqueName))); end; procedure TOneInstance.AllocWnd(var Message: TMessage); begin if Message.Msg <> WM_USER+200 then exit; if IsPrevAppExist then begin if Assigned(FOnPrevInstanceExist) then FOnPrevInstanceExist(self); PostMessage(Application.Handle,WM_SYSCOMMAND,SC_CLOSE,0); end else SetProp(Application.Handle,PChar(FUniqueName),1111); end; end.
このコンポーネントをアプリケーションの MainForm (最初に作られるフォーム、デフォルトでは Form1 である)に置いて、UniqueName プロパティーに適当な文字列を設定するだけで二重起動を防止できる。さらに OnPrevInstanceExist イベントハンドラを
List 5
procedure TForm1.OneInstance1PrevInstanceExist(Sender: TObject); begin OneInstance1.ActivatePrevApp; end;
のようにすると、起動済みアプリを最前面にもってくることができる。面白いのは、
List 6
procedure TForm1.OneInstance1PrevInstanceExist(Sender: TObject); begin ShowMessage('このアプリは二重起動できません'); OneInstance1.ActivatePrevApp; end;
のようにして、モーダルダイアログ ShowMessage() を実行すると、一応 Form1 が表示されるがダイアログのOKボタンをおすと、そのアプリが終了する。これにより、「一応、ご要求のように起動しましたが、すでに起動済みなので閉じます」というメッセージがユーザに明確に伝わるだろう。