Не надо мне другую! Дайте мне ту же самую!

В прежние времена, когда на PC использовалась MS DOS, запустить работать параллельно несколько программ средствами операционной системы не представлялось возможным. С появлением Windows 3.0 ситуация изменилась. Уже режим не вытесняющей многозадачности, который был реализован в 16-разрядной ее версии, позволял запускать параллельно несколько программ или несколько экземпляров одной и той же программы при достаточном количестве ресурсов (хотя это и была, по большому счету, псевдомногозадачная надстройка над однозадачной DOS с графическим интерфейсом). Ну а при работе под управлением современными версиями Windows в режиме вытесняющей многозадачности и обилии аппаратных ресурсов о параллельной работе программ задумываются разве что программисты, настолько это обыденно для пользователя. Вот с точки зрения программиста и посмотрим на этот аспект.

При всех прелестях многозадачности логика работы программы может не допускать ее запуска в нескольких экземплярах. Это могут быть программы, которые работают с ресурсами, конкуренция за которые нежелательна или не допустима. В качестве примера можно привести, например, почтовых клиентов Microsoft Outlook и Outlook Express. В некоторых случаях возможность запуска только одной копии программы диктует логика ее работы. Сюда можно отнести "резидентно" запущенные органайзеры, браузер Opera (когда он работает в режиме фрейма). Некоторые программы допускают настройку своей реакции на запуск второй копии. Так что, в конечном итоге, все определяется поставленными целями. В силу этого для программиста может встать задача предотвращения запуска второй копии программы. И, оказывается, решение этой задачи в 32-разрядной Windows имеет свои особенности.

Как это реализовать? Работа разделяется на две части. В первую очередь, надо выяснить, был ли уже запущен экземпляр программы. Если такой факт установлен, то дальше могут быть варианты. Можно просто прекратить работу второй копии. Этот вариант подходит для тех программ, которые не предполагают интерактивной работы с пользователем, т.е. не предполагают создания визуальным образом окон, в которых пользователь производит некоторые действия с помощью клавиатуры или мыши. Обычно это какие-то сервисные модули, и маловероятно, что рядовой пользователь доберется до их запуска вручную, но на это полагаться, мягко говоря, неразумно. Если же интерактивность предполагается, то пользователь может быть введен в заблуждение - программа, которая нормально запускалась, почему-то не запускается (т.е. ее окно не появляется). Конечно, поковырявшись, он может догадаться, что она уже была запущена и потому при повторном запуске нет никакой реакции, и ее нужно искать самостоятельно, например, на панели задач. Но, согласитесь, это весьма "недружественное" решение. Можно перед завершением второй копии выдать пользователю сообщение наподобие: "Данная программа уже запущена. Используйте Панель Задач для переключения". Это уже гораздо лучше, чем описанный первый вариант, однако он требует от пользователя совершения определенных действий. Самым лучшим, видимо, является решение, когда второй экземпляр завершается, а первый активизируется (или наоборот), т.е. если первый экземпляр был свернут или закрыт окнами других программ, то он разворачивается, а окно программы появляется поверх остальных окон и получает фокус.

Вначале разберемся с первой задачей. Поиск запущенного приложения по имени или классу ее окна (функция API FindWindow) ненадежен (а иногда и невозможен, если программа не создает окна), поскольку необязательно программа в данный момент имеет окно, например, происходит процесс инициализации, который может быть достаточно длительным в силу тех или иных причин (кроме этого, существует вероятность того, что название окна или класса окна, по которым ведется поиск, окажутся такими же для совершенно иной программы, поэтому, если уж принимается решение о ее использовании, нужно позаботиться об уникальности этих наименований). Кроме этого, в процессе работы программы наименование окна может изменяться.

Следовательно, желателен другой уникальный признак для установления факта того, что программа уже запущена. Первый экземпляр приложения при своем запуске должен установить этот признак, а при завершении работы его сбросить. При этом требуется, чтобы такой признак был сброшен даже в случае краха приложения. Для этих целей удобным является использование объектов ядра операционной системы - мьютексов (mutex), семафоров (semaphore), файлмэппинга (file mapping), счетчики ссылок которых управляются операционной системой.

 

Самым простым способом является использование мьютекса - взаимного исключения. Обнаружение второго экземпляра приложения определяется невозможностью создания мьютекса с таким же именем во втором экземпляре (Листинг 1).

Теперь рассмотрим решение задачи активизации предыдущего экземпляра. Это равносильно активации его главного окна, для чего нужно провести некоторые манипуляции с использованием handle. Для этого его сначала надо получить. И здесь также возможны варианты. Поскольку первая задача - обнаружение запущенного экземпляра - выполнена, можно воспользоваться "отвергнутой" функцией FindWindow для поиска главного окна приложения. Поскольку наименование его может изменяться в процессе работы программы (например, в нем может отображаться наименование загруженного файла), то искать следует по наименованию класса окна. Для того, чтобы исключить возможное совпадение имени класса с именем класса окна других программ, придется несколько усложнить наименование класса окна. Как это сделать, вопрос личного предпочтения.

