СТАТЬЯ

26.09.01


Предыдущая часть

Профессиональная разработка приложений с помощью Delphi 5

Часть 3. Оформление приложений для Windows95/98/NT/2000 в Delphi

Сергей Трепалин,
УКЦ Interface Ltd.
КомпьютерПресс #3 2001

Статья была опубликована в КомпьютерПресс (www.cpress.ru)

В настоящей публикации речь пойдет о правилах, которые желательно соблюдать при написании Windows-приложений, работающих с документами (полный список этих правил содержится в документах Microsoft, входящих в состав Platform SDK для соответствующей версии Windows. — Прим. ред.). В принципе, эти правила соблюдать не обязательно — можно создать вполне работоспособные приложения, которые им не подчиняются. Однако это усложняет понимание пользователями графического интерфейса приложений и затрудняет работу с ним. Естественно, при первой же возможности пользователь откажется от такого приложения. В настоящей публикации изложены только основные разделы, которые вызывают наибольшее затруднения у программистов.

Регистрация расширений файлов и пиктограммы документа в системном реестре

Регистрация пиктограмм документов оговаривается Microsoft как обязательное условие для приложений, претендующих на получение логотипа Win95 Compatible. При этом установлено, что необходимо регистрировать пиктограммы размером и 32*32, и 16*16 пикселов. На практике достаточно использовать пиктограммы 32*32. Известен один тип приложений, которым реально нужны пиктограммы 16*16, — это приложения, отображающие пиктограмму указанного размера в правом нижнем углу панели задач (tray icon).

В Delphi имеется модуль Registry, который обеспечивает доступ к системному реестру. Приведенный ниже фрагмент кода позволяет зарегистрировать расширение файлов *.mfe:

Uses Registry;
…

procedure TMainForm.FormCreate;
var
    Reg:TRegistry;
begin
   Reg:=nil;
   try      {Register icon}
      Reg:=TRegistry.Create;
      Reg.RootKey:=HKEY_CLASSES_ROOT;
      Reg.OpenKey('\.mfe',True);
      Reg.WriteString('', 'MainFormData');
      Reg.CloseKey;
      Reg.OpenKey('\MainFormData',True);
      Reg.WriteString('', 'My private datafiles');
      Reg.CloseKey;
      Reg.OpenKey('\MainFormData\Shell\Open\Command',True);
      Reg.WriteString('',ParamStr(0)+' %1');
      Reg.CloseKey;
      Reg.OpenKey('\MainFormData\DefaultIcon',True);
      Reg.WriteString('',ParamStr(0)+', 1');
      Reg.CloseKey;
      Reg.Free;
   except
      if Assigned(Reg) then Reg.Free;
   end;
end;

В принципе, эту процедуру достаточно вызвать один раз при инсталляции приложения. Однако согласно правилам Microsoft приложение при запуске должно проверять относящиеся к нему секции системного реестра и при наличии недоброкачественной информации — исправлять ее. Наиболее просто это достигается повторной регистрацией.

Объект TRegistry не имеет владельца, и поэтому все манипуляции с ним должны проводиться в защищенном блоке. Необходимо использовать блок try … except … end без повторного возбуждения исключения оператором raise в секции except … end. Если происходит исключение, то приложение продолжит работу без сообщения пользователю какой-либо информации. Это в данном случае оправданно, так как процедура регистрации относится только к сервису и не влияет на работу самого приложения. При повторном возбуждении исключения (или использования блока try … finally … end), если возникнет исключительная ситуация, то главная форма не будет создана и приложение не будет запущено. Исключительная ситуация гарантированно возникнет при запуске приложения в Windows NT, если проект был скомпилирован в Delphi 3.0 или в более ранней версии, а пользователь вошел не под именем системного администратора. В Delphi 3.01 этот недостаток уже устранен. Если все же необходимо сообщить пользователю о проблемах с системным реестром, то рекомендуется использовать метод типа MessageDlg в секции except … end, но не генерировать исключение.

Данный фрагмент кода создает в системном реестре две секции: .mfe, которая просто ссылается на другую секцию — MainFormData. Информационная строка ‘My private datafiles’ будет видна в Windows Explorer при выборе режима просмотра содержимого каталогов в виде таблицы рядом с каждым файлом с зарегистрированным расширением. Кроме того, в этой секции также прописываются пиктограмма для документа (которая в принципе может совпадать с пиктограммой приложения, но так лучше не делать), а также команда, которую необходимо выполнить, если пользователь дважды щелкнул мышью в Windows Explorer по имени файла с зарегистрированным расширением.

