среда, 6 марта 2013 г.

Form Events, события формы для работы с пользовательскими данными

Маленькие дети – это прекрасно. Но очень хлопотно… Эта заметка планировалась очень короткой, и без неё я пока не хочу продолжать развивать идею базовых формы и фреймы.

Итак, сделаем довольно простую вещь. Создадим приложение, которое наглядно проиллюстрирует, как правильно работать с формой в VCL. В частности, как правильно использовать обработчики событий OnCreate, OnShow, OnHide, OnClose и OnDestroy. Тут мысль простая – чтобы понять, какой обработчик использовать OnHide или OnClose (или какой-то другой) – нужно знать, в каком порядке и в каких случаях эти события происходят.

Создаём новое приложение: File \ New \ VCL Forms Application – Delphi. В новой форме в коде добавляем BaseForms в uses, объявление TForm1 = class(TForm) меняем на TForm1 = class(TBaseForm). Нажимаем последовательно F12, Alt+F12, Alt+F12. Теперь новая форма (которая будет главной) наследована от базовой.

Попутно отключаем опцию в Delphi  Tools \ Options –> Environment Options \ VCL Designer –> Auto create forms & data modules. Хотя, конечно, этого можно и не делать, но я эту опцию отключаю, т.к. считаю, что формы нужно создавать по мере необходимости, т.е. по требованию пользователя.

Теперь создаём вторую форму, она будет испытуемой. В этой форме прописываем обработчики событий OnCreate, OnShow и т.п., которые регистрируют в логе главной формы факт вызова обработчика.

У меня получилось такое приложение:

image

В голову приходят такие способы использования использования некоторой формы:

  • Показ формы в модальном режиме.
  • Показ формы в обычном режиме (как плавающее окно, например).
  • Показ формы в MDIChild стиле.

Модальный режим хорош тем, что основной поток выполнения программы приостанавливается, пока пользователь не закроет модальное окно. Т.е. это самое место спросить пользователя о чём-то критичном, что необходимо для дальнейшей работы приложения. Удобен этот режим и для редактирования полей записи БД. (Кстати, как правильно уничтожать модальную форму.) Чтобы увидеть форму в этом режиме жамкаем кнопки [Create] и [ShowModal]. Далее закрываем форму любым доступным образом: [X] (Alt+F4), [Modal OK] (Enter), [Close] (Escape). После этого можно снова вызвать [ShowModal], либо [Destroy].

Ещё одна примечательная особенность модального режима: события OnCloseQuery, OnClose и OnHide вызываются последовательно.

Обычный режим более дружественен пользователю. Но зато последовательность событий может быть не такой, как в модальном режиме. Чтобы увидеть форму в обычном режиме, вместо [ShowModal] жамкаем [Show]. Либо меняем [Visible]. Как и в ShowModal вызывается обработчик OnShow. Однако форму теперь можно закрыть, или скрыть не закрывая ([Hide], [Close], [Visible]). Это очень важно знать, что события OnCloseQuery и OnClose могут быть не вызваны.

Стиль MDIChild может быть использован, если MainForm имеет стиль MDIForm, т.е. если приложение спроектировано в стиле MDI. MDI стиль более строг к MDIChild форме, тут форму нельзя просто скрыть, т.е. обработчики OnCloseQuery и OnClose будут вызваны при попытке закрыть форму (Close, Alt+F4). Но они также могут и не быть вызваны, если приложение закрывается через Application.Terminate. OnHide вообще не будет вызван самой VCL, т.к. VCL запрещает скрывать MDIChild-формы. Это тоже надо знать.

В общем, покликав на кнопки в разном порядке видно, что для события OnCreate “обратным” является событие OnDestroy, а для OnShow – событие OnHide (правда помним, что в MDIChild-форме OnHide не вызывается). И ещё не совсем очевидно, как использовать обработчики событий OnCloseQuery и OnClose.

На самом деле тут так. VCL предлагает в OnCloseQuery спросить пользователя о необходимости закрыть окно (документ), и если пользователь передумал – корректно прервать это действие, выставив CanClose := False. Это можно сделать и в OnClose, задав CloseAction := caNone, однако VCL рекомендует это делать именно в OnCloseQuery. В частности потому, что при закрытии главной формы MDI приложения событие OnCloseQuery вызывается последовательно для всех MDIChild форм, и если хоть одна из них сказала “нет”, то происходит отмена закрытия главной формы, и все открытые документы останутся открытыми. Это наиболее распространённое (и ожидаемое пользователем) поведение для MDI приложения.

Ещё в OnClose можно сказать: caHide (т.е. скрыть, это действие является действием по умолчанию для обычного режима), caFree (т.е. закрыть, скрыть и освободить) и caMinimize (т.е. свернуть, это действие по умолчанию для MDIChild, т.к. такие формы VCL запрещает скрывать).

В своей практике я пришёл к выводу, что изменять действие по умолчанию в OnClose имеет смысл только на caFree, поэтому и появилось свойство FreeOnClose в базовой форме.

------------------------

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