В случае, если окно найдено (его handle <> 0), можно заставить его развернуться (если оно свернуто) и переместиться поверх остальных окон. И здесь придется сказать о специфике приложения, создаваемого инструментарием от Borland. Дело в том, что главное окно программы и главное окно приложения в них - вещи разные. Окно приложения управляет главным окном, само имея при этом нулевые размеры (длина=ширине=0). Поэтому, если приложение было свернуто, то и разворачивать надо его окно, а не главное окно программы (которое при этом будет показано автоматически). В Листинге 2 использована следующая техника. Второй экземпляр отправляет найденному главному окну первого экземпляра сообщение и выгружается, а первый экземпляр его обрабатывает. Главное - чтобы экземпляры "понимали" друг друга, т.е. они должны оперировать одним и тем же сообщением. При получении такого сообщения восстанавливается (развертывается) приложение (если оно было свернуто), передается фокус главному окну и оно перемещается на передний план. В случае, если пользователь подряд осуществил запуск приложения, может оказаться, что мьютекс создан, главное окно создано, но еще не видимо. В этом случае возникнет exception при попытке передачи фокуса невидимому окну, который и отсекается пустой секцией обработки исключений (для упрощения, исключение можно отработать и явно). Участок кода приведен в Листинге 2.

Можно задаться целью не дергать первый экземпляр, а всю работу по активации выполнить во втором экземпляре при попытке его старта. Для этого нужно выяснить handle окна приложения первого экземпляра. При этом придется использовать иную технику, например, упомянутый файлмэппинг. Помимо факта запуска приложения и значения handle приложения, можно передавать также информацию о количестве запущенных экземпляров, так что можно по-другому управлять их количеством - например, не более некоторого значения (последнее также реализуемо с помощью семафоров), о загруженном предыдущим экземпляром файле и пр.

В заключение следует отметить, что в приведенных примерах проверка производится в теле проекта, т.е. после выполнения секций initialization всех модулей, входящих в проект. Для больших проектов это может быть достаточно длительный процесс, к тому же совершенно бесполезный, если экземпляр все равно будет затем выгружен. Кроме того, бесполезная работа по инициализации может быть не самым плохим вариантом, поскольку ошибочный запуск второго экземпляра может привести, например, к некорректному изменению или потере данных вашим приложением, что гораздо хуже. Поэтому имеет смысл поместить проверку в отдельный модуль и выполнять ее в разделе initialization, а сам модуль поместить первым в списке модулей проекта. Управлять прекращением работы программы в этом случае придется инструкцией halt.

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

Юрий А. СМАНЦЕР,
georgesman@mail.ru

Листинг 1. Фрагмент файла проекта Test.dpr
var
 theMutex: HWND = 0;
 PrevWnd: HWND = 0;
 NotFirstInstance: Boolean;
begin
 theMutex:= CreateMutex(nil, False, PChar('MyMainMutex_100'));
 NotFirstInstance:= (GetLastError = ERROR_ALREADY_EXISTS)
  or (theMutex = 0);
 if NotFirstInstance then // если это не первая копия
  begin
  CloseHandle(theMutex);
  theMutex:= 0;
  PrevWnd:= FindWindow(PChar('TMyMainForm_100'), nil);
  if PrevWnd <> 0 then
   begin
   SendMessage(PrevWnd, WM_USER, 0, 0);
   exit;
   end;
  end;
 Application.Initialize;
 Application.CreateForm(TMyMainForm_100, MyMainForm_100);
 Application.Run;
 CloseHandle(theMutex);
 theMutex:= 0;
end.
Листинг 2. Фрагмент файла формы TMyMainForm_100 MainFrm.pas.
type
 TMyMainForm_100 = class(TForm)
 ...
private
procedure WMUser(var Msg: TMessage); message WM_USER;
 ...
end;
...
procedure TMyMainForm_100.WMUser(var Msg: TMessage);
 begin
 Application.Restore;
 try
 Self.SetFocus;
 SetForegroundWindow(Self.Handle);
 except
 // отсекаем исключение, когда окно еще невидимо
 end;
end;
Версия для печатиВерсия для печати

Номер: 

15 за 2003 год

Рубрика: 

Software
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!
 

Комментарии

Аватар пользователя Killer{R}
В в2к SetForegroundWindow может блокироваться на время запуска новой программы (окно начинает мигать на таскбаре, но не выходит наверх), чтобы избежать этого я делаю обычно так: "новая" прога получает хэндл окна "старой" и шлет ему контрольное сообщение, старая программа делает Restore себе (Application->Restore()) либо через WM_SYSCOMMAND, возвращает хэндл своего окна, новая получив из SendMessage этот хэндл делает Sleep(0) и затем делает SetForegroundWindow на него и тихо мирно - ExitProcess.