Обе эти команды требуют, чтобы в реестре был прописан полный путь к приложению. Можно его прописать сразу же в явном виде, например: C:\MyDir\MyProject.exe, но так делать не рекомендуется. Дело в том, что пользователи имеют привычку переименовывать каталоги, при этом путь к приложению будет утерян. Если использовать результат, возвращаемый функцией ParamStr(0), – полный путь и имя приложения, то при однократном запуске из переименованного каталога значимая информация будет восстановлена в системном реестре.

Помимо ссылки на файл *.exe или *.dll, для регистрации пиктограммы необходимо указать ее индекс. Он начинается с нуля — главной пиктограммы приложения. Очевидно, что в ресурсах приложения необходимо иметь как минимум две пиктограммы: для обозначения документов и самого приложения. Это достигается посредством создания отдельного *.res-файла и включением директивы {$R filename.res} в *.pas-файл. Хотя любое приложение имеет *.res-файл, совпадающий с именем проекта, включать туда вторую пиктограмму (и вообще любые другие ресурсы) абсолютно бессмысленно — этот файл полностью переписывается при вызове команды Project/Options в среде разработки. Отмечу также, что все пиктограммы, загружаемые в формы при использовании свойства Icon, не хранятся в виде понятных Windows ресурсов, и ссылаться на них по индексам бессмысленно.

Строка %1 в регистрации команды, которая будет вызываться при двойном щелчке на именах файлов документов, означает подстановку полного пути и названия файла документа вместо параметра %1. Поэтому приложение при старте обязано проверять результат, возвращаемый функцией ParamCount. Если он больше нуля и ParamStr(1) возвращает легальное имя файла, то документ необходимо загрузить автоматически после старта приложения. Здесь имеется существенное различие для приложений SDI (Single Document Interface) и MDI (Multiply Document Interface).

В SDI-приложениях можно проанализировать значение ParamCount в обработчике события OnCreate главной формы и там же выполнить все необходимые процедуры по загрузке документа. В MDI-приложениях необходимо выполнить следующую последовательность действий:

  1. При запуске приложения проверить, работает ли уже его копия. Наиболее просто в среде Windows 95/NT эта задача решается с помощью мьютекса. При наличии работающей копии MDI-приложение обязано обратиться к ней для восстановления ее на экране (она может быть прежде минимизирована пользователем) и поднятия окна на верхний уровень (она может быть перекрыта другими окнами). Это достигается посылкой сообщения методом PostMessage, причем параметр типа HWND может быть найден вызовом метода FindWindow. После этого приложение должно закрыться без показа главной формы на экране. Но перед закрытием необходимо проанализировать ParamCount, ParamStr(1) и при наличии легального файла документа передать его название и путь в работающую копию. Для передачи данных можно использовать Clipboard. Ниже приводится фрагмент кода, иллюстрирующий сказанное:
program OneInst;

uses
   Forms,
   Windows,
   ClipBrd,
   UMain in 'UMain.pas' {MainForm};

{$R *.RES}

var
   HM:THandle=0;

function CheckForInstance:boolean;
var
   HW:THandle;
   N:integer;
begin
   N:=0;
   HM:=OpenMutex(MUTEX_ALL_ACCESS,False,'MyMutex');
   Result:=True;
   if HM<>0 then begin
      HW:=FindWindow('TMainForm','MainForm');
      if HW<>0 then begin
          if ParamCount>0 then begin
             Clipboard.AsText:=ParamStr(1);
             N:=1;
          end;
          PostMessage(HW,WM_RESTOREMESSAGE,N,0);
      end;
      Result:=False;
      HM:=0;
   end else HM:=CreateMutex(nil,False,'MyMutex');
end;


begin
   if CheckForInstance then begin
      Application.Initialize;
      Application.CreateForm(TMainForm, MainForm);
      Application.Run;
      if HM<>0 then ReleaseMutex(HM);
   end;
end.

Константа WM_RESTOREMESSAGE и обработчик события определены в единице UMain:

Const
WM_RESTOREMESSAGE=WM_USER+3245;

…
procedure TMainForm.WMRestoreMessage(var Message:TMessage);
var
   S:string;
