Оформление приложений для Windows95/NT в Delphi

Сергей Трепалин

В настоящей публикации речь пойдет о тех правилах, которые желательно соблюдать при написании приложений, работающих с документами для Windows. Эти правила соблюдать не обязательно – можно создать вполне работоспособные приложения, которые не подчиняются данным правилам. Однако при их несоблюдении можно создать недружественный интерфейс и это приведет к тому, что пользователь при первой же возможности откажется от такого приложения.
Для приведенных здесь примеров используется язык Delphi фирмы Inprise (ранее -Borland), но все примеры легко перенести на другие языки программирования.

Регистрация расширений файлов и иконы документа в системном реестре. Регистрация икон документов сформулирована Microsoft как обязательное условие для приложений, которые претендуют на получение значка Win95 Logo. При этом сказано, что необходимо регистрировать как иконы 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 без перевозбужения исключения в секции 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;

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

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

Обработка сообщения WM_DROPFILES. Это сообщение возникает, когда пользователь открывает Windows Explorer, отмечает один или несколько файлов и, нажав левую кнопку мыши, перемещает ее на какую-либо форму (технология перетащить-и-отпустить). Реализация этого интерфейса начинается с вызова метода 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 реализация интерфейса ‘перетащить и бросить’ успешно работает и для диалогов. Имеется великолепный пример и исходные коды этого интерфейса в книге Тейлора.

Обработка сообщения WM_GETMINMAXINFO. Это сообщение вызывается при попытки изменения размера формы с толстыми границами. При обработке этого сообщения необходимо задать минимальные и максимальные размеры формы а также начальные координаты верхнего левого угла формы. Как правило, этот обработчик события используют только для задания минимальных ширины и высоты формы. При их определении исходят из того, что все контроли на форме обязаны быть всегда доступны пользователю. Соответствеено, разработчик никогда не должен создавать формы более 640*480 пикселов размером ( а еще лучше – 600*450). Не следует принимать во внимание возможное увеличение размера формы при увеличении размеров системного шрифта – системные шрифты с большим размером, как правило, используют при большом графическом разрешении экрана. Типичный пример обработчика WM_GETMINMAXINFO приведен ниже:

procedure TMainForm.WMGetMinMaxInfo(var Message:TWMGetMinMaxInfo);
begin
if csLoading in ComponentState then Exit;
with Message.MinMaxInfo^ do begin
ptMinTrackSize.X:=ScreenToClient(BitExport.ClientToScreen(Point(BitExport.Width,0))).X+200;
ptMinTrackSize.Y:=ScreenToClient(BitExport.ClientToScreen(Point(0,BitExport.Height))).Y
+2*GetSystemMetrics(SM_CYFRAME)+GetSystemMetrics(SM_CYCAPTION)+4;
end;
end;

Обратите внимание на проверку, что все ресурсы уже были загружены (if csLoading in ComponentState). При ее отсутствии возникает исключительная ситуация при старте приложения!
Обработчик данного события не позволит сделать высоту формы менее, чем нижний край контроля BitExport, а минимальная ширина формы будет на 200 пикселей больше, чем правый край этого контроля (там размещается контроль, размеры которого изменяются при изменении размера формы). Нельзя использовать координаты в пикселях при задании значений ptMinTrackSize.X и ptMintrackSize.Y! Во первых, при изменении положений контролей на форме на этапе разаботки, потребуются исправления в коде. Во вторых, контроли могут изменять размеры и позиции при переносе *.exe файла на компьютер с другим размером системного шрифта (смотрите следующий раздел). С этой точки зрения, новое свойство формы в Delphi 4, которое позволяет задать эти величины на этапе разработки, оформлено неудачно. Лучше было бы сделать событие, которое экспонировалось бы в инспекторе обьектов.

И, наконец, рекомендуется использовать системную метрику для определения величины границ неклиентной области формы. Использование системной метрики и цветов – требование Microsoft.

Масштабирование формы при изменении размера системного шрифта. При переносе приложения с одного компьютера на другой часто происходят нежелательные искажения форм: часть заголовков контролей перестают быть видимыми или контроли начинают перекрываться друг с другом и.т.д. Это происходит при изменении размера системного шрифта и приложение пытается изменить размеры и позиции контролей с тем, чтобы в том же масштабе разместить на них заголовки. В доступных мне приложениях, помимо того что в некоторых приложениях происходят безобразные искажения, я наблюдал три способа решения этой проблемы:

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

2. Приложение не изменяет позиций и размеров контролей при изменении величины системного шрифта, но размеры контролей достаточно большие, чтобы разместить на них надписи при больших размерах системного шрифта. Формы в таких приложениях часто производят плохое впечатление при малых размерах системного шрифта. Кроме того, если пользователь установит системный шрифт больше, чем стандартный «Large Font» Windows большого графического разрешения, то надписи часто не помещаются на контролях…

