Антон Григорьев дата публикации 20-09-1999 00:00 Обобщающие примеры работы с WinAPI. Пример №3
Более полный вариант этой статьи вошёл в книгу "О чём не пишут в книгах по Delphi"
Расширения файлов могут быть связаны (ассоциированы) с определённой программой. Такие ассоциации помогают системе выбрать программу для выполнения различных действий с файлом из Проводника. Так, например, если на компьютере установлен Microsoft Office, двойной щелчок в Проводнике на файле с расширением xls приведёт к запуску Microsoft Excel и открытию файла в нём. Это происходит потому, что расширение xls ассоциировано с приложением Microsoft Excel.
Примечание: Добиться аналогичного эффекта в своей программе можно, используя функцию ShellExecute (стандартная системная функция, в Delphi импортируется в модуле ShellAPI). Эта функция запускает файл, имя которого передано ей как параметр. Если это исполнимый файл, он запускается непосредственно, если нет — функция ищет ассоциированное с расширением файла приложение и открывает файл в нём.
Пример, который мы здесь рассмотрим (программа DKSView), умеет ассоциировать файлы с расширением dks с собой, а также проверять, не были ли они ассоциированы с другим приложением. DKSView является MDI-приложением, т.е. может открывать одновременно несколько файлов. Если приложение уже запущено, а пользователь пытается открыть ещё один dks-файл, желательно, чтобы он открывался не в новом экземпляре DKSView, а появлялось новое окно в уже имеющемся. Поэтому наш пример будет также уметь обнаруживать уже запущенный экземпляр программы и переадресовывать открытие файла ему.
Файловые ассоциации прописываются в реестре, в разделе HKEY_CLASSES_ROOT. Чтобы связать расширение с приложением, необходимо выполнить следующие действия:
- В корне раздела HKEY_CLASSES_ROOT нужно создать раздел, имя которого совпадает с расширением с точкой перед ним (в нашем случае это будет раздел с именем ".dks"). В качестве значения по умолчанию в этот раздел должна быть записана непустая строка, которая будет идентифицировать соответствующий тип файла. Содержимое этой строки может быть произвольным и определяется разработчиком (в нашем случае эта строка имеет значение "DKS_View_File").
- Далее в корне раздела HKEY_CLASSES_ROOT следует создать раздел, имя которого совпадает со значением ключа из предыдущего пункта (т.е. в нашем случае — с именем "DKS_View_File"). В качестве значения по умолчанию для этого ключа нужно поставить текстовое описание типа (это описание будет показываться пользователю в Проводнике в качестве типа файла).
- В этом разделе создать подраздел Shell, в нём — подраздел Open, а в нём — подраздел Command, значением по умолчанию которого должна стать командная строка для запуска файла. Имя файла в ней заменяется на %1 (подробнее о командной строке чуть ниже).
- Описанных выше действий достаточно, чтобы система знала, как правильно открывать файл из Проводника или с помощью ShellExecute. Однако правила хорошего тона требуют, чтобы с файлом была ассоциирована также иконка, которую будет отображать рядом с ним Проводник. Для этого в разделе, созданном во втором пункте, нужно создать подраздел "DefaultIcon" и в качестве значения по умолчанию задать ему имя файла, содержащего иконку. Если это ico-файл, содержащий только одну иконку, к имени файла ничего добавлять не надо. Если иконка содержится в файле, в котором может быть несколько иконок (например, в exe или dll), после имени файла нужно поставить запятую и номер требуемой иконки (иконки нумеруются с нуля).
Приведённый выше список — это самый минимальный набор действий, необходимых для ассоциирования расширения с приложением. Вернёмся к третьему пункту. Имя подраздела "Open" задаёт команду, связанную с данным расширением, т.е. в данном случае — команду "Open". В разделе Shell можно сделать несколько аналогичных подразделов — в этом случае с файлом будет связано несколько команд. У функции ShellExecute есть параметр lpOperation, в котором задаётся имя требуемой команды. Пользователь проводника может выбрать одну из возможных команд через контекстное меню, которое появляется при нажатии правой кнопки мыши над файлом. Существует возможность установить для этих пунктов меню более дружественные имена. Для этого нужно задать значение по умолчанию соответствующего подраздела. В этой строке можно использовать символ "&" для указания горячей клавиши, аналогично тому как это делается, например, в компоненте TButton.
Если в ShellExecute команда не указана явно, используется команда по умолчанию (то же самое происходит при двойном щелчке на файле в Проводнике). Если не оговорено обратное, командой по умолчанию является команда "Open" или, если команды "Open" нет, первая команда в списке. При необходимости можно задать другую команду по умолчанию. Для этого нужно указать её название в качестве значения по умолчанию раздела Shell.
В нашем примере будет две команды: open (открыть для редактирования) и view (открыть для просмотра). Поэтому информация в реестр заносится так:
const
FileExt = '.dks';
FileDescr = 'DKS_View_File';
FileTitle = 'Delphi Kingdom Sample file';
OpenCommand = '&Открыть';
ViewCommand = '&Просмотреть';
procedure TDKSViewMainForm.SetAssociation(Reg: TRegistry);
begin
Reg.OpenKey('\' + FileExt, True);
Reg.WriteString('', FileDescr);
Reg.OpenKey('\' + FileDescr, True);
Reg.WriteString('', FileTitle);
Reg.OpenKey('Shell', True);
Reg.OpenKey('Open', True);
Reg.WriteString('', OpenCommand);
Reg.OpenKey('command', True);
Reg.WriteString('', '"' + ParamStr(0) + '" "%1"');
Reg.OpenKey('\' + FileDescr, True);
Reg.OpenKey('Shell', True);
Reg.OpenKey('View', True);
Reg.WriteString('', ViewCommand);
Reg.OpenKey('command', True);
Reg.WriteString('', '"' + ParamStr(0) + '" "%1" /v');
Reg.OpenKey('\' + FileDescr, True);
Reg.OpenKey('DefaultIcon', True);
Reg.WriteString('', ParamStr(0) + ',0');
end;
|
|
Командная строка досталась Windows в наследство от DOS. Там основным средством общения пользователя с системой был ввод команд с клавиатуры. Команда запуска приложения выглядела так:
<Имя приложения> <Строка параметров>
|
|
Строка параметров — это произвольная строка, которая без изменений передавалась программе. От имени программы она отделялась пробелом (пробелы в именах файлов и директорий в DOS не допускались). Разработчик конкретного приложения мог, в принципе, интерпретировать эту строку как угодно, но общепринятым стал способ, когда строка разбивалась на отдельные параметры, которые разделялись пробелами. Вид и смысл параметров зависел от конкретной программы. В качестве параметров нередко передавались имена файлов, с которыми должна была работать программа.
В Windows мало что изменилось — функции CreateProcess и ShellExecute, запускающие приложение, по-прежнему используют понятие командной строки. Разве что теперь максимальная длина строки стала существенно больше, и командную строку можно получить в кодировке Unicode. Но по-прежнему разделителем параметров считается пробел. Однако теперь пробел может присутствовать и в имени файла — как в имени самой программы, так и в именах файлов, передаваемых в качестве параметров. Чтобы отличать такой пробел от пробела-разделителя, параметры, содержащие пробелы, заключаются в двойные кавычки. Если имя программы содержит пробелы, они тоже заключаются в двойные кавычки. И, конечно же, если в кавычки окажется заключенным параметр, в котором нет пробелов, хуже от этого не будет.
Для работы с параметрами командной строки в Delphi существуют две стандартные функции: ParamCount и ParamStr. ParamCount возвращает количество параметров, переданных в командной строке, ParamStr — параметр с заданным порядковым номером. Параметры нумеруются начиная с единицы; нулевым параметром считается имя самой программы (при подсчётах с помощью ParamCount этот "параметр" не учитывается). Эти функции осуществляют разбор командной строки по описанным выше правилам: разделитель — пробел, за исключением заключённых в кавычки. Кавычки, в которые заключён параметр, функция ParamStr не возвращает.
Для запуска ассоциированного файла используется механизм командной строки. В реестр записывается командная строка (вместе с именем приложения), в которой имя открываемого файла заменяется на "%1". Когда пользователь запускает ассоциированный файл (или он запускается приложением через ShellExecute), система извлекает из реестра соответствующую командную строку, вместо "%1" подставляет реальное имя файла и пытается выполнить получившуюся команду. Отметим, что если имя файла содержит пробелы, в кавычки оно автоматически не заключается, поэтому о кавычках приходится заботиться самостоятельно, заключая в них "%1". Таким образом, в реестр в качестве командной строки должно записываться следующее:
Если существуют разные варианты запуска одного файла (т.е. как в нашем случае — open и view), они могут различаться дополнительными параметрами. В частности, в нашем примере для открытия для редактирования не будут требоваться дополнительные параметры, для открытия для просмотра в качестве второго параметра должен передаваться ключ "/v", т.е. в реестр для этой команды будет записана такая строка:
Программа должна анализировать переданные ей параметры и открывать соответствующий файл в требуемом режиме. В нашем случае этот код выглядит очень просто:
procedure TDKSViewMainForm.FormShow(Sender: TObject);
var
OpenForView: Boolean;
begin
OpenForView := (ParamCount > 1) and (CompareText(ParamStr(2), '/v') = 0);
if ParamCount > 0 then
OpenFile(ParamStr(1), OpenForView);
TEventWaitThread.Create(False);
end;
|
|
В более сложных случаях (например, при большем числе команд для ассоциированного файла) потребуется более сложный анализ командной строки, но принципы этого анализа останутся те же.
Во многих случаях желательно не давать пользователю возможности запустить второй экземпляр вашего приложения. В 16-разрядных версиях Windows все приложения выполнялись в одной виртуальной машине, и каждому приложению через переменную HPrevInstance передавался дескриптор предыдущей копии. В 32-разрядных версиях эта переменная для совместимости оставлена, но всегда равна нулю, т.к. предыдущая копия работает в своей виртуальной машине, и её дескриптор не имеет смысла. Альтернативного механизма обнаружения уже запущенной копии система не предоставляет, приходится выкручиваться своими силами.
Для обнаружения уже запущенного приложения многие авторы предлагают использовать именованные системные объекты (мьютексы, семафоры, атомы и т.п.). При запуске программа пытается создать такой объект с определённым именем. Если оказывается, что такой объект уже создан, программа "понимает", что она — вторая копия, и завершается. Недостаток такого подхода — с его помощью можно установить только сам факт наличия предыдущей копии, но не более того. В нашем случае задача шире: при запуске второго экземпляра приложения должен активизироваться первый, а если второму экземпляру была передана непустая командная строка, первый должен получить эту строку и выполнить соответствующее действие, поэтому описанный выше способ нам не подходит.
Для решения задачи нам подойдут почтовые ящики (mailslots). Это специальные системные объекты для односторонней передачи сообщений между приложениями (ничего общего с электронной почтой эти почтовые ящики не имеют). Под сообщением здесь понимаются не сообщения Windows, а произвольный набор данных (здесь, скорее, следовало бы использовать термин "дейтаграмма", а не "сообщение"). Каждый почтовый ящик имеет уникальное имя. Алгоритм отслеживания повторного запуска с помощью почтового ящика следующий: сначала программа пытается создать почтовый ящик как сервер. Если оказывается, что такой ящик уже существует, она подключается к нему как клиент и передаёт содержимое своей командной строки и завершает работу. Сервером в таком случае становится экземпляр приложения, запустившийся первым — он-то и создаст почтовый ящик. Остальным экземплярам останется только передать ему данные.
Примечание: В случае аварийного завершения программы система сама закроет все открытые ей дескрипторы, поэтому даже если первая копия будет снята системой и не сможет корректно закрыть дескриптор почтового ящика, ящик будет уничтожен и не помешает пользователю запустить новую копию программы.
Почтовый ящик лучше создать как можно раньше, поэтому мы будем его создавать не в методе формы, а в основном коде проекта, который обычно программист не исправляет. В результате код в dpr-файле проекта будет выглядеть так:
const
MailslotName = '\\.\mailslot\DekphiKingdomSample_Viewer_FileCommand';
EventName = 'DelphiKingdomSample_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);
if ParamCount > 0 then
begin
OpenForView := (ParamCount > 1) and (CompareText(ParamStr(2), '/v') = 0);
if OpenForView then
Letter := 'v' + ParamStr(1)
else
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);
Application.Initialize;
Application.CreateForm(TDKSViewMainForm, DKSViewMainForm);
Application.Run;
CloseHandle(ServerMailslotHandle);
CloseHandle(CommandEvent);
end;
end.
|
|
Теперь осталось научить первую копию приложения обнаруживать момент, когда в почтовом ящике оказываются сообщения, и забирать их оттуда. Было бы идеально, если бы при поступлении данных главная форма получала бы какое-то сообщение, но готового такого механизма, к сожалению, не существует. Из положения можно выйти, используя события.
Примечание: События — это объекты синхронизации, использующиеся в системе. Событие может быть взведено и сброшено. С помощью функции WaitForSingleObject можно перевести нить в состояние ожидания до тех пор, пока указанное событие не будет взведено. Подробное рассмотрение объектов синхронизации выходит за рамки этой статьи.
В принципе, при использовании перекрытого ввода-вывода система может сама взводить указанное событие при получении данных почтовым ящиком, но перекрытый ввод-вывод имеет ограниченную поддержку в Windows 9x/ME и на почтовые ящики не распространяется. Чтобы приложение могло работать не только в Windows NT/2000/XP, мы не будем использовать перекрытый ввод-вывод.
События относятся к именованным объектам, поэтому с их помощью можно синхронизировать разные процессы. В нашем случае первая копия приложения с помощью CreateEvent создаёт событие, а последующие копии с помощью OpenEvent получают дескриптор этого события и взводят его, чтобы послать сигнал о появлении данных в почтовом ящике. Для обнаружения этого момента в первой копии приложения создаётся отдельная нить, которая ожидает событие и, дождавшись, посылает главной форме сообщение (эта нить практически не требует процессорного времени, потому что почти всё время находится в режиме ожидания, т.е. квант времени планировщик задач ей не выделяет; по крайней мере, проверка наличия данных в главной нити по таймеру отняла бы больше ресурсов). Это сообщение определяется пользователем и берётся из диапазона WM_USER, т.к. его широковещательной рассылки не будет. При получении этого сообщения форма выполняет следующий код:
procedure TDKSViewMainForm.WMCommandArrived(var Message: TMessage);
var
Letter: string;
begin
GoToForeground;
Letter := ReadStringFromMailslot;
while Letter <> '' do
begin
case Letter[1] of
'e': OpenFile(Copy(Letter, 2, MaxInt), False);
'v': OpenFile(Copy(Letter, 2, MaxInt), True);
end;
Letter := ReadStringFromMailslot;
end;
end;
function TDksViewMainForm.ReadStringFromMailslot: string;
var
MessageSize: DWORD;
begin
GetMailslotInfo(ServerMailslotHandle, nil, MessageSize, nil, nil);
if MessageSize = MAILSLOT_NO_MESSAGE then
begin
Result := '';
Exit;
end;
SetLength(Result, MessageSize);
ReadFile(ServerMailslotHandle, Result[1], MessageSize, MessageSize, nil);
end;
|
|
Примечание: Так как события являются именованными объектами, второй экземпляр приложения мог бы обнаруживать наличие первого не по почтовому ящику, а по событию. Более того, если бы нам не нужно было бы передавать данные первому экземпляру, а только активизировать его, можно было бы вообще обойтись одним только событием.
Первая копия приложения, получив команду от другой копии, должна вывести себя на передний план. Казалось бы, всё просто: с помощью функции SetForegroundWindow мы можем вывести туда любое окно. Однако так было только до Windows 95 и NT 4. В более поздних версиях введены ограничения, и теперь программа не может вывести себя на передний план по собственному усмотрению. Функция SetForegroundWindow просто заставит мигать соответствующую кнопку на панели задач.
Тем не менее, если программа свёрнута, команда Application.Restore не только восстанавливает окно, но и выводит его на передний план, что нам и требуется. Ну а если программа не свёрнута, то "выливаем из чайника воду и тем самым сводим задачу к предыдущей" — сначала сворачиваем приложение с помощью Application.Minimize, а потом разворачиваем его. Цели мы добились — главное окно на переднем плане.
Дело портит только то, что изменение состояния окна сопровождается анимацией: видно, как главное окно сначала сворачивается, а потом разворачивается. Чтобы убрать этот неприятный эффект, можно на время сворачивания/разворачивания окна запретить анимацию, а потом восстановить его. С учётом этого метод GoToForeground выглядит так:
procedure TDKSViewMainForm.GoToForeground;
var
Info: TAnimationInfo;
Animation: Boolean;
begin
Info.cbSize := SizeOf(TAnimationInfo);
Animation := SystemParametersInfo(SPI_GETANIMATION, SizeOf(Info), @Info, 0) and
(Info.iMinAnimate <> 0);
if Animation then
begin
Info.iMinAnimate := 0;
SystemParametersInfo(SPI_SETANIMATION, SizeOf(Info), @Info, 0);
end;
if not IsIconic(Application.Handle) then
Application.Minimize;
Application.Restore;
if Animation then
begin
Info.iMinAnimate := 1;
SystemParametersInfo(SPI_SETANIMATION, SizeOf(Info), @Info, 0);
end;
end;
|
|
Теперь у нас сделано всё, что нужно: приложение умеет ассоциировать расширение с двумя командами; проверять, не ассоциировано ли расширение с другим приложением, и если да, предлагать пользователю установить эту ассоциацию; запрещать запуск второй копии приложения, переводя вместо этого на передний план первую копию; передавать параметры второй копии первой, чтобы она могла выполнить требуемые действия.
К материалу прилагаются файлы:
[TRegIniFile] [Окна, оконные сообщения] [Взаимодействие с 'чужими' процессами/приложениями] [Использование почтовых ящиков (mailslots)] [Реестр системы, ini-файлы. ] [Ассоциированные файлы] [Командная строка] [Передача параметров в приложение]
Обсуждение материала [ 08-06-2012 10:34 ] 11 сообщений |