В этой заметке я хочу рассказать о базовой форме и базовой фрейме. Что это такое, и зачем это нужно. Под словом “базовая” я подразумеваю, что любая форма/фрейма в проекте является наследником TMyBaseForm/TMyBaseFrame, а не стандартных TForm/TFrame. Т.е. слово “базовая” никакого отношения к базам данных не имеет; в своих проектах, при создании новых форм и фрейм, я всегда наследуюсь от базовых.
Немного лирики
Представьте себе типичный Delphi-проект для работы с базой данных. Наиболее распространённым и привычным является такой интерфейс: таблица БД (набор из нескольких записей) отображается в гриде, данные редактируются в отдельной (модальной) форме. Возьмём, к примеру, справочник контрагентов. Делаем: а) форму со строкой поиска и гридом, для отображения контрагентов; б) форму для редактирования карточки контрагента. По двойному клику на строке в гриде открывается карточка контрагента на редактирование в модальном режиме.
В моей практике подобный тип интерфейса наиболее распространённый: грид предоставляет интерфейс для отображения списка записей таблицы, модальная форма используется для редактирования конкретной записи. Есть и другие варианты, но для сегодняшнего вопроса это не принципиально.
Итак, допустим мы сделали пару форм для контрагентов. Потом ещё пару форм для работы со справочником поставщиков. Потом ещё пару форм. А потом ещё. А потом мы вдруг обнаруживаем проблему-неприятность: если пользователь свернёт модальную форму, то модальная форма свернётся не в панель задач, а в нижнюю часть рабочего стола, причём родительская форма останется на экране и одновременно не доступной для пользователя. Чтобы такую “красоту” избежать, можно скрыть/сделать неактивной кнопку сворачивания в модальной форме. Но это тоже не красиво – пользователь не сможет свернуть приложение. Поэтому я предпочитаю в модальной форме обрабатывать сообщение WM_SYSCOMMAND: SC_MINIMIZE как-то так:
procedure TForm1.WMSysCommand(var Msg: TMessage); begin if (Msg.WParam = SC_MINIMIZE) and (fsModal in FormState) then Application.Minimize else inherited; end;
Это наиболее ожидаемое поведение приложения для пользователя: если форма открыта в модальном режиме, то при её сворачивании происходит сворачивание всего приложения. Отлично, проблему решили для карточки контрагента, теперь копируем код в карточку поставщика, потом ещё, и ещё…
Или вот ещё одно привычное для пользователя поведение GUI: если форма не главная (окно настроек, модальная карточка контрагента, плавающее окно и т.п.), то при нажатии на клавишу Escape пользователь ожидает закрытия (скрытия) окна. Как правильно закрывать форму по Escape я писал ранее, а именно: обработка сообщения CM_DIALOGKEY. Ну т.е. и эту проблему мы решили для карточки контрагента, затем для карточки поставщика (copy/paste рулит), затем ещё, и ещё…
Надеюсь Вы поняли, к чему я клоню. Чем больше форм, тем больше копирований приходится делать, устраняя какую-то мелочь. Опять же, дублирование кода, тоже плохо.
В общем, устраняя в очередной раз проблему невольно задумываешься: а как бы сделать так, чтобы не дублировать код? В принципе, в некоторых случаях можно написать общую ловушку (хук) на cообщения и анализировать ActiveForm. Но хук – это, во-первых, не красиво, а во-вторых – не всё можно сделать “снаружи” формы без ухищрений. Например, тоже полезная вещь: автоматическое уничтожение формы при её закрытии. Имея базовую форму, можно в ней объявить свойство FreeOnClose.
Базовая форма
Итак, нам нужна базовая форма. Базовая форма – это класс наследник от TForm, в котором мы реализуем “рюшечки”, “хотелки” и “необходимки”, которых нету в TForm, но которые являются общими для всех форм нашего приложения. А вот все формы в приложении уже наследуются от базовой.
К слову сказать, сегодня я обнаружил поведение, свойственное всем приложениям Delphi при включённой теме Aero в Windows (обычно я её отключаю, поэтому и не замечал ранее): если закрыть окно (с последующим уничтожением), то иконка в строке заголовка окна успеет сменится на дефолтовую. Это заметно как раз в тот момент, когда окно начинает уменьшаться и плавно исчезает с экрана. Естественно, это элементарно фиксится в базовой форме. И именно мысль “как здорово иметь базовую форму” побудила меня написать заметку, которую я уже давно планировал написать…
Базовая форма создаётся элементарно – отдельный модуль, в котором размещается наш класс (назовём его пока TMyBaseFrame = class(TForm)), и реализуется вся логика. Сложнее сделать так, чтобы эта форма попала в меню IDE Delphi –> File \ New. Это нужно, чтобы формы для своих приложений создавать в пару кликов, и чтобы IDE могла отобразить дополнительные published-свойства базовой формы. Для это придётся написать визард.
Я не стал изобретать велосипед, а просто сделал по образу и подобию того, как это сделано в устаревшей (уже, но всё ещё актуальной для Delphi 7), и бесплатной библиотеке TntComponents.
А далее, на основе базовой формы можно сделать ещё несколько базовых форм: форма со строкой состояния, форма с кнопками [OK]/[Отмена/Закрыть]/[Применить], форма с кнопками и набором вкладок (PageControl) – для этих форм можно не делать визард, а достаточно поместить их в репозитарий IDE (правой кнопкой на форме в дизайнере – Add to Repository).
Базовая фрейма
Базовая фрейма нужна для того же, для чего и базовая форма – возможность реализации общих вещей для всех фрейм в одном месте. Вообще, фрейма (правильно говорить “фрейм”, но пусть слово “фрейма” будет женского рода, Вы не против?) чаще всего используется, как контейнер для грида и панелей управления данными – фильтры, поиск и действия над табличными данными. Т.е. когда я в самом начала писал, что мол “создаём форму с гридом”, на самом деле я создаю фрейму с гридом. А фрейм уже легко поместить на базовую форму… хочешь – на форму с кнопкой “Закрыть”, хочешь – на форму со строкой состояния, а хочешь – на Master-Detail форму.
Исходники
Исходники пока не выкладываю. Ибо хочу:
- выкинуть лишнее и оставить только то, что будет полезно всем;
- накидать пару примеров и написать отдельную заметку/руководство;
- хочу опробовать выложить исходники в Git, чтобы не обновлять каждый раз архивы на файлообменнике.
Ваши комментарии:
- послужат дополнительным стимулом поскорее подготовить исходники;
- могут повлиять на содержание следующей заметки;
- интересно узнать Ваш ответ на озвученный вопрос.
11 коммент.:
То что описано в статье - правильно, но банально. А вот сделать статейку, без лишней воды, про создание визардов для форм, и нюансы помещения форм в репозитарий, было бы не плохо
Как у вас реализованы документы? Ведь форма документа может быть с одной стороны формой просмотра табличных частей, с другой стороны - формой редактирования шапки документа.
Прочитал. Жду продолжения. И да, про репозитарий и его бэкап тоже можно туда включить.
Да. Базовая форма - must have.
Когда я только начинал работать на своей текущей работе, то введение базовых форм было первым с чего я начинал.
Правда в моей базовой форме нет никаких GUI-контролов и никакой бизнес логики. И, самое главное, никакого визуального наследования.
На данный момент моя базовая форма делает следующее:
1) обходит все контролы/компоненты на форме, и унифицирует некоторые свойства
2) загружает/сохраняет размеры формы и некоторых свойств
3) переводит форму (у меня свой переводчик)
4) корректирует шрифты/стили
5) выставляет help-index-ы
6) объявляет основные методы, которые могут перекрываться в наследниках
Причём, почти вся работа делегируется внешнему классу реализующему интерфейс IMyFormProcessor. Типа такого:
ImyControlProcessor = interface(IMyCoreService)
['{GUID....}']
///
/// processes all controls and components on form
///
procedure ProcessForm(aForm:TForm);
///
/// processes all components on data module
///
procedure ProcessDataModule(aDataModule:TDataModule);
end;
Спасибо за комментарии.
У меня неделя началась напряжно, но постараюсь что-то начать делать и выкладывать куда-нибудь.
Думаю материал предоставить в таком порядке: вот базовая форма, в ней пока ничего нет, вот теперь мы внедрили её в дизайнер Delphi. А потом - наращиваем постепенно функционал базовой формы. А потом и про фрейму.
atruhin, банально, да не банально. Почему-то об этом почти никто не пишет, а если пишет - то вскользь. Данный пост можно считать началом мотивации написать что-то конкретное. И, может быть, подстроиться под публику.
Андрей, я не совсем понял суть вопроса. Обычно я делаю две вещи: а) фрейму с гридом - для отображения таблиц (список документов, к примеру), б) форму для редактирования конкретной записи. Если это документ, то в этой форме может быть несколько вкладок: "Основные" - шапка документа, "Дополнительные" - табличная часть, которая из себя представляет фрейму с гридом (как в пункте а). Код этой фреймы может вызывать фукнции создания/редактирования записей во внешней форме. Ну типа матрёшки получается. И нюансы надо учитывать, типа делать коммит сразу, или обновлять пакетом, зависит от ситуации, кратко не описать.
SnowSonic, спасибо.
Aleksey Timohin, читая твои комментарии и посты (и комментарии к постам:), меня не покидает чувство, что в наших проектах много похожего. И набор компонентов, и ведение исходников для двух версий сборок (Delphi7 + Delphi 2010). И базовую форму/фрейму я тоже внедрял чуть ли не первым делом, когда пришёл в компанию, где работаю уже 6 лет.
С интерфейсами я не соглашусь. Либо не совсем понял... я тоже думал об интерфейсах, а потом понял, что если у меня ВСЕ без исключения формы будут наследоваться от TMyBaseForm, то писать как-то так: (Form1 as TMyBaseForm).DoSomethingBase; быстрее, нежели чем запрашивать интерфейс у объекта... ну мне так показалось.
Кстати в базовой форме у меня есть всё из перечисленного, кроме help-индексов.
Кузан Дмитрий, Вы писали о том, что не всё хорошо со шрифтами - в дизайн-тайме одно, в ран-тайме - другое. Я предлагаю такой подход. Пользователь приложения может выбирать шрифт (и его размер) для всего приложения. По умолчанию - подставляется шрифт из темы оформления Windows. Когда форма открывается - загружается её положение на экране, размеры и устанавливается выбранный шрифт для формы. А у всех меток и контролов сказано: ParentFont = True (тем самым мне не надо явно перебирать все контролы и устанавливать у них шрифт).
Затем запускается обработчик (Event), в котором программист может дополнительно для нужных меток видоизменить шрифт - сделать его жирным/курсивом, увеличить/уменьшить.
Правда в дизайн-тайме, при таком подходе, шрифты менять нельзя. (мне кажется, что это меньшее зло)
Ещё я хочу в будущем написать про масштабирование - если у пользователя выбрано в настройках ОС dpi отличное от dpi, в котором "рисовалась" форма, Delphi (по крайней мере старые версии), не корректно масштабирует форму и все контролы в них, может получиться очень криво. На delphikingdom я как-то публиковал свой код в комментариях к http://www.delphikingdom.ru/asp/answer.asp?IDAnswer=48940 от 02-09-2008 04:48
ссылка. Там код не претерпел сильных изменений...
>Aleksey Timohin, ... меня не покидает чувство, что в наших проектах много похожего.
Подозреваю, что это типичная ситуация для проектов, которые начались в середине 90х и продолжают писаться до сих пор. Менялись только программисты. Кто-то опытнее, кто-то нет. Поэтому такие проекты могут похвастаться отсутствием архитектуры, зоопарком технологий, и обилием копипасты, и кучей ошибок, про которые все знают настолько давно, что уже перестали замечать. Ресурсов нет не только на переписывание всего с нуля, но даже и на полное тестирование.
>ВСЕ без исключения формы будут наследоваться от TMyBaseForm, то писать как-то так: (Form1 as TMyBaseForm).DoSomethingBase; быстрее, нежели чем запрашивать интерфейс у объекта... ну мне так показалось.
Если все без исключения, то да.
Но так не бывает. Т.е. в простых случаях да. Особенно если всё в одном приложении. Но как только начинаешь выносить код из проекта в общие библиотеки, приходится изворачиваться и придумывать как позволить этому коду нормально работать, не перетаскивая туда половину классов проекта. И тут уже приходится думать, либо выносить общий код в базовый класс, либо делать какой-то промежуточный класс Proxy/Connector, либо - интерфейсы. Вот тут кстати очень уместны идеи Dependency Injection/Inversion of Control (Delphi Spring) - и там тоже всё на интерфейсах.
К тому же, интерфейсы дают куда больше гибкости. Вместо формы, можно передавать и фрейм и компонент. Главное, чтобы он реализовывал нужный интерфейс.
Единственно неприятный момент, это переделывание старых классов (TObject) на работу с интерфейсами (TInterfacedObject). Там можно много граблей словить, если где по ошибке останется обращение к сущности как к объекту. Лучше даже вместо TInterfacedObject использовать TComponent и продолжать самостоятельно следить за памятью.
Меня немного смутила фраза "А фрейм уже легко поместить на базовую форму"... неправильно понял ее смысл. В целом я использую аналогичный подход.
Не пробовали пойти дальше и автоматизировать процесс создания таких приложений?
ps: В этом плане мне больше нравится Lazarus, т.к. там можно делать вложенные фреймы, что может упростить сопровождение GUI. В Delphi приходится делать компоненты для таких целей..
Андрей, фрейм можно встроить хоть в форму, хоть в фрейму. Тут ограничений нет, и Delphi тоже позволяет вкладывать фрейму в фрейму. Более того, можно и без фрейм обойтись - ничто не мешает встаривать форму в форму (правда только в рантайме). Поэтому я предпочитаю фреймы - их можно вкладывать друг в друга и в дизайнере.
> Не пробовали .. автоматизировать процесс создания..
У нас большое кол-во форм/фрейм создаётся автоматически по метаописанию (xml-файл - в нём хранится описание полей таблицы, поля-подстановки и доп. информация для грида/формы редактирования).
В теории, я представляю как создавать целые приложения по некоторому описанию, но на практике с этим не сталкивался.
> ..фрейм уже легко поместить на базовую форму..
Ну фрейм легко поместить на любую форму/фрейму. Но если на форме нет ничего, кроме самой фреймы, то такую форму можно и не создавать в дизайнере, достаточно вызвать что-то типа такого: CreateDialog(AOwner, TFrameClass). Я об этом планирую написать.
PS кстати вопрос как Вы в фрейме
работаете с сообщениями, например такой тривиальный код перехватывающий enter и заставляющий ее работать как Tab (передовать фокус на след.элемент) в форме работает а в фрейме увы нет, т.к обработка сообщений в фрейме не много не такая как в форме. Это существенный подводный камень
***
procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY;
public
{ Public declarations }
end;
implementation
{$R *.dfm}
{ TFrShablonFrame }
procedure TFrShablonFrame.CMDialogKey(var Message: TCMDialogKey);
begin
case Message.CharCode of
VK_RETURN : begin // следующий элемент
Perform(WM_NEXTDLGCTL,0,0);
end;
else
inherited;
end
end;
***
Дмитрий, спасибо за интересный вопрос. Действительно, до фреймы сообщение CM_DIALOGKEY может и не дойти. (я этого не знал:)
По логике вещей, CM_DIALOGKEY должен бы был попасть сначала на фрейму, если фокус ввода стоит на одном из контролов фреймы. А потом уже на форму.
Но в действительности, VCL посылает CM_DIALOGKEY не снизу-вверх, а сверху-вниз, т.е. от формы к дочерним контролам.
Однако перед посылкой этого сообщения, VCL посылает ещё одно сообщение - CM_CHILDKEY, вот оно то как раз отсылается в нужном порядке.
Откройте Controls.pas и проследите за методами TWinControl.CNKeyDown и TWinControl.CNSysKeyDown, думаю этот код ответит на вопрос.
Ещё раз спасибо, я эту замечание включу в одну из следующих заметок.
Отправить комментарий