begin
   Application.Restore;
   SetForegroundWindow(Handle);
   S:='';
   if (Message.wParam<>0) 
   and Clipboard.HasFormat(CF_TEXT) then begin
      S:=Clipboard.AsText;
      Clipboard.AsText:='';
   end;
   if length(S)>0 then if FileExists(S) then CreateMDIChild(S);
end;

Если работающая копия MDI-приложения отсутствует, то, как и в случае SDI-приложения, сразу после старта необходимо загрузить соответствующий документ. Если попытаться это сделать в обработчике события OnCreate главной формы, то приложение остановится с сообщением об ошибке. Создавать дочернее MDI-окно необходимо в обработчике события OnShow.

Таким образом, в результате регистрации в системном реестре пиктограммы и команды открытия документа визуальный интерфейс выглядит следующим образом:

Обработка сообщения WM_DROPFILES

Сообщение WM_DROPFILES возникает, когда пользователь открывает Windows Explorer, отмечает один или несколько файлов и, нажав левую кнопку мыши, перемещает ее указатель на какую-либо форму (технология drag-and-drop). Реализация этого интерфейса начинается с вызова метода DragAcceptFiles(Handle,True) в обработчике события OnCreate главной формы и заканчивается вызовом этого же метода, но с параметром False в обработчике события OnDestroy. DragAcceptFiles определена в единице ShellAPI. Вызов этого метода (когда еще не добавлен обработчик события WM_DROPFILES) приводит к появлению «разрешающего» курсора, когда перетаскиваются файлы из Windows Explorer.

Пример обработчика события WM_DROPFILES (для MDI-приложения) приведен ниже:

procedure TMainForm.WMDropFiles(var Message:TWMDropFiles);
var
   HF:THandle;
   S,SMessage:string;
   C:array[0..MaxPathLength] of char;
   I,Count:integer;
begin
   HF:=Message.Drop;
   Count:=DragQueryFile(HF,$FFFFFFFF,nil,0);
   SMessage:= '';
   if Count>0 then for I:=0 to Count-1 do begin
      DragQueryFile(HF,I,C,MaxPathLength);
      S:=StrPas(C);
      if not CreateMDIChild(S) then SMessage:=SMessage+#13+#10+S;
   end;
   DragFinish(HF);
   if length(SMessage)>0 then 
   MessageDlg(Format('Next files can not be loaded: %s’, 
                     [SMessage]),mtError,[mbOK],0);
end;

Метод DragQueryFile с параметром $FFFFFFFF возвращает общее число файлов, выбранное пользователем в Windows Explorer. Этот же метод копирует путь и имя файла в буфер C при легальном значении счетчика I. Далее MDI-приложение пытается создать окно и загрузить выбранный документ. Не рекомендуется проверять расширение файла для определения того, содержит ли файл документ нужного формата. Во-первых, пользователь может переименовать файл с документом, а во-вторых, в файл с нужным расширением он может поместить «посторонние» данные. При неудачной загрузке документа функция CreateMDIChild не создает дочернее окно и возвращает False без показа пользователю возможных ошибок. Список файлов, которые не могут быть загружены (если таковые имеются), приводится в одном диалоге в конце выполнения команды. В заключение обязательно должен вызываться метод DragFinish — он освобождает память, которую выделил Windows Explorer для сохранения имен и путей выбранных файлов.

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

Если приложение работает с составными документами и имеются диалоги для редакции отдельных его частей, логично реализовать WM_DROPFILES для отдельных диалогов. Например, если в документ входит растровое графическое изображение и имеется диалог для его редактирования, то разумно разрешить в нем загрузку *.bmp-файлов.

Все вышесказанное о реализации WM_DROPFILES абсолютно неприменимо к диалоговым панелям — только главная форма приложения способна получать сообщение WM_DROPFILES. Здесь очень кстати вспомнить, что реализация ShellAPI базируется на технологии COM (Component Object Model). OLE-реализация интерфейса drag-and-drop успешно работает и для диалоговых панелей. Великолепный пример и исходные коды этого интерфейса приведены в книге Тейлора (Don Taylor, Jim Mischel, John Penman, Terence Goggin, John Shemitz. High Performance Delphi 3 Programming. Coriolis Group Book, 1997; русский перевод этой книги, вышедший в издательстве «Питер» в 1998 году, известен под названием «Delphi 3: библиотека программиста». — Прим. ред.).

Продолжение статьи

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Отправить ссылку на страницу по e-mail
Обсудить на форуме Inprise/Borland


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 26.09.01