Предотвращение нескольких экземпляров - но также обрабатывать параметры командной строки?

Я обрабатываю файлы связанных с ним приложений из 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, которые будут открыты, просто сфокусируются на одном и только экземпляре, и я обрабатываю файлы как обычно

Спасибо

UPDATE

Я нашел здесь хорошую статью: http://www.delphidabbler.com/articles?article=13&part=2, который, как мне кажется, мне нужен, и показывает, как работать с Windows API как упомянутый rhooligan. Я собираюсь прочитать это сейчас.

Ответ 1

Вот простой пример кода, который выполняет задание. Надеюсь, это понятно.

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.

Ответ 2

Логика идет примерно так. Когда вы запускаете приложение, вы перебираете список запущенных процессов и смотрите, запущено ли ваше приложение. Если он запущен, вам нужно активировать окно этого экземпляра, а затем выйти.

Все, что вам нужно сделать, это в Windows API. Я нашел этот пример кода на CodeProject.com, который занимается процессами:

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

При обнаружении и активации окна основной подход заключается в том, чтобы найти интересующее окно, используя имя класса окна, а затем активировать его.

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

Надеюсь, это даст вам хорошую отправную точку.

Ответ 3

Здесь много ответов, в которых показано, как реализовать это. Я хочу показать, почему НЕ использовать подход FindWindow.

Я использую FindWindow (что-то похожее на то, что показано Дэвидом Х), и я видел, что это не сработало с Win10 - я не знаю, что они изменили в Win10.
Я думаю, что разрыв между временем начала приложения и временем, когда мы устанавливаем уникальный идентификатор через CreateParams, слишком велик, поэтому другой экземпляр имеет какое-то время для запуска в этом промежутке/интервале.

Представьте, что два экземпляра запускались всего на расстоянии 1 мс (предположим, что пользователь нажимает на EXE файл, а затем нажимает кнопку ввода и удерживает его нажатой случайно на короткое время). Оба экземпляра будут проверять, существует ли окно с этим уникальным идентификатором, но ни один из них не имел возможности установить флаг/уникальный идентификатор, поскольку создание формы выполняется медленно, а уникальный идентификатор устанавливается только при создании формы. Таким образом, оба экземпляра будут работать.

Итак, я бы рекомендовал вместо этого решение CreateSemaphore: fooobar.com/questions/196473/...
Марьян V уже предложил это решение, но не объяснил, почему он лучше/безопаснее.

Ответ 4

Я бы использовал мьютексы. Вы создаете его, когда запускается ваша программа.

Когда создание не выполняется, это означает, что еще один экземпляр уже запущен. Затем вы отправляете этому экземпляру сообщение с параметрами командной строки и закрываете. Когда ваше приложение получает сообщение с командной строкой, оно может анализировать параметры, как вы уже делали, проверить, есть ли файл уже открыт и действовать соответственно.

Обработка этого приложения конкретным сообщением ia также место, чтобы получить ваше приложение на фронт, если он еще не был. Пожалуйста, сделайте это вежливо (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 создает приложения-установщики и использует этот подход в установщике для проверки того, установлена ​​ли уже предыдущая версия приложения, которое установлено.

Находя другой экземпляр и отправляя ему сообщение, я уйду на другой вопрос (или вы можете использовать метод WM_COPYDATA из ответа Дэвида). На самом деле есть вопрос StackOverflow, который имеет прямое отношение к этому: Как получить поток процесса, который владеет мьютексом Получение процесса/потока, который владеет мьютексом, может быть немного вызов, но ответы на этот вопрос касаются способов получения информации от одного экземпляра к другому.

Ответ 5

У Windows есть разные способы обработки ассоциаций файлов с исполняемыми файлами.

Подход "командной строки" является только самым простым, но также и самым ограниченным.

Он также поддерживает DDE (он все еще работает, хотя официально устарел) и COM (см. http://msdn.microsoft.com/en-us/library/windows/desktop/cc144171(v=vs.85).aspx).

Если я правильно напомню, что DDE и COM позволят вашему приложению получить весь список выбранных файлов.

Ответ 6

Я сам использовал метод window/message с добавлением событий для отслеживания, если выполняется другой экземпляр:

  • Попробуйте создать событие "Global\MyAppCode" ( "Глобальное" пространство имен используется для обработки различных пользовательских сеансов, поскольку мне нужен единый экземпляр по всей системе; в вашем случае вы, вероятно, предпочтете "локальное" пространство имен, которое устанавливается по умолчанию)
  • Если CreateEvent вернула ошибку и GetLastError = ERROR_ALREADY_EXISTS, то экземпляр уже запущен.
  • FindWindow/WM_COPYDATA для передачи данных в этот экземпляр.

Но недостатки с сообщениями/окнами более чем значительны:

  • Вы всегда должны постоянно сохранять свою подпись Caption. В противном случае вам нужно будет перечислить все окна в системе и пропустить их для частичного появления некоторой постоянной части. Кроме того, заголовок окна можно легко изменить с помощью приложения пользователя или третьей части, чтобы поиск не удался.
  • Метод требует создания окна, поэтому никаких консольных/сервисных приложений, или они должны создавать окно и выполнять цикл сообщений, особенно для обработки одного экземпляра.
  • Я не уверен, что FindWindow может найти окно, открытое в другом сеансе пользователя.
  • Для меня WM_COPYDATA - довольно неудобный метод.

Итак, в настоящее время я являюсь поклонником подхода с именованным каналом (еще не реализовал его).

  • При запуске приложение пытается подключиться к "Global\MyAppPipe". В случае успеха выполняется другой экземпляр. Если это не удается, он создает этот канал и завершает проверку экземпляра.
  • Второй экземпляр записывает необходимые данные в канал и выходы.
  • 1-й экземпляр получает данные и делает некоторые вещи.

Он работает через все сеансы пользователя (с пространством имен "Глобальный" ) или только с текущего сеанса; он не зависит от строк, используемых UI (без проблем с локализацией и модификацией); он работает с консольными и сервисными приложениями (вам нужно будет выполнять чтение канала в отдельном потоке/контуре сообщения).