防止多个实例 - 还要处理命令行参数?

时间:2011-12-31 12:28:20

标签: delphi

我正在处理来自Windows的应用程序关联扩展文件。所以当你从Windows双击一个文件时,它将执行我的程序,我从那里处理文件,如:

procedure TMainForm.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to ParamCount -1 do
  begin
    if SameText(ExtractFileExt(ParamStr(i)), '.ext1') then
    begin
      // handle my file..

      // break if needed
    end else
    if SameText(ExtractFileExt(ParamStr(i)), '.ext2') then
    begin
      // handle my file..

      // break if needed
    end else
  end;
end;

这几乎是我想要的,但是当我测试时,我意识到它不会考虑只使用我的程序的一个实例。

因此,例如,如果我从Windows中选择了几个文件并同时打开它们,这将创建与我的程序相同数量的实例,并打开文件数。

什么是一个很好的方法来解决这个问题,以便不是打开我的程序的几个实例,而是打开Windows的任何其他文件只会集中回到唯一的实例,并且我正常处理文件?

由于

更新

我在这里发现了一篇好文章:http://www.delphidabbler.com/articles?article=13&part=2我认为这就是我需要的,并展示了如何使用rhooligan提到的Windows API。我现在要读完它..

6 个答案:

答案 0 :(得分:8)

这是一些简单的示例代码,可以完成工作。我希望这是不言自明的。

program StartupProject;

uses
  SysUtils,
  Messages,
  Windows,
  Forms,
  uMainForm in 'uMainForm.pas' {MainForm};

{$R *.res}

procedure Main;
var
  i: Integer;
  Arg: string;
  Window: HWND;
  CopyDataStruct: TCopyDataStruct;
begin
  Window := FindWindow(SWindowClassName, nil);
  if Window=0 then begin
    Application.Initialize;
    Application.MainFormOnTaskbar := True;
    Application.CreateForm(TMainForm, MainForm);
    Application.Run;
  end else begin
    FillChar(CopyDataStruct, Sizeof(CopyDataStruct), 0);
    for i := 1 to ParamCount do begin
      Arg := ParamStr(i);
      CopyDataStruct.cbData := (Length(Arg)+1)*SizeOf(Char);
      CopyDataStruct.lpData := PChar(Arg);
      SendMessage(Window, WM_COPYDATA, 0, NativeInt(@CopyDataStruct));
    end;
    SetForegroundWindow(Window);
  end;
end;

begin
  Main;
end.

unit uMainForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Controls, Forms, StdCtrls;

type
  TMainForm = class(TForm)
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
  protected
    procedure CreateParams(var Params: TCreateParams); override;
    procedure WMCopyData(var Message: TWMCopyData); message WM_COPYDATA;
  public
    procedure ProcessArgument(const Arg: string);
  end;

var
  MainForm: TMainForm;

const
  SWindowClassName = 'VeryUniqueNameToAvoidUnexpectedCollisions';

implementation

{$R *.dfm}

{ TMainForm }

procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.WinClassName := SWindowClassName;
end;

procedure TMainForm.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  for i := 1 to ParamCount do begin
    ProcessArgument(ParamStr(i));
  end;
end;

procedure TMainForm.ProcessArgument(const Arg: string);
begin
  ListBox1.Items.Add(Arg);
end;

procedure TMainForm.WMCopyData(var Message: TWMCopyData);
var
  Arg: string;
begin
  SetString(Arg, PChar(Message.CopyDataStruct.lpData), (Message.CopyDataStruct.cbData div SizeOf(Char))-1);
  ProcessArgument(Arg);
  Application.Restore;
  Application.BringToFront;
end;

end.

答案 1 :(得分:2)

逻辑就是这样的。启动应用程序时,将遍历正在运行的进程列表,并查看您的应用程序是否已在运行。如果它正在运行,则需要激活该实例的窗口然后退出。

您需要执行此操作的所有内容都在Windows API中。我在CodeProject.com上找到了处理进程的示例代码:

http://www.codeproject.com/KB/system/Win32Process.aspx

在查找和激活窗口时,基本方法是使用窗口类名称找到感兴趣的窗口,然后激活它。

http://www.vb6.us/tutorials/activate-window-api

希望这给你一个很好的起点。

答案 2 :(得分:0)

这里有很多答案说明如何实现这一点。我想说明为什么不使用FindWindow方法。

我正在使用FindWindow(类似于David H所示的那个),我看到它从Win10开始失败 - 我不知道他们在Win10中改变了什么。
我认为应用程序启动的时间与我们通过CreateParams设置唯一ID的时间之间的差距太大,因此另一个实例在某种程度上有时间在此间隙/间隔中运行。

想象一下,两个实例的起始距离仅为1毫秒(让我们说用户点击EXE文件,然后按下输入并在短时间内意外按下它)。两个实例都将检查是否存在具有该唯一ID的窗口,但是它们都没有机会设置标志/唯一ID,因为创建表单很慢并且仅在构造表单时设置唯一ID。因此,两个实例都将运行。

