Профессиональная разработка приложений с помощью Delphi 5
Часть 3. Оформление приложений для Windows95/98/NT/2000 в Delphi
Регистрация расширений файлов и пиктограммы документа в системном реестре
Обработка сообщения WM_DROPFILES
Обработка сообщения WM_GETMINMAXINFO
Создание приложений с невидимой главной формой и использование Tray Icon
В настоящей публикации речь пойдет о правилах, которые желательно соблюдать при написании 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-приложениях необходимо выполнить следующую последовательность действий:
- При запуске приложения проверить, работает ли уже его копия. Наиболее просто в среде 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.
Таким образом, в результате регистрации в системном реестре пиктограммы и команды открытия документа визуальный интерфейс выглядит следующим образом:
- в Windows Explorer (а также в диалогах File/Open и File/Save) файлу с соответствующим расширением ставится в соответствие зарегистрированная пиктограмма;
- там же можно увидеть информационную строку — краткую аннотацию документа;
- при двойном щелчке мыши на имени файла документа автоматически происходит запуск приложения и загружается выбранный документ;
- все файлы, с которыми пользователь работал, попадают в списки документов. Вызвать этот список можно выбором пункта меню Windows Пуск/Документы, что приведет к старту приложения и загрузке выбранного документа.
Обработка сообщения 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: библиотека программиста». — Прим. ред.).
Обработка сообщения WM_GETMINMAXINFO
В Delphi 4 и 5 класс TForm обладает свойством Constraints типа TSizeConstraints. Используя это свойство, программист может задать минимальную и максимальную ширину и высоту формы. Соответственно если пользователь во время выполнения приложения попытается изменить указанную ширину или высоту формы, то приложение не позволит сделать этого. Событие WM_GETMINMAXINFO как раз и предназначено для того, чтобы сообщить системе о возможных диапазонах изменения границ элемента управления. Однако по непонятным соображениям свойство Constraints в Delphi 5 используется не в обработчике события WM_GETMINMAXINFO, а в обработчике события WM_SIZE. Такая «кривая» реализация приводит к некорректному поведению формы при изменении ее размеров: пользователь может сделать ее очень маленькой или очень большой, и только после отпускания кнопки мыши используется информация из свойства Constraints, в соответствии с которой форма вновь изменяет свои размеры. Точно так же некорректно реализовано событие OnConstrainedResize, в котором динамически можно заполнить структуру TSizeConstraints. В связи с этим в Delphi 5, как и в предыдущих версиях Delphi, необходимо создавать обработчик сообщения 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. Во-первых, при изменении положений элементов управления на форме в процессе разработки потребуются и соответствующие исправления в коде. Во-вторых, элементы управления могут изменять размеры и положение при переносе готового приложения на компьютер с другим размером системного шрифта (об этом будет рассказано в следующем разделе).
И наконец, рекомендуется использовать системную метрику для определения величины границ неклиентской области формы. Использование системной метрики и цветов является требованием Microsoft. В вышеприведенном примере обращение к системной метрике осуществляется вызовом функции GetSystemMetrics.Масштабирование формы при изменении размера системного шрифта
При переносе приложения с одного компьютера на другой часто происходят нежелательные искажения форм, например часть заголовков элементов управления перестает быть видимой, элементы управления перекрывают друг друга и т.д. Это случается при изменении размера системного шрифта, после чего приложение пытается изменить размеры и положение элементов управления с целью размещения на них надписей в том же масштабе.
Можно отметить три способа решения этой проблемы:
- Приложение устанавливает новый системный шрифт. От приложений такого рода следует по возможности избавляться, поскольку непонятно его поведение при запуске второго аналогичного приложения, которое снова захочет изменить системный шрифт для себя.
- Приложение не изменяет позиций и размеров элементов управления при изменении величины системного шрифта, однако размеры самих элементов управления достаточно велики, чтобы разместить на них надписи при увеличении размеров системного шрифта. Формы в таких приложениях часто производят плохое впечатление при малых размерах системного шрифта. Кроме того, если пользователь устанавливает системный шрифт больше стандартного Large Font Windows высокого графического разрешения, надписи зачастую все равно не умещаются на элементах управления.
- Наконец, в ряде приложений величина и размеры элементов управления меняются пропорционально размеру системного шрифта. Это, на мой взгляд, наиболее корректный способ масштабирования, именно о нем и пойдет речь ниже.
Форма имеет два свойства, которые регулируют масштабирование: Scaled и PixelsPerInch. Если установить свойство Scaled равным False, то при изменении системного шрифта форма масштабироваться не будет. Если при этом заранее сделать элементы управления большими, получится тот самый результат, что в пункте 2. Следовательно, значение свойства Scaled должно быть равно True.
Непонятно, зачем свойство 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) шрифта. Вместо этого нужно вызвать функцию Windows API GetTextMetrics и использовать поле tmHeight структуры TTextMetric.
Создание приложений с невидимой главной формой и использование Tray Icon
В некоторых специальных типах приложений (например, MIDAS-серверов) требуется, чтобы при его старте главная форма не показывалась на экране. Попытка изменить свойство Visible главной формы ни к чему хорошему не приводит. Для скрытия главной формы в момент старта приложения необходимо установить свойство Application.ShowMainForm в False. Это обычно достигается добавлением одной строки кода в *.dpr-файл:
begin Application.Initialize; Application.ShowMainForm:=False; Application.CreateForm(TForm1, Form1); Application.Run; FNID:TNotifyIconData; end.
В приложениях такого типа не только не показывается форма, но и на панели задач (taskbar) отсутствует кнопка для показа или скрытия главной формы приложения. Поэтому, если все же по требованию пользователя необходимо показывать главную форму, это достигается помещением небольшой пиктограммы (tray icon) в правом углу панели задач (taskbar). Подобным образом это реализовано, например, в Microsoft Volume Control и ряде других приложений.
Создание tray-пиктограммы необходимо описывать в конструкторе класса формы. Для этого сошлемся на модуль ShellAPI и секции private класса TForm1 объявим переменную:
Далее, воспользовавшись прилагаемым к Delphi графическим редактором Image Editor, создадим новый ресурсный файл с расширением *.res, а в него поместим пиктограмму, размеры которой должны составлять 16*16 (пиктограммы других размеров не могут быть помещены на панель задач!). Назовем эту пиктограмму MICON и сохраним файл ресурсов под именем Unit1.res. Дальнейшие изменения производим в файле Form1.pas. Прежде всего считаем ресурсы из файла Unit1.res. Для этого добавим команду:
{$R *.RES}
Затем объявим в секции Private переменные FHI:Ticon и FNID:TNotifyIconData. Конструктор класса TForm1 перепишем следующим образом:
constructor TForm1.Create(AOwner:TComponent); begin inherited Create(AOwner); FHI:=TIcon.Create; FHI.Handle:=LoadIcon(HInstance,'MICON'); FNID.cbSize:=sizeof(FNID); FNID.Wnd:=Handle; FNID.uID:=1; FNID.uCallbackMessage:=WM_ICONNOTIFY; FNID.HIcon:=FHI.Handle; FNID.szTip:='Hidden application'; FNID.uFlags:=nif_Message or nif_Icon or nif_Tip; Shell_NotifyIcon(NIM_ADD,@FNID); end;
Первоначально создается объект TIcon и из ресурсов загружается пиктограмма. После этого заполняется структура TNotifyIconData (она определена в модуле ShellAPI), при этом сразу же осуществляется создание кода для всплывающего меню. Указывается дескриптор окна, которому следует посылать сообщения, — в данном случае это главная форма приложения. Поле FNID.uID содержит порядковый номер пиктограммы в ресурсах. FNID.uCallbackMessage содержит сообщение, которое будет посылаться окну FNID.Wnd при движении или нажатии кнопки мыши над tray-пиктограммой. Оно определено как константа:
const WM_ICONNOTIFY=WM_USER+1234;
Поле FNID.szTip содержит подсказку, которая будет всплывать при прохождении курсора мыши над пиктограммой. Поле uFlags определяет тип tray-пиктограммы — в данном примере при двойном щелчке на пиктограмме должны быть инициированы посылка сообщений окну FNID.Wnd и показ пиктограммы плюс показ подсказки. И наконец, вызов метода Shell_NotifyIcon использует заполненную структуру для показа пиктограммы.
Для работы всплывающего меню поместим компонент TPopupMenu на форму Form1. Определим два метода: Show (показ формы) и Close (прекращение работы приложения). В обработчике события OnClick для строки меню Show поместим код остановки сервиса:
procedure TForm1.Show1Click(Sender: TObject); begin Show; BringWindowToTop(Handle); end;
и в обработчике события OnClick строки меню Properties осуществим показ формы:
procedure TForm1.Close1Click(Sender: TObject); begin Application.Terminate; end;
Но этого еще недостаточно — осталось вызвать всплывающее меню при нажатии правой кнопки мыши над tray-пиктограммой. Сообщение WM_ICONNOTIFY будет посылаться главной форме. Создадим обработчик событий WM_ICONNOTIFY следующим образом:
procedure TForm1.WMIconNotify(var Message:TMessage); var PT:TPoint; begin if Message.lParam=WM_LBUTTONDOWN then begin Show; BringWindowToTop(Handle); end else if Message.lParam=WM_RBUTTONDOWN then begin GetCursorPos(PT); PopupMenu1.Popup(PT.X,PT.Y); end; end;
При нажатии левой кнопки мыши будет показываться главная форма приложения, а с помощью правой кнопки мыши над пиктограммой будет вызываться всплывающее меню. Соответственно при выборе меню Show происходит показ формы, а при выборе меню Close — закрытие приложения.
После окончания работы приложения с панели задач необходимо удалить пиктограмму. Сделать это необходимо обязательно, иначе после окончания работы приложения оставшаяся пиктограмма попытается обратиться к уже не существующим окнам. Поэтому перепишем деструктор главной формы:
destructor TForm1.Destroy; begin FNID.uFlags:=0; Shell_NotifyIcon(NIM_DELETE,@FNID); FHI.Free; inherited Destroy; end;
В этом фрагменте кода, помимо прекращения показа TrayIcon, происходит освобождение ресурсов.
И наконец, последняя проблема, которую необходимо решить при создании данного приложения, — это его поведение при нажатии кнопки Close на форме. По умолчанию при нажатии этой кнопки приложение закрывается. В данном случае это нежелательно — закрытие приложения разумно осуществлять в команде всплывающего меню, как это было описано выше. Форма имеет обработчик события OnClose, где можно изменить переменную Action и тем самым изменить поведение формы при нажатии кнопки Close. Однако это не относится к главной форме приложения — любое значение переменной Action вызовет деструктор главной формы и, как следствие, закрытие приложения. Для того, чтобы спрятать главную форму, не вызывая ее деструктора, необходимо переписать событие WM_CLOSE:
procedure TForm1.WMClose(var Message:TMessage); begin Message.Result:=0; Hide; end;
Обратите внимание на отсутствие оператора Inherited в данном коде. Обработчик WM_CLOSE вызывает деструктор по умолчанию, поэтому Inherited нельзя вызывать. Это один из немногих случаев, когда не требуется вызывать обработчик события по умолчанию.
Информация о версии
Информация о версии прежде всего используется для корректной установки приложений с расширяемыми ресурсами (например, с DLL — динамически загружаемыми библиотеками). Смысл расширяемых ресурсов заключается в том, что они могут использоваться несколькими приложениями, в том числе созданными и сторонними производителями. Поскольку разработчики приложений такого типа чаще всего ничего друг о друге не знают, при создании дистрибутивов им необходимо учитывать возможности существования нескольких версий данного расширяемого ресурса. Даже если в данный момент имеется только одна версия, вполне возможно, что производитель расширяемых ресурсов (чаще всего это компания Microsoft) выпустит их в будущем.
Кроме того, и в обычных приложениях необходимо использовать информацию о версии. На первый взгляд, это может показаться странным — при создании дистрибутива программист, естественно, включает последнюю версию своего программного обеспечения. Однако никто не гарантирует, что у пользователя не останется старый дистрибутив: ошибочно запустив этот дистрибутив, он лишится новой версии программы.
Главный смысл контроля версии: любой файл, содержащий код (исполняемый файл или динамически загружаемая библиотека), содержит в ресурсах информацию о версии, которая понятна Windows. Это четыре числа, например 1.0.0.0 или 1.1.0.2. Чем больше эти числа, тем новее версия, причем чем левее стоит число, тем больший у него приоритет при определении версии. Перед копированием такого файла на диск проверяется, существует ли файл с таким именем. В случае положительного ответа сравниваются эти числа в уже существующем и новом файле. Если оказывается, что существующий файл имеет ту же самую или более новую версию, копирование не выполняется. Если же существующий файл имеет более раннюю версию, то старый файл переписывается новым с дистрибутива. Некоторые инсталляционные приложения требуют подтверждения копирования у пользователя и в конце инсталляции дают ему список замененных файлов.
Имеется ряд функций Windows API, которые позволяют выполнять манипуляции с файлами с учетом их версий. Например, VerInstallFile копирует и распаковывает файл только в том случае, если у пользователя отсутствует файл с более новой версией. Обычно программисту не требуется вызывать эти методы, поскольку их вызов осуществляется автоматически в инсталляционных программах типа InstallShield. Но если программист сам создает инсталляционное приложение, то без вызова этих методов ему не обойтись.
Для внесения информации о версии в открытом проекте необходимо вызвать команду меню Project/Options и в появляющемся диалоге выбрать закладку Version Info. Для добавления информации о версии необходимо отметить опцию Include Version Information in project (рис. 1)
Если на данном компьютере имеется поддержка русского языка, то по умолчанию ставится значение Language=$0419 (Русский). Это, в принципе неправильно, поскольку такая информация о версии может быть прочитана только русской версией Windows. Однако очень многие пользователи имеют английскую версию Windows c поддержкой русского языка. Если приложение не представляет собой какую-либо только национальную специфику, в нем обязательно должна быть включена информация о версии на английском языке (Language=$0409).
Значения всех флагов и элементов управления в этом диалоге очевидны и не требуют дальнейших пояснений. Некоторые затруднения вызывает заполнение текстовых полей. Их значение следующее:
- CompanyName — название компании — производителя программного продукта;
- FileDescription — краткое текстовое описание назначения программного продукта;
- FileVersion — заполняется автоматически при выборе соответствующих значений в контролях;
- InternalName — название приложения, обычно имя файла без расширения;
- LegalCopyright — строка типа Copyright © 1999-2000 by SciSoft Ltd;
- LegalTrademarks — зарегистрированная торговая марка на продукт (если таковая имеется);
- OriginalFileName — имя файла с расширением;
- ProductName — то, с чем работает данный програмный продукт. Если проект — *.exe-файл, то тут указывается Windows, если *.dll — то название *.exe-файла, для которого сделана данная библиотека;
- ProductVersion — версия того, с чем работает данный продукт, например 4.0.0.0. Если в предыдущей строке было указано Windows, то обе эти строки вместе означают Windows NT;
- Comments — любой текстовый комментарий. Часто здесь указывается, как связаться с производителем данного программного продукта.
В заключение полезно привести фрагмент кода, который позволяет считывать информацию о версии из ресурсов. Эта информация может быть показана, например, в диалоге About:
var ThisVerComments,ThisVerCompanyName, ThisVerFileDescription,ThisVerFileVersion,ThisVerInternalName, ThisVerLegalCopyright,ThisVerLegalTrademarks,ThisVerOriginalFilename, ThisVerProductVersion,ThisVerProductName,ThisVerSpecialBuild, ThisVerPrivateBuild:string; ThisVerFixedFileInfo:TVSFixedFileInfo; procedure InitializeVersion; var VSize:integer; VHandle:DWORD; VData:pointer; PC:pointer; Len:UINT; C:array[0..1000] of char; begin VData:=nil; ThisVerComments:=''; ThisVerCompanyName:=''; ThisVerFileDescription:=''; ThisVerFileVersion:=''; ThisVerInternalName:=''; ThisVerLegalCopyright:=''; ThisVerLegalTrademarks:=''; ThisVerOriginalFilename:=''; ThisVerProductVersion:=''; ThisVerProductName:=''; ThisVerSpecialBuild:=''; ThisVerPrivateBuild:=''; {Reading This product version from resource} GetModuleFileName(HInstance,C,1000); VSize:=GetFileVersionInfoSize(C,VHandle); if VSize>0 then begin GetMem(VData,VSize); if GetFileVersionInfo(C,VHandle,VSize,VData) then begin if VerQueryValue(VData,'\StringFileInfo\040904E4\Comments',PC,Len) then ThisVerComments:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\CompanyName',PC,Len) then ThisVerCompanyName:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\FileDescription',PC,Len) then ThisVerFileDescription:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\FileVersion',PC,Len) then ThisVerFileVersion:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\InternalName',PC,Len) then ThisVerInternalName:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\LegalCopyright',PC,Len) then ThisVerLegalCopyright:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\LegalTrademarks',PC,Len) then ThisVerLegalTrademarks:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\OriginalFilename',PC,Len) then ThisVerOriginalFilename:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\ProductVersion',PC,Len) then ThisVerProductVersion:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\ProductName',PC,Len) then ThisVerProductName:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\SpecialBuild',PC,Len) then ThisVerSpecialBuild:=StrPas(PC); if VerQueryValue(VData,'\StringFileInfo\040904E4\PrivateBuild',PC,Len) then ThisVerPrivateBuild:=StrPas(PC); if VerQueryValue(VData,'\',PC,Len) then ThisVerFixedFileInfo:=PVSFixedFileInfo(PC)^; end; end;
Настраиваемые опции проекта
Некоторые опции проекта в Delphi по умолчанию имеют значения, приводящие к снижению качества создаваемых приложений. Прежде всего, все добавленные формы по умолчанию становятся автоматически создаваемыми (Auto Created). Это означает, что их конструкторы вызываются автоматически в момент запуска приложения. В серьезных проектах зачастую насчитывается более ста форм, и вызов их конструкторов в момент старта приложения приводит к следующему:
- Приложение очень долго загружается
- Резервируется большое количество ресурсов для невидимых элементов управления. Это влечет за собой повышение требований к рабочему месту, на котором такое приложение используется. Часто на компьютерах с небольшим объемом оперативной памяти после старта приложение начинает обращаться к виртуальной памяти на жестком диске, что существенно снижает скорость его работы.
Поэтому для всех форм, кроме главной и немодальных, которые будут показываться в единственном экземпляре, флаг Auto Created должен быть убран. Для этого их необходимо перенести в список Available. Соответственно их конструктор и деструктор должны быть вызваны динамически. Поэтому вместо одной строчки кода:
Form2.ShowModal;
которая используется для показа модального диалога формы с флагом Auto Created, потребуется написать следующий код:
Form2:=nil; try Form2:=TForm2.Create(Self); Form2.ShowModal; finally if Assigned(Form2) then Form2.Release; end;
Такая добавка не приводит к «раздуванию» кода программы, даже если показ диалогов осуществляется из сотен мест. Однако с точки зрения потребления ресурсов операционной системы это приложение будет гораздо более экономным.
В опциях проекта необходимо также определить имя и путь файла справки (Application/Help File). Ясно, что если указать имя файла с путем, то пользователь должен будет ставить приложение в фиксированный каталог, чего допускать нельзя. В случае же отсутствия пути в имени файла WinHelp осуществляет поиск файла помощи в текущем каталоге. При этом если текущий каталог не совпадает с каталогом, в котором находится *.hlp-файл, то WinHelp объявляет о его отсутствии. Поэтому не рекомендуется указывать имя файла справки при разработке проекта. Вместо этого выполняется следующая последовательность действий:
- *.hlp-файл (предположим его имя myhelp) помещается в тот же каталог, где находится исполняемый файл проекта (*.exe).
- В обработчике события OnCreate главной формы добавляется оператор:
Application.HelpFile:=ExtractFilePath(ParamStr(0))+’myhelp.hlp’;
При такой записи пользователь может выбирать для инсталляции любой каталог и файл справки будет найден всегда, независимо от текущего каталога.
Заключение
Для создания полноценного интерфейса необходимо также корректно использовать меню и панели инструментов, создать систему помощи и подсказок. Первый пункт не вызывает затруднений у опытных программистов. Создание же современной системы подсказок — достаточно сложный раздел, и, как правило, программисты этим не занимаются.
Следующий этап в создании проектов (особенно больших) — это разбиение проекта на отдельные модули. Такое разбиение позволяет организовать групповую работу над проектом, упростить отладку, сделать опционную инсталляцию проекта — когда часть функций может быть недоступна пользователю, сэкономить ресурсы. О создании отдельных модулей — динамически загружаемых библиотек (DLL) — и пойдет речь в следующей публикации данного цикла.
КомпьютерПресс 3'2001