понедельник, 14 января 2013 г.

А Вы ещё не используете базовую форму и базовую фрейму в своих Delphi-проектах?

В этой заметке я хочу рассказать о базовой форме и базовой фрейме. Что это такое, и зачем это нужно. Под словом “базовая” я подразумеваю, что любая форма/фрейма в проекте является наследником 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 коммент.:

atruhin комментирует...

То что описано в статье - правильно, но банально. А вот сделать статейку, без лишней воды, про создание визардов для форм, и нюансы помещения форм в репозитарий, было бы не плохо

Андрей комментирует...

Как у вас реализованы документы? Ведь форма документа может быть с одной стороны формой просмотра табличных частей, с другой стороны - формой редактирования шапки документа.

SnowSonic комментирует...

Прочитал. Жду продолжения. И да, про репозитарий и его бэкап тоже можно туда включить.

Aleksey Timohin комментирует...

Да. Базовая форма - 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 комментирует...

>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, думаю этот код ответит на вопрос.

Ещё раз спасибо, я эту замечание включу в одну из следующих заметок.

Отправить комментарий

.

.