3. Наконец в ряде приложений величина и размеры контролей меняются пропорционально размеру системного шрифта. Это, на мой взгляд, наиболее корректный способ масштабирования и ниже пойдет об этом способе масштабирования.

Форма имеет два свойства, которые регулируют масштабирование: Scaled и PixelsPerInch. Если установить свойство Scaled в False, то форма не будет масштабироваться при изменении системного шрифта. Если при этом заранее сделать контроли большими, то получится тот самый результат, что и в п. 2 выше. Поэтому значение свойства Scaled должно быть True.
Свойство PixelsPerInch непонятно зачем выведено в Инспектор Объектов. Перед показом формы PixelsPerInch пересчитывается в соответствии с реальным размером системного шрифта и выставленное в Инспекторе Объектов значение затирается. Но именно пересчитанное значение важно анализировать для корректного масштабирования формы.
Если форма имеет толстые границы (ее размер в этом случае может изменяться пользователем во время выполнения приложения) или если форме разрешено иметь каретки для скроллирования, то размер клиентной области формы останется таким же, как и был на этапе разработки. При этом часть контролей уйдет за пределы клиентной области и пользователь вынужден будет пользоваться каретками для прокрутки формы или же менять границы при ее показе что приводит к лишним операциям.
Поэтому перед показом форм иногда необходимо писать код для пересчета ширины и высоты. Я это делаю в обработчике события OnShow. Использование OnShow гарантирует, что все ресурсы (в том числе и размеры контролей) уже загружены.

procedure TDialogForm.FormShow(Sender: TObject);
var
I,XMax,YMax:integer;
PT:TPoint;
begin
if not FFirstRun then Exit;
{Resizing of form}
if (Screen.PixelsPerInch<>96) and (ComponentCount>0) then begin
XMax:=0;
YMax:=0;
for I:=0 to ComponentCount-1 do if Components[I] is TControl then with Components[I] as TControl do begin
PT:=Self.ScreenToClient(ClientToScreen(Point(Width,Height)));
if PT.X>XMax then XMax:=PT.X;
if PT.Y>YMax then YMax:=PT.Y;
end;
XMax:=XMax+2*GetSystemMetrics(SM_CXDLGFRAME)+4;
YMax:=YMax+2*GetSystemMetrics(SM_CYDLGFRAME) +GetSystemMetrics(SM_CYCAPTION) +4;
Width:=XMax;
Height:=YMax;
end;
FFirstRun:=False;
end;

Этот код можно использовать в большинстве форм. Исключение – использование компонента TScrollBox – его детей не надо учитывать для определения размеров формы. Классовая переменная FFirstRun используется для однократного запуска данного кода.
Если контроли создаются динамически, то есть во время выполнения, а не на этапе разработки, то при установки границ контролей нельзя пользоваться абсолютными координатами – только относительными! Например, если контроль MyControl должен располагаться под кнопкой BitOK, иметь такую же ширину, а по высоте на 4 пиксела не доходить до края формы, то следует писать код: MyControl.SetBounds(BitOK.Left, BitOK.Top+BitOK.Height+4, BitOK.Width, ClientWidth-BitOK.Top-BitOK.Height-8); но не следует писать, например, так: MyControl.SetBounds(10,60,70,180);
Все сказанное выше о масштабировании работает только при совпадении шрифтов формы и установленных на ней контролях. Поэтому не рекомендуется определять отдельные шрифты для контролей. Если же это необходимо, то размеры таких контролей должны быть пересчитаны в явном виде перед показом формы. При этом для расчета масштабного коэффициента нельзя пользоваться свойством Height (или Size) шрифта! Необходимо вызвать Win API функцию GetTextMetrics и использовать поле tmHeight структуры TTextMetric.
Для создания полноценного интерфейса необходимо также корректно использовать меню и панели инструментов, создать систему помощи и подсказок, использовать информацию о версии. По этим вопросам имеется подробная документация в приведенной ниже литературе.

Литература:

1. Xavier Pachecho & Steve Teixeira. Delphi 2 Developer’s Guide. SAMS pub., 1996.
2. Don Taylor et all. High performance Delphi 3 programming. Coriolis Group, 1997 (имеется русский перевод).
3. Marco Cantu. Mastering Delphi 3. Sybex, 1997.

Координаты автора:
Учебно-консалтинговый центр Interface Ltd., тел. (095)135-55-00, 135-25-19,
mail@interface.ru


Interface Ltd.

Ваши замечания и предложения направляйте по адресу: webmaster@interface.ru

Reklama.Ru. The Banner Network.