所以,我建议使用 CreateSemaphore 解决方案: https://stackoverflow.com/a/460480/46207
Marjan V已经提出了这个解决方案,但没有解释为什么它更好/更安全。

答案 3 :(得分:-1)

我会使用互斥锁。您在程序启动时创建一个。

创建失败时意味着另一个实例已在运行。然后,使用命令行参数向此实例发送消息并关闭。当您的应用程序收到带有命令行的消息时,它可以像您正在执行的那样解析参数,检查它是否已打开文件并继续进行。

处理此应用专用消息也是将应用程序放到前面的地方(如果尚未安装)。请礼貌地(SetForegroundWindow)这样做,而不是试图强迫你的应用程序在所有其他人面前。

function CreateMutexes(const MutexName: String): boolean;
// Creates the two mutexes to see if the program is already running.
//  One of the mutexes is created in the global name space (which makes it
//  possible to access the mutex across user sessions in Windows XP); the other
//  is created in the session name space (because versions of Windows NT prior
//  to 4.0 TSE don't have a global name space and don't support the 'Global\'
//  prefix).
var
  SecurityDesc: TSecurityDescriptor;
  SecurityAttr: TSecurityAttributes;
begin
  // By default on Windows NT, created mutexes are accessible only by the user
  //  running the process. We need our mutexes to be accessible to all users, so
  //  that the mutex detection can work across user sessions in Windows XP. To
  //  do this we use a security descriptor with a null DACL. 
  InitializeSecurityDescriptor(@SecurityDesc, SECURITY_DESCRIPTOR_REVISION);
  SetSecurityDescriptorDacl(@SecurityDesc, True, nil, False);
  SecurityAttr.nLength := SizeOf(SecurityAttr);
  SecurityAttr.lpSecurityDescriptor := @SecurityDesc;
  SecurityAttr.bInheritHandle := False;
  if (CreateMutex(@SecurityAttr, False, PChar(MutexName)) <> 0 )
  and (CreateMutex(@SecurityAttr, False, PChar('Global\' + MutexName)) <> 0 ) then
    Result := True
  else
    Result := False;
end;

initialization
  if not CreateMutexes('MyAppNameIsRunningMutex') then
    //Find and SendMessage to running instance
    ;
end.

注意:上面的代码是根据InnoSetup网站上的示例改编的。 InnoSetup创建安装程序应用程序,并在安装程序中使用此方法检查正在安装的应用程序(以前的版本)是否已在运行。

找到另一个实例并向其发送消息,我将留下另一个问题(或者您可以使用David的答案中的WM_COPYDATA方法)。实际上,有一个StackOverflow问题完全解决了这个问题:How to get the process thread that owns a mutex获取拥有互斥锁的进程/线程可能有点挑战,但这个问题的答案确实解决了从一个问题获取信息的方法实例到另一个。

答案 4 :(得分:-1)

Windows有不同的方法来处理与可执行文件的文件关联。

“命令行”方法只是最简单的方法,但也是最有限的方法。

它还支持DDE(虽然官方弃用它仍然有效)和COM(参见http://msdn.microsoft.com/en-us/library/windows/desktop/cc144171(v=vs.85).aspx)。

如果我没记错,DDE和COM都会让您的应用程序收到所选文件的完整列表。

答案 5 :(得分:-1)

我自己使用了窗口/消息方法,添加了用于跟踪另一个实例是否正在运行的事件:

  1. 尝试创建活动&#34; Global \ MyAppCode&#34; (&#34; Global&#34;命名空间用于处理各种用户会话,因为我需要系统范围内的单个实例;在您的情况下,您可能更喜欢&#34; Local&#34;命名空间由默认值)
  2. 如果CreateEvent返回错误并且GetLastError = ERROR_ALREADY_EXISTS则实例已在运行。
  3. FindWindow / WM_COPYDATA将数据传输到该实例。
  4. 但消息/窗口的缺点非常重要:

    1. 您必须始终保持窗口的标题不变。否则,您必须列出系统中的所有窗口并循环遍历它们以部分出现某些常量部分。此外,用户或第三方应用程序可以轻松更改窗口的标题,因此搜索失败。
    2. 方法需要创建一个窗口,因此不需要控制台/服务应用程序,或者它们必须创建一个窗口并执行消息循环,尤其是处理单个实例。
    3. 我不确定FindWindow是否可以找到在另一个用户会话中打开的窗口
    4. 对我来说,WM_COPYDATA是一种相当尴尬的方法。
    5. 所以目前我是命名管道方法的粉丝(虽然还没有实现它)。

      1. 启动时,应用会尝试连接到&#34; Global \ MyAppPipe&#34;。如果成功,其他实例正在运行。如果失败,则会创建此管道并完成实例检查。
      2. 第二个实例将所需数据写入管道并退出。
      3. 第一个实例接收数据并做一些事情。
      4. 它适用于所有用户会话(带有命名空间&#34; Global&#34;)或仅用于当前会话;它不依赖于UI使用的字符串(没有本地化和修改问题);它适用于控制台和服务应用程序(您需要在单独的线程/消息循环中实现管道读取)。