<Имя программы> "%1" v
Программа должна анализировать переданные ей параметры и открывать соответствующий файл в требуемом режиме. В нашем случае этот код выглядит очень просто (листинг 1.47).
Листинг 1.47. Анализ командной строки
procedure TDKSViewMainForm.FormShow(Sender: TObject);
var
OpenForView: Bооlean;
begin
// Проверяем наличие ключа "/v" в качестве второго параметра
OpenForView := (ParamCount > 1) and (CompareText(ParamStr(2), '/v') = 0);
if ParamCount > 0 then OpenFile(ParamStr(1), OpenForView);
...
end;
B более сложных случаях (например, при большем числе команд для ассоциированного файла) анализ командной строки будет сложнее, но его принципы останутся теми же.
1.3.2.3. Поиск уже запущенной копии приложения
Во многих случаях желательно не давать пользователю возможности запустить второй экземпляр вашего приложения. В 16-разрядных версиях Windows все приложения выполнялись в одной виртуальной машине, и каждому из них через переменную HPrevInstance передавался дескриптор предыдущей копии. По значению HPrevInstance программа легко могла найти свой предыдущий экземпляр или определить, что других экземпляров нет, если HPrevInstance равна нулю. В 32-разрядных версиях эта переменная для совместимости оставлена, но всегда равна нулю, т.к. предыдущая копия работает в своей виртуальной машине, и ее дескриптор не имеет смысла. Альтернативного механизма обнаружения уже запущенной копии система не предоставляет, приходится выкручиваться своими силами.
Для обнаружения уже запущенного приложения многие авторы предлагают использовать именованные системные объекты (мьютексы, семафоры, атомы и т.п.). При запуске программа пытается создать такой объект с определенным именем. Если оказывается, что такой объект уже создан, программа "понимает", что она — вторая копия, и завершается. Недостаток такого подхода — с его помощью можно установить только сам факт наличия предыдущей копии, но не более того. В нашем случае задача шире: при запуске второго экземпляра приложения должен активизироваться первый, а если второму экземпляру была передана непустая командная строка, первый должен получить эту строку и выполнить соответствующее действие, поэтому описанный способ нам не подходит.
Для решения задачи нам подойдут почтовые ящики (mailslots). Это специальные системные объекты для односторонней передачи сообщений между приложениями (ничего общего с электронной почтой эти почтовые ящики не имеют). Под сообщением здесь понимаются не сообщения Windows, а произвольный набор данных (здесь больше подходит скорее термин "дейтаграмма", а не "сообщение"). Каждый почтовый ящик имеет уникальное имя. Алгоритм отслеживания повторного запуска с помощью почтового ящика следующий. Сначала программа пытается создать почтовый ящик как сервер. Если оказывается, что такой ящик уже существует, то она подключается к нему как клиент и передает содержимое своей командной строки и завершает работу. Сервером в таком случае становится экземпляр приложения, запустившийся первым, — он-то и создаёт почтовый ящик. Остальным экземплярам останется только передать ему данные.
Примечание
В случае аварийного завершения программы система сама закроет все открытые ею дескрипторы, поэтому даже если первая копия будет снята системой и не сможет корректно закрыть дескриптор почтового ящика, ящик будет уничтожен и не помешает пользователю запустить новую копию программы.
Почтовый ящик лучше создать как можно раньше, поэтому мы будем его создавать не в методе формы, а в основном коде проекта, который обычно программист не исправляет. В результате код в dpr-файле проекта будет выглядеть так, как показано в листинге 1.48.
Листинг 1.48 Создание почтового ящика в главном файле проекта
const
MailslotName = '\.mailslotDelphiKingomSample_Viewer_FileCommand';
EventName = 'DelphiKingdomSamplе_Viewer_Command_Event';
var
ClientMailslotHandle: THandle;
Letter: string;
OpenForView: Boolean;
BytesWritten: DWORD;
begin
// Пытаемся создать почтовый ящик
ServerMailslotHandle := CreateMailSlot(MailslotName, 0,
MAILSLOT_WAIT_FOREVER, nil);
if ServerMailslotHandle = INVALID_HANDLE_VALUE then
begin
if GetLastError = ERROR_ALREADY_EXISTS then
begin
// Если такой ящик уже есть, подключаемся к нему, как клиент
ClientMailslotHandle := CreateFile(MailslotName, GENERIC_WRITE,
FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
// В зависимости от того, какие переданы параметры, формируем
// строку для передачи предыдущему экземпляру. Первый символ
// строки - команда:
// e - открыть файл для редактирования
// v — открыть файл для просмотра
// s — просто активизировать предыдущий экземпляр
// Для команд e и v к строке, начиная со 2-го символа,
// добавляется имя файла
if ParamCount > 0 then
begin
OpenForView := (ParamCount > 1) and
(CompareText(ParamStr(2), '/v') = 0);
if OpenForView then Letter := 'v' + ParamStr(1)
elsе Letter := 'e' + ParamStr(1);
end
else Letter := 's';
// Отправляем команду в почтовый ящик
WriteFile(ClientMailslotHandle, Letter[1], Length(Letter),
BytesWritten, nil);
// Сигнализируем об отправке данных через специальное событие
CommandEvent := OpenEvent(EVENT_MODIFY_STATE, False, EventName);
SetEvent(CommandEvent);
// Закрываем все дескрипторы
CloseHandle(CommandEvent);
CloseHandle(ClientMailslotHandle);
end;
end
else
begin
// Создаем событие для сигнализирования о поступлении данных
CommandEvent := CreateEvent(nil, False, False, EventName);
// Выполняем обычный для VCL-приложений цикл
Application.Initialize;
Application.CreateForm(TDKSViewMainForm, DKSViewMainForm);
Application.Run;
// Закрываем все дескрипторы
CloseHandle(ServerMailslotHandle);
CloseHandle(CommandEvent);
end;
end.
Теперь осталось "научить" первую копию приложения обнаруживать момент, когда в почтовом ящике оказываются сообщения, и забирать их оттуда. Было бы идеально, если при поступлении данных главная форма получала бы какое-то сообщение, но готового такого механизма, к сожалению, не существует. Из положения можно выйти, задействовав события.
Примечание
События — это объекты синхронизации, использующиеся в системе. Событие может быть взведено и сброшено. С помощью функции WaitForSingleObject можно перевести нить в состояние ожидания до тех пор. пока указанное событие не будет взведено. Подробное рассмотрение объектов синхронизации выходит за рамки нашей книги; они детально описаны, например, в [2].
В принципе, при использовании перекрытого ввода-вывода система может сама взводить указанное программой событие при получении данных почтовым ящиком, но перекрытый ввод-вывод имеет ограниченную поддержку в Windows 9х/МЕ и на почтовые ящики не распространяется. Чтобы приложение могло работать не только в Windows NT/2000/XP, мы не будем применять перекрытый ввод-вывод.
События относятся к именованным объектам, поэтому с их помощью можно синхронизировать разные процессы. В нашем случае первая копия приложения с помощью CreateEvent создает событие, а последующие копии с помощью OpenEvent получают дескриптор этого события и взводят его. чтобы послать сигнал о появлении данных в почтовом ящике. Для обнаружения этого момента в первой копии приложения создается отдельная нить, которая ожидает событие и, дождавшись, посылает главной форме сообщение (эта нить практически не требует процессорного времени, потому что почти все время находится в режиме ожидания, т.е. квант времени планировщик задач ей не выделяет, по крайней мере, проверка наличие данных в главной нити по таймеру отняла бы больше ресурсов). Это сообщение определяется пользователем и берется из диапазона WM_USER, т.к. его широковещательной рассылки не будет. При получении этого сообщения форма выполняет код, приведенный в листинге 1.49.
Листинг 1.49. Реакция формы на поступление данных в почтовый ящик
// Реакция на получение команд от других экземпляров приложения
procedure TDKSViewMainForm.WMCommandArrived(var Message: TMessage);
var
Letter: string;
begin
// Переводим приложение на передний план
GoToForeground;
// Пока есть команды, читаем их и выполняем
Letter := ReadStringFromMailslot;