Теперь посмотрим на то, как форма может взаимодействовать с вызывающей стороной. В модальном режиме можно использовать такую схему:

MyForm := TMyForm.Create
-> передача параметров в MyForm
MyForm.ShowModal
<- получаем результат от MyForm
MyForm.Free

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

В обычном режиме схема может быть такой:

MyForm := TMyForm.Create
-> передача параметров в MyForm
MyForm.Show

Здесь форма уже должна знать, куда и как сохранять результат своей работы. Т.е. форма должна знать о своём окружении, что в общем случае не есть хорошо. Чтобы изолировать код формы от вызывающей стороны, можно в качестве параметров в форму передавать некий интерфейс для чтения параметров и сохранения результата. В качестве интерфейса может выступать некоторый объект, либо CallBack-процедура и указатель на какие-то данные. Понятно, что интерфейс инициализируется вызывающей стороной. Т.е. получаем такую схему:

MyForm := TMyForm.Create
MyForm.SetUp(SomeInterface) либо MyForm.SetUp(@CallBack, @Data)

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

------------------------

Поспешный вывод по использованию обработчиков в самой форме может быть таким: в OnCreate создаём всё необходимое и достаточное, для работы формы, в OnDestroy – уничтожаем. В OnShow можно выполнять дополнительные действия, которые зависят от передаваемых в форму параметров. Например, загрузка пользовательских данных в форму из вне. В OnHide – сохраняем результат, если это действительно нужно.

Правда есть несколько нюансов.

Первое. Как на уровне обработчика OnHide определить, нужно ли сохранять данные формы? Ну ладно, определить можно, либо по свойству ModalResult, либо по какой-нибудь внутренней переменной, отвечающую за выбор пользователя. Но OnHide может и не быть вызван.

Второе. Если в OnShow формы произойдёт ошибка (при загрузке внешних данных), то форма всё равно будет отображена. А значит пользователь увидит форму с не проинициализированными пользовательскими данными. Аналогично и при ошибке в OnHide – форма скроется, а значит пользователь потеряет свою работу. Это всё усугубляется тем, что в обработчиках OnShow и OnHide VCL запрещает менять Visible формы.

Так что на самом деле не всё так просто, как казалось бы. Есть ещё и такая особенность – свойство OldCreateOrder. Если это свойство случайно установить в True, то могут возникнуть проблемы при взаимодействиями с компонентами на форме в OnCreate.

В общем вывод получается таким: если мы хотим, чтобы код формы не зависел от способа её отображения, стандартные обработчики формы – не место для работы с пользовательскими данными. Дальше я эту мысль постараюсь развить.

 

Вопрос: а как Вы используете обработчики формы? Напишите в комментариях, думаю это будет интересно всем.

 

Вы можете скачать скомпилированное приложение (zip-архив, 393 Кб), и сами поиграться с возможными сценариями вызова испытуемой формы.

Исходники доступны на этой странице.Ссылка для быстрого скачивания.

5 коммент.:

Всеволод Леонов комментирует...

>>если мы хотим, чтобы код формы не зависел от способа её отображения,

Какова вероятность (лучше с примером), что созданная форма как не-модальная будет показан в модальном режиме?

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

Николай Зверев комментирует...

Всеволод, вероятность зависит от архитектуры. Что заложишь, то и получишь.
Типичный пример: для некоторого справочника (таблица в БД) пишется форма (грид + фильтр + кнопки редактирования). Для MDI-приложения форма отображается как MDIChild. Если приложение SDI, эта же форма отображается в обычном режиме (с отдельной кнопкой в панели задач, для удобства). Но в тех случаях, когда пользователю надо выбрать элемент справочника (как параметр в другую форму), то форма со справочником отображается в модальном режиме (с кнопками [Выбрать] и [Отмена]).
Есть и обратные варианты - форма по умолчанию отображается в модальном режиме (к примеру - создание новой записи), но при включении специальной пользовательской опции форма становится обычной (т.е. можно на создавать N-записей не закрывая окна, иногда это очень удобно).
А вообще, я бы хотел почитать, как делают другие программисты.

Dmitro25 к сожалению свой комментарий удалил, хотя пример у него интересный. Если он позволит, я его восстановлю.

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

Да, поздновато я сюда вернулся, извините.
Если ещё актуально и Вы ждёте моего разрешения, то можете восстановить мой комментарий, Николай.

Николай Зверев комментирует...

11 марта 20013 г. Dmitro25 писал:
Я использую для загрузки данных в форму (особенно в главную форму приложения) событие OnActivate, при этом предусматриваю специальную переменную, чтобы действия по инициализации не выполнялись повторно при последующих вызовах OnActivate.
Если инициализацию проводить в OnCreate, то в случае возникновения ошибки и вывода модельного сообщения об ошибке, потом после инициализации форма не появляется на экране, а остаётся свёрнутой.
Если нужно выйти из приложения по результатам неверной инициализации в OnActivate, то использую PostMessage WM_CLOSE или WM_QUIT.

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

.

.