二重起動の禁止

参照元


redball.gif目次へ戻る

アプリケーションを二重に起動させたくない場合の対処法も、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.
n01101.gif

ここでは、usesDialogs を加えて、右図のように ShowMessage() を実行している。このような表示をするアプリケーションはかなりたくさんある。


さらにユーザに親切にするには、すでに起動しているアプリを前面に持ってくるとよいであろう。そのためには、そのアプリの Application ウィンドウのハンドルを取得しなければならない。これは、あまり簡単ではない。Delphi で作られたアプリの最上位のトップレベルウィンドウのクラス名は常に TApplication であるから、検索にクラス名を使うことはできない。参照元では、検索すべきトップレベルウィンドウに、ユニークなプロパティーを設定することでこれを解決している。ウィンドウには、OS から見える「プロパティー」を設定できる。すでに API の稿で説明しているが、プロパティーを設定するには、

function SetProp(hWnd: HWND; 
             lpString: PChar; 
                hData: THandle): BOOL; stdcall;

を実行すると、文字列 lpString で識別される32ビットの数値 hDatahWnd に設定される。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ボタンをおすと、そのアプリが終了する。これにより、「一応、ご要求のように起動しましたが、すでに起動済みなので閉じます」というメッセージがユーザに明確に伝わるだろう。

redball.gif目次へ戻る