Сообщений 2 Оценка 45 [+1/-0] Оценить |
Исходные тексты примера к статье
Все мы любим иногда поболтать. Это человеческое свойство передалось программам, и они частенько у вас что-то спрашивают, а вы им что-то отвечаете, иногда невпопад. “Беседа”, правда, идет текстом, а не голосом. Так вот, разговор между программой и пользователем называется диалогом. Организация диалога — важнейшая часть любой программы. Ваша прямая обязанность сделать этот диалог приятным. По форме диалог прост — появляется окно с некоторым сообщением, полем для ввода вашего ответа и кнопкой OK. Вы внимательно читаете сообщение, набираете строку-ответ и нажимаете кнопку OK. Вот и все. Создатели среды Delphi предусмотрели все возможные типы диалогов и создали для вас ряд великолепных “домашних заготовок”.
В основе диалога между пользователем и компьютером лежит окно диалога (dialog box) — форма, содержащая компоненты для ввода данных: кнопки, текстовые поля, флажки, переключатели, списки и др. С помощью этих компонентов пользователь просматривает и вводит данные. В среде Delphi окно диалога создается на основе обычной формы.
Окна диалога могут работать в одном из двух режимов, монопольном (иногда говорят модальном, от англ. modal) и немонопольном (немодальном, от англ. modeless).
Монопольное окно диалога не дает пользователю возможности переключиться на другие окна программы до тех пор, пока работа с ним не будет завершена. Сразу заметим, что это не мешает пользователю переключаться на другие программы, например, с помощью панели задач Windows или нажатием комбинации клавиш Alt+Tab. Большинство окон диалога работает в монопольном режиме.
Немонопольные окна диалога предоставляют пользователю свободу выбора, позволяя вводить данные сразу в нескольких окнах.
Простейшим примером окна диалога является окно About ("О программе"). Как правило, оно открывается по команде меню Help | About... , работает в монопольном режиме и служит лишь для информирования пользователя. В предыдущей главе мы рассматривали программу PicView, там как раз не достает окна About. Исправим это упущение и на практике познакомимся с созданием простейших окон диалога.
Шаг 1. Запустите среду Delphi и откройте проект PictureViewer. Добавьте в главное меню пункт Help (программный идентификатор HelpMenuItem) с командой About... (программный идентификатор AboutMenuItem). По команде About... (рисунок 9.1) будет вызываться окно диалога About, которое мы дальше разработаем.
Рисунок 9.1. Пункт меню для вызова окна About
Шаг 2. Добавьте в проект новую форму, переименуйте ее в AboutForm и сохраните модуль под именем About.pas. Придайте форме нужные размеры и установите ее заголовок (свойство Caption) в значение About Picture Viewer. Далее сделаем из этой формы окно диалога.
Шаг 3. Обычная форма имеет много "фитюлек", которые совсем не нужны окну диалога, например раздвижную границу, меню управления окном, кнопки сворачивания и разворачивания окна. Чтобы их убрать, установите свойство BorderStyle в значение bsDialog (рисунок 9.2).
Рисунок 9.2. Превращение формы в окно диалога
Результат сделанного изменения проявится только во время работы программы и будет выражаться в следующем:
Шаг 4. Большинство монопольных окон диалога появляются в центре экрана. За это у формы отвечает свойство Position. Изначально оно равно poDesigned — форма появляется точно в том же месте, где она находится во время разработки. Чтобы центрировать форму на экране, установите свойство Position в значение poScreenCenter. Заметим, что другие значения свойства Position позволяют центрировать форму относительно главной формы, относительно формы-владельца или вообще не центрировать (см. параграф 7.3.4).
С формой разобрались, займемся компонентами. Окно About обычно содержит красивый рисунок, название программного продукта, замечания по поводу авторских прав или что-нибудь в этом роде и, конечно же, кнопку OK. Начнем с того, что добавим в форму кнопку.
Разрабатывая окно диалога, прежде всего необходимо обеспечить для пользователя возможность завершения диалога по окончание ввода данных. Вот тут как раз и нужны кнопки (buttons). В простейшем окне диалога, каким является окно About, достаточно всего одной кнопки (она обычно называется OK или Close). В более сложных окнах может понадобиться несколько кнопок. Например, в том случае, когда окно диалога принимает от пользователя данные, в него помещают кнопки OK и Cancel, которые позволяют пользователю подтвердить или отменить результат диалога.
Обычная кнопка создается с помощью компонента Button, расположенного в палитре компонентов на вкладке Standard.
Рисунок 9.3. Компонент Button
Характерные свойства этого компонента кратко описаны в таблице 9.1.
Свойство | Описание |
---|---|
Action | Команда, хранящаяся в компоненте ActionList и выполняемая при нажатии кнопки. |
Cancel | Если равно значению True, то кнопка срабатывает при нажатии клавиши Esc. |
Caption | Текст на кнопке. |
Default | Если равно значению True, то кнопка срабатывает при нажатии клавиши Enter. Исключением является ситуация, когда в окне диалога активна другая кнопка — в этом случае срабатывает она. |
ModalResult | При нажатии кнопки значение этого свойства копируется в одноименное свойство формы, что приводит к закрытию монопольного окна диалога. Однако для этого значение свойства должно отличаться от mrNone. |
WordWrap | Если равно значению True, то работает перенос слов. |
Текст на кнопке определяется значением свойства Caption. В тексте может присутствовать символ &, который не пишется на кнопке, а обеспечивает подчеркивание следующего за ним символа. Нажатие подчеркнутого символа на клавиатуре в комбинации с клавишей Alt вызывает срабатывание кнопки. Например, если свойство Caption содержит строку &Yes, то на кнопке будет написано Yes, и для нажатия кнопки можно воспользоваться комбинацией клавиш Alt+Y.
Пользователи, привыкшие работать с клавиатурой, знают, что одна из кнопок в окне диалога срабатывает при нажатии клавиши Enter. Это происходит при условии, что кнопка содержит значение True в свойстве Default. Такая кнопка, как правило, содержит текст OK и внешне отличается от остальных наличием жирной рамки.
Одна из кнопок окна диалога может срабатывать при нажатии клавиши Esc. Это происходит, если кнопка содержит значение True в свойстве Cancel. Как правило, такая кнопка подписывается текстом "Отмена" (Cancel).
Когда пользователь нажимает кнопку, в компоненте Button происходит событие OnClick. В обработчике этого события вы можете завершить диалог. Завершение немонопольного диалога выполняется вызовом метода Close у объекта формы. Завершение монопольного окна диалога выполняется установкой ненулевого значения в свойстве ModalResult формы. Впрочем, без этого можно обойтись, если воспользоваться свойством ModalResult компонента Button (мы не ошиблись, это свойство имеет и форма, и кнопка). Свойство ModalResult компонента Button устанавливается в окне свойств и по умолчанию равно значению mrNone. Если вы выберите другое значение, то нажатие кнопки будет вызывать автоматическое завершение диалога с копированием этого значения в свойство ModalResult формы. Анализируя свойство ModalResult после завершения диалога, программа узнает, какую кнопку нажал пользователь и в соответствии с этим направляет работу программы в нужное русло.
Помните, что свойство ModalResult работает только в монопольных окнах диалога.
Шаг 5. Перейдем теперь от теории к практике и создадим в нашем окне About кнопку OK (рисунок 9.4). Для этого выберите в палитре компонентов компонент Button, опустите его в форму и установите его свойства следующим образом:
Рисунок 9.4. Кнопка OK в окне About
Каждый из нас в душе художник. Поэтому рано или поздно стандартные невзрачные кнопки, содержащие лишь “голый” текст, перестанут вам нравиться. Появится естественное желание их как-то приукрасить. В этом случае мы советуем вам вместо компонента Button воспользоваться компонентом BitBtn. Он расположен в палитре компонентов на вкладке Additional.
Рисунок 9.5. Компонент BitBtn
Компонент BitBtn обладает теми же возможностями, что и компонент Button, но кроме текста может содержать значок, который придает кнопке более привлекательный вид. По сравнению с компонентом Button компонент BitBtn имеет некоторые новые свойства, которые отражены в таблице 9.2.
Свойство | Описание |
---|---|
Glyph | Значок на кнопке. |
NumGlyphs | Количество вариантов значка. Компонент делит рисунок Glyph по горизонтали на заданное количество значков и рисует один из них в зависимости от состояния кнопки. |
Layout | Положение значка относительно текста: blGlyphLeft — слева, blGlyphRight — справа, blGlyphTop — сверху, blGlyphBottom — снизу. |
Margin | Расстояние от границы кнопки до значка. Если оно равно -1, то значок вместе с текстом центрируются на кнопке. |
Spacing | Расстояние от значка до текста. Если оно равно -1, то текст центрируется между значком и границей кнопки. |
Kind | Задает кнопку стандартного вида. Упрощает создание таких стандартных кнопок, как OK, Cancel, Yes, No, Close, Abort, Retry, Ignore, All, Help. |
Заметим, что кнопка, содержащая значок, принимает наиболее красивый вид, если свойство Margin равно значению 4, а свойство Spacing равно значению 1.
С помощью компонента BitBtn стандартные кнопки OK, Cancel, Yes, No, Close, Abort, Retry, Ignore, All и Help создаются проще, чем при использовании компонента Button. Для этого в свойстве Kind достаточно выбрать одно из значений, приведенных в таблице 9.3.
Значение | Вид кнопки | Результат установки значения | Пояснения |
---|---|---|---|
bkOK | Caption = 'OK' Default = True ModalResult = mrOK |
Кнопка, подтверждающая ввод данных. | |
bkCancel | Caption = 'Cancel' Cancel = True ModalResult = mrCancel |
Кнопка, отменяющая ввод данных. | |
bkYes | Caption = '&Yes' Default = True ModalResult = mrYes |
Кнопка для положительного ответа на вопрос. | |
bkNo | Caption = '&No' Cancel = True ModalResult = mrNo |
Кнопка для отрицательного ответа на вопрос. | |
bkAll | Caption = '&All' ModalResult = mrAll |
Кнопка для положительного ответа на все вопросы. | |
bkAbort | Caption = 'Abort' ModalResult = mrAbort |
Кнопка для прерывания операции. | |
bkRetry | Caption = '&Retry' ModalResult = mrRetry |
Кнопка для повторения операции. | |
bkIgnore | Caption = '&Ignore' ModalResult = mrIgnore |
Кнопка для игнорирования произошедших изменений и продолжения начатой операции. | |
bkHelp | Caption = '&Help' | Кнопка для вызова справочника. Вы можете вызывать справочник в обработчике события OnClick или возложить эту работу на среду Delphi, установив у формы свойство HelpContext в нужное значение. | |
bkClose | Caption = '&Close' | Кнопка, закрывающая форму. | |
bkCustom | Любой | Любой | Кнопка для ваших собственных целей. |
Например, если вам нужна кнопка OK, установите свойство Kind в значение bkOK. В результате на кнопке появится зеленая "галочка" и текст "OK", свойство Default получит значение True и свойство ModalResult получит значение mrOK.
Хорошее приложение должно быть не только функционально, но и эстетично. Как говорится, хорошее блюдо должно быть красиво подано. Это относится и к нашему замечательному приложению, и конечно к его окну About. А что может лучше украсить окно About, чем яркий, запоминающийся рисунок.
Шаг 6. Как вы уже знаете, рисунок создается с помощью компонента Image, расположенного в палитре компонентов на вкладке Additinal. Выберите этот компонент и поместите его в форму AboutForm.
Шаг 7. Установите свойство AutoSize в значение True, чтобы компонент автоматически подгонял свои размеры под размеры рисунка, и установите свойство Transparent в значение True, чтобы рисунок отображался с прозрачным фоном.
Рисунок 9.6. Прозрачный фон для рисунка задается установкой свойства Transparent в значение True
Шаг 8. Чтобы установить рисунок, перейдите к свойству Picture и нажатием кнопки с многоточием откройте окно Picture Editor. Это окно должно быть вам уже знакомо. Загрузите файл Athena.bmp из папки "C:\Program Files\Common Files\Borland Shared\Images\Splash\16Color". Рисунок появится в форме (рисунок 9.7).
Рисунок 9.7. В компоненте Image загружен рисунок
Сейчас мы подошли к созданию текстовых надписей в окне About. Они будут сообщать пользователю о названии программы и средствах ее разработки. Для нашей учебной задачи этого достаточно, в реальных приложениях окно About может дополнительно содержать замечание об авторских правах, серийный номер и другую регистрационную или лицензионную информацию.
Следуя традиции, напишем название программы большими жирными буквами, а название средства разработки — обычным мелким шрифтом. Для этого воспользуемся компонентом Label, который находится в палитре компонентов на вкладке Standard.
Рисунок 9.8. Компонент Label
Характерные свойства компонента Label кратко описаны в таблице 9.4.
Свойство | Описание |
---|---|
Align | Способ выравнивания в пределах содержащего компонента. |
Alignment | Выравнивание текста в пределах компонента: taLeftJustify — прижат к левой границе, taRightJustify — прижат к правой границе, taCenter — центрирован. |
AutoSize | Если равно значению True, то размеры компонента автоматически подгоняются по ширине и высоте текста. |
Caption | Текст надписи. С помощью символа & в тексте может быть задана “горячая” клавиша. |
FocusControl | Компонент формы, получающий фокус ввода при нажатии “горячей” клавиши. |
ShowAccelChar | Если равно значению True, то записанный в тексте символ & транслируется в подчеркивание следующего за ним символа. Подчеркнутый символ используется в комбинации с Alt как “горячая” клавиша. |
Transparent | Если равно значению True, то фон надписи является прозрачным. Прозрачный фон полезен при наложении надписи на рисунок. |
WordWrap | Если равно значению True, то работает перенос слов. |
Компонент Label отображает нередактируемый текст, хранящийся в свойстве Caption. Текст выравнивается в пределах компонента одним из трех способов: по левому краю, по правому краю или по центру. Способ выравнивания определяется свойством Alignment. Если текст надписи слишком велик, можно организовать его вывод в несколько строк (с переносом слов). Для этого достаточно установить свойство WordWrap в значение True. Еще одна удобная возможность — автоматическая подгонка размеров компонента по ширине и высоте текста. Она контролируется свойством AutoSize и по умолчанию включена.
Фон надписи можно сделать прозрачным, установив свойство Transparent в значение True. Такую надпись можно вынести наверх графического изображения, в результате получится текст на фоне рисунка.
С помощью компонента Label часто создаются подсказки к другим компонентам, в частности к полям ввода. При этом свойство Caption содержит не только текст, но и “горячую” клавишу, при выборе которой активизируется связанный с надписью компонент. Активизируемый компонент указывается в свойстве FocusControl.
Шаг 9. Вспомним, что компонент Label понадобился нам для того, чтобы сделать необходимые надписи в окне About. Опустите в форму первую надпись справа от изображения и установите ее свойства следующим образом:
Рисунок 9.9. Надпись выполнена с помощью компонента Label
Шаг 10. После этого поместите в форму еще один компонент Label с текстом "Developed in Delphi" в свойстве Caption.
Окно диалога почти готово, но для полного ажура не хватает одной мелочи — рельефной канавки вокруг рисунка и надписей (это придаст окну законченность). Для решения подобных задач служит компонент Bevel, расположенный в палитре компонентов на вкладке Additional.
Рисунок 9.10. Компонент Bevel
Шаг 11. Поместите в форму компонент Bevel, придайте ему нужные размеры и положение, после чего установите свойство Shape в значение bsFrame.
Рисунок 9.11. Компонент Bevel в форме
Рельефные канавки удобно создавать с помощью компонента Bevel. Однако компонент Bevel не может быть контейнером для других компонентов, а следовательно, перемещение рамки не вызывает перемещение компонентов, находящихся внутри нее. Если нужна не просто рамка, а контейнер с рамкой, то пользуются рельефной панелью — компонентом Panel (вкладка Standard панели инструментов).
Рисунок 9.12. Компонент Panel
Отличительные свойства компонента Panel сведены в таблицу 9.5.
Свойство | Описание |
---|---|
Align | Способ выравнивания панели в пределах владельца. |
BevelInner | Внутренний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelOuter | Внешний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelWidth | Ширина скосов рельефной рамки. |
BorderWidth | Расстояние в пикселях между внутренним и внешним скосами. |
BorderStyle | Определяет, имеет ли панель рамку. |
Caption | Текст на панели. |
DockSite | Определяет, используется ли панель для стыковки других компонентов. |
FullRepaint | Свойство сохранено для совместимости с предыдущими версиями библиотеки VCL. Оно не влияет на способ перерисовки компонента. |
Locked | Если равно False, то OLE-серверу разрешено заменить панель на свою панель инструментов. Если равно True и панель выравнена по какой-нибудь стороне формы, то она остается нетронутой при активизации OLE-сервера по месту. |
UseDockManager | Включает режим автоматического размещения стыкуемых компонентов на панели. Компоненты стыкуются методом деления панели по горизонтали и вертикали. Если свойство равно значению False, то программист должен сам позаботиться о размерах и местоположении стыкуемых компонентов, определив обработчики событий OnGetSiteInfo и OnDockDrop. |
OnCanResize | Происходит при попытке изменить размеры панели. Запрос на изменение размеров может исходить от пользователя. Устанавливая значение параметра Resize, обработчик события OnCanResize может разрешить или запретить действительное изменение размеров панели. |
OnConstrainedResize | Происходит при изменении размеров панели и позволяет на лету изменять ее минимальные и максимальные размеры. |
OnGetSiteInfo | Происходит, когда у компонента запрашивается место для стыковки. |
Шаг 12. Уберите компонент Bevel из формы и поместите на его место компонент Panel. Откорректируйте его положение и размеры и установите свойства следующим образом (рисунок 9.13):
Caption = <пусто>
BevelInner = bvRaised
BevelOuter = bvLowered
Рисунок 9.13. Компонент Panel заменил в форме компонент Bevel
Шаг 13. С помощью окна Object TreeView перенесите компоненты Image1, Label1 и Label2 на панель Panel1 (рисунок 9.14).
Рисунок 9.14. Компоненты Image1, Label1 и Label2 переносятся на панель Panel1
Теперь рельефная рамка заменена рельефной панелью и при ее перемещении перемещаются все надписи и рисунок (рисунок 9.15).
Рисунок 9.15. Компоненты Image1, Label1 и Label2 — теперь на панели Panel1
Будьте аккуратны при удалении панели! Вместе с ней всегда удаляются внутренние компоненты.
Визуальное проектирование окна About закончено, осталось обеспечить его вызов при выборе пользователем команды Help | About... главного меню. Для этого нужно сделать следующее:
Шаг 14. Как реализовать первый пункт плана вы уже знаете, поэтому мы не будем на нем останавливаться, и сразу перейдем ко второму пункту плана. Активизируйте PictureForm, а затем выберите в меню File команду Use Unit... . На экране появится окно (рисунок 9.16):
Рисунок 9.16. С помощью окна Use Unit модуль About подключается в модуле Main
Выберите в этом окне модуль AboutUnit и щелкните на кнопке OK. Модуль AboutUnit, содержащий определение формы AboutForm, подключится в модуле MainUnit, содержащем определение главной формы PictureForm. Чтобы в этом убедиться, перейдите к редактору кода. В разделе implementation модуля MainUnit вы обнаружите строку
uses AboutUnit;
|
Ее можно было бы набрать и вручную, но мы решили продемонстрировать вам еще одну возможность визуальной среды программирования.
Шаг 15. Выполните теперь второй пункт плана — создайте обработчик события OnExecute для команды AboutAction компонента ActionList:
procedure TPictureForm.AboutActionExecute(Sender: TObject); begin AboutForm.ShowModal; end; |
Метод ShowModal запускает окно диалога в монопольном режиме и возвращает управление только после его завершения. В нашем примере методвызывается как процедура, но в действительности это функция, которая возвращает код завершения диалога. Код берется у формы из свойства ModalResult. Напомним, что для выполнения диалога в немонопольном режиме его нужно вызвать с помощью метода Show, а не метода ShowModal.
Шаг 16. Выполните компиляцию и запустите программу, затем проверьте работу новоиспеченного окна диалога, выполнив команду меню Help | About... (рисунок 9.17).
Рисунок 9.17. Диалоговое окно в работе
Все работает правильно, остается только выяснить один вопрос: где, когда и кем конструируется объект AboutForm. Ведь никаких усилий мы для этого не предпринимали, и тем не менее использовали объект при вызове метода ShowModal как уже существующий.
Объект AboutForm создается при запуске программы и существует на протяжении всей ее работы. В этом можно убедиться, заглянув в главный файл программы с помощью команды меню View | Project Source. В главном программном блоке вы найдете оператор:
Application.CreateForm(TAboutForm, AboutForm); |
Он-то и обеспечивает "автоматическое" создание объекта формы. Это удобно, но имеет и отрицательные стороны, так как память, выделенная объекту остается занятой даже тогда, когда форма невидима, т.е. до запуска диалога и после его завершения. Если программа имеет одну-две формы, это не так важно, а если много? В этом случае от автоматического создания всех форм, кроме главной, лучше отказаться.
Шаг 17. Чтобы исключить автоматическое создание формы AboutForm, откройте окно Project Options и на вкладке Forms отбуксируйте элемент AboutForm из списка Auto-create forms в список Available forms (рисунок 9.18):
Рисунок 9.18. Форма AboutForm исключена из списка автоматически создаваемых форм
В результате среда Delphi удалит приведенный выше оператор из главного файла программы, переложив заботу о создании формы на ваши плечи.
Шаг 18. Разумеется, что после сделанных действий метод AboutItemClick, из которого вызывается окно диалога, нужно переписать:
procedure TPictureForm.AboutActionExecute(Sender: TObject); begin AboutForm := TAboutForm.Create(Self); try AboutForm.ShowModal; finally AboutForm.Free; end; end; |
Ну вот, теперь ресурс оперативной памяти используется более рационально. Кстати, обратите внимание, как обеспечивается защита объекта AboutForm от исключительных ситуаций, которые могут возникнуть в период его работы (это конечно маловероятно, но чего в этой жизни не бывает!). Если объект AboutForm успешно создается, то благодаря оператору try...finally...end он всегда корректно освобождается, даже в случае возникновения исключительной ситуации.
Вы получили первое представление об окнах диалога, научились создавать и выполнять простейшие из них. Но вы еще не знаете, как организовать диалог для получения данных от пользователя. Эта задача решается с помощью компонентов для ввода данных, к изучению которых мы сейчас приступаем.
Использование компонентов для ввода данных рассмотрим на примере приложения Alarms. Эта полезная программа позволит создать список будильников для уведомления о предстоящих событиях. По сигналу будильника в заданные время и день появится окно с сообщением и прозвучит сигнал. Список будильников будет отображаться в главном окне программы, а установка их параметров будет выполняться в модальном окне диалога. В процессе разработки этого приложения вы познакомитесь с такими компонентами, как CheckBox, RadioButton, ComboBox, ListBox, GroupBox, Edit, MaskEdit и некоторыми другими. Итак, приступим.
Шаг 1. Сначала приготовьте новый проект с пустой формой, выбрав команду меню File | New Application. Дайте форме идентификатор MainForm, скорректируйте ее размеры и установите следующие значения свойств:
Сохраните модуль формы под именем MainUnit.pas, а проект — под именем Alarms.dpr.
Шаг 2. В форме MainForm будет отображаться список будильников. Для управления списком нужны кнопки: New, Edit и Delete. Для быстрого и удобного закрытия формы нужна еще кнопка Close. Поэтому поместите в форму соответствующее число компонентов Button (с идентификаторами NewButton, EditButton, DeleteButton, CloseButton) и задайте для них надписи, размеры и положение как на рисунке 9.19.
Рисунок 9.19. Кнопки для управления списком будильников
Шаг 3. Установите в компоненте NewButton свойство Default в значение True, чтобы кнопка срабатывала при нажатии клавиши Enter.
Шаг 4. Для компонента CloseButton создайте следующий обработчик события OnClick:
procedure TMainForm.CloseButtonClick(Sender: TObject); begin Close; end; |
Шаг 5. Необходимые подготовительные операции сделаны. Теперь перейдем к разработке окна диалога, предназначенного для ввода параметров будильника. Это окно будет вызываться при нажатии кнопок New и Edit. С этой целью добавьте в проект новую форму, дайте ей идентификатор AlarmDetailsForm, скорректируйте размеры и установите следующие значения свойств:
Теперь сохраните модуль формы под именем AlarmDetailsUnit.pas.
Шаг 6. Добавьте в форму кнопки OK и Cancel и установите их свойства так, как показано на рисунке 9.20.
Рисунок 9.20. Стандартные кнопки OK и Cancel и их свойства
Шаг 7. Теперь займемся размещением компонентов для ввода данных. Прежде всего подумаем, какие параметры должны устанавливаться в диалоге. К ним относятся: текстовое сообщение, которое появится по сигналу будильника, время сигнала с точностью до минуты, признак того, нужно ли проигрывать звуковой сигнал, периодичность выдачи сигналов (ежедневно, в заданный день недели или в конкретный день). Учли все? Вроде бы, да. Тогда разместите в форме компоненты, обеспечивающие ввод перечисленных параметров (рисунок 9.21).
Рисунок 9.21. Эскиз окна для установки параметров будильника
Эскиз окна диалога создан (с помощью выносок на рисунке пояснены названия компонентов). Окинув его взором, вы обнаружите, что знакомых вам компонентов немного — это Bevel, Label и Button. Зато новых — хоть отбавляй: CheckBox, RadioButton, Edit, MaskEdit, GroupBox, ComboBox, DateTimePicker. Однако не пугайтесь, мы обо всех расскажем, и вы убедитесь, что обращаться с ними вовсе не сложно.
Во время работы программы только один из компонентов принимает клавиатурный ввод в текущий момент времени. Принято говорить, что такой компонент обладает фокусом ввода или просто — активен. Передача фокуса ввода осуществляется щелчками компонентов или нажатием клавиш Tab и Shift+Tab на клавиатуре. При использовании клавиатуры фокус ввода передается последовательно от одного компонента другому, причем клавиша Tab обеспечивает перебор элементов в прямом порядке, а сочетание клавиш Shift+Tab – в обратном.
Очередность, в которой компонент получает фокус ввода, задается его свойством TabOrder. Свойство TabOrder действует относительно содержащего компонента, например очередность переборакомпонентов MessageEdit и TimeMaskEdit задается относительно формы, а очередность перебора компонентов WeeklyComboBox и DatePicker — относительно компонента GroupBox.
Если нужно исключить компонент из очереди на фокус ввода, установите свойство TabStop в значение False. Однако в этом случае фокус ввода можно насильно передать компоненту с помощью мыши или «горячей» клавиши.
Изначально порядок перебора соответствует порядку добавления компонентов в форму, но его можно изменить, устанавливая значения свойства TabOrder в компонентах. Если компонентов в окне диалога очень много, то это занятие может стать довольно утомительным. В этом случае удобнее пользоваться окном Edit Tab Order, которое открывается по команде меню Edit | Tab Order... (рисунок 9.22).
Рисунок 9.22. В окне Edit Tab Order выставляется порядок перебора компонентов формы
Шаг 8. В этом окне задайте порядок перебора компонентов формы AlarmDetailsForm таким, как показан на рисунке.
Ну хорошо, скажете вы, допустим, порядок перебора задан. А как управлять передачей фокуса ввода программно? Очень просто. Чтобы передать фокус ввода требуемому компоненту, у него нужно вызвать метод SetFocus. Кстати при управлении фокусом ввода очень полезным может оказаться свойство формы ActiveControl, которое указывает активный компонент. Это свойство доступно в окне свойств и часто используется для указания компонента, который первым получит фокус ввода (в обход номера очереди). Если значение свойства не задано, то первым фокус ввода получает компонент, в котором значение свойства TabOrder равно нулю.
Шаг 9. Хотя окно диалога Alarm Details еще не готово, вам, наверное, не терпится его опробовать и убедиться, что перебор компонентов происходит в нужном порядке. Для этого нужно связать выполнение диалога с нажатием в главной форме кнопки New.... Поэтому подключите модуль AlarmDetails в модуле Main и определите следующий обработчик события OnClick для кнопки NewButton:
procedure TMainForm.NewButtonClick(Sender: TObject); begin AlarmDetailsForm := TAlarmDetailsForm.Create(Self); try AlarmDetailsForm.ShowModal; finally AlarmDetailsForm.Free; end; end; |
Шаг 10. Теперь выполните компиляцию и запустите программу. В окне Clock Alarms нажмите кнопку New... . Вашему взору предстанет окно диалога Alarm Settings (рисунок 9.23).
Рисунок 9.23. Рабочий прототип окна для установки параметров будильника
Диалог запускается, фокус ввода перемещается в нужном порядке, но в целом компоненты работают не так, как надо. Ничего удивительного, ведь мы ими почти не занимались. Поэтому дальше мы приступаем к детальному изучению различных типов компонентов. На этом пути полигоном для испытаний нам послужит окно диалога Alarm Details.
Переключатели (check boxes) используются для установки параметров, характеризуемых двумя значениями – "Да" и "Нет" (True и False). Переключатели создаются с помощью компонента CheckBox, расположенного в палитре компонентов на вкладке Standard (рисунок 9.24).
Рисунок 9.24. Компонент CheckBox
Характерные свойства этого компонента собраны в таблице 9.6.
Свойство | Описание |
---|---|
Alignment | Определяет, с какой стороны от переключателя находится текст: taRightJustify – справа, taLeftJustify – слева. |
AllowGrayed | Если равно True, то переключатель имеет три состояния. |
Caption | Текст рядом с переключателем. |
Checked | Определяет, включен ли переключатель. |
State | Содержит текущее состояние переключателя. |
WordWrap | Если равно значению True, то работает перенос слов. |
Обычно переключатель имеет два состояния: включен или выключен. Текущее состояние определяется значением свойства Checked. Если оно равно значению True, то переключатель включен, иначе — выключен. Бывает, что переключатель имеет еще и третье состояние — неопределенное (grayed). В этом состоянии переключатель закрашивается серым цветом. Если переключатель имеет три состояния, то вместо свойства Checked используется свойство State, а в свойстве AllowGrayed (разрешает неопределенное состояние) устанавливается значение True.
Когда при работе программы пользователь щелкает переключатель, в нем изменяются значения свойств Checked и State, а также происходит событие OnClick. Обрабатывая это событие, можно установить любую зависимость между состоянием переключателя и состоянием других компонентов.
В нашем примере компонент CheckBox используется для установки параметра Play Sound, управляющего выдачей звукового сигнала (рисунок 9.25). Мы назвали его SoundCheckBox.
Рисунок 9.25. Переключатель Play Sound представлен компонентом CheckBox
Шаг 11. Чтобы при первом появлении окна диалога, режим Play Sound был включен, установите в компоненте SoundCheck свойство Checked в значение True.
Взаимоисключающие переключатели (radio buttons) служат для установки взаимоисключающих параметров. Они обычно объединяются в группы и позволяют пользователю выбрать одно значение из фиксированного (причем немногочисленного) набора вариантов. При включении одного такого переключателя остальные переключатели этой же группы выключаются.
Группа переключателей создается с помощью нескольких компонентов RadioButton. Компонент RadioButton находится в палитре компонентов на вкладке Standard (рисунок 9.26).
Рисунок 9.26. Компонент RadioButton
Характерные свойства компонента RadioButton описаны в таблице 9.7.
Свойство | Описание |
---|---|
Alignment | Определяет, с какой стороны от переключателя находится текст: taRightJustify — справа, taLeftJustify — слева. |
Caption | Текст рядом с переключателем. |
Checked | Определяет, включен ли переключатель. |
WordWrap | Если равно значению True, то работает перенос слов. |
В форму всегда помещаются несколько компонентов RadioButton, соответствующих возможным значениям устанавливаемого параметра. Например, в форме AlarmDetailsForm содержится три таких компонента (EverydayRadioButton, WeeklyRadioButton и DateRadioButton), которые используются для выбора периодичности срабатываний будильника (рисунок 9.27).
Рисунок 9.27. Переключатели Everyday, Weekly и Date представлены компонентами RadioButton
Внимание! Выбор положения переключателя осуществляется с помощью клавиш со стрелками.
Шаг 12. Текущее состояние переключателя контролируется свойством Checked. Если в одном переключателе оно устанавливается в значение True, то во всех остальных переключателях этой же группы оно устанавливается в значение False. Установите в компоненте EverydayRadioBox свойство Checked в значение True — этот переключатель будет изначально включен.
Шаг 13. Когда пользователь щелкает переключатель, в соответствующем компоненте RadioButton свойство Checked получает значение True и происходит событие OnClick. Обрабатывая это событие, можно установить любую зависимость между состоянием переключателя и состоянием других компонентов формы. Например, когда включен режим Everyday, компоненты для ввода дня недели и даты должны быть недоступны. Когда включен режим Weekly on, должен быть доступен выбор дня недели, но недоступна установка даты. Наконец, когда включен режим Date, установка дня недели должна быть недоступна, а установка даты — доступна. Чтобы достигнуть такой согласованности в работе, определите для переключателей EverydayRadioButton, WeeklyRadioButton и DateRadioButton единый обработчик события OnClick:
procedure TAlarmDetailsForm.RecurringRadioButtonClick(Sender: TObject); begin WeeklyComboBox.Enabled := Sender = WeeklyRadioButton; DatePicker.Enabled := Sender = DateRadioButton; end; |
Шаг 14. Разумеется, компоненты WeeklyComboBox и DatePicker первоначально должны быть недоступны. Поэтому, удерживая клавишу Shift, выберите названную группу компонентов и установите свойство Enabled в значение False.
Теперь запустите программу и проверьте правильность ее работы.
Для быстрой организации группы взаимоисключающих переключателей очень удобен компонент RadioGroup, расположенный в палитре компонентов на вкладке Standard (рисунок 9.28).
Рисунок 9.28. Компонент RadioGroup
Его характерные свойства кратко описаны в таблице 9.8.
Свойство | Описание |
---|---|
Align | Способ выравнивания в пределах содержащего компонента. |
Caption | Подпись к группе переключателей. |
Columns | Число колонок в группе переключателей. |
ItemIndex | Номер выбранного элемента, начиная с нуля. Если все переключатели находятся в выключенном состоянии, то значение свойства равно -1. |
Items | Подписи к переключателям. |
Компонент RadioGroup удобен тем, что заменяет группу компонентов RadioButton. Расположение переключателей, которые он отображает, подбирается автоматически с учетом заданного в свойстве Columns количества колонок. Номер активного зависимого переключателя хранится в значении свойства ItemIndex. Следующий рисунок 9.29 не относится к приложению Alarms, а просто поясняет, что такое компонент RadioGroup:
Рисунок 9.29. Четырехпозиционный переключатель представлен компонентом RadioGroup
Компонент RadioGroup создает группу, состоящую исключительно из переключателей. Однако он не годится в тех случаях, когда в группе должны присутствовать другие компоненты, что как раз имеет место в нашем примере. В этой ситуации для группировки компонентов применяется компонент GroupBox.
Компонент GroupBox служит для группировки компонентов, он расположен в палитре компонентов на вкладке Standard (рисунок 9.30).
Рисунок 9.30. Компонент GroupBox
Компонент GroupBox выглядит как панель с заголовком рисунок 9.31. Текст заголовка задается в свойстве Caption.
Рисунок 9.31. Панель с заголовком представлена компонентом GroupBox
Компонент GroupBox может содержать в себе другие компоненты. Это, например, означает, что установка его свойства Visible в значение False прячет группу со всеми расположенными внутри компонентами, а не только рамку с заголовком.
Для ввода текста предназначены компоненты Edit и Memo. Они представляют собой соответственно поле ввода и редактор многострочного текста (кроме них существует еще компонент RichEdit, предназначенный для ввода и отображения форматированного текста, но о нем мы поговорим отдельно).
Поле ввода (Edit) служит для ввода различных слов, фраз и другого относительно короткого текста. Он не имеет полос прокрутки, но разрешает прокручивать текст по горизонтали клавишами перемещения курсора влево и вправо. Компонент Edit расположен в палитре компонентов на вкладке Standard (рисунок 9.32).
Рисунок 9.32. Компонент Edit
Характерные свойства компонента Edit описаны в таблице 9.9.
Свойство | Описание |
---|---|
AutoSelect | Если равно значению True, то при активизации редактора находящийся в нем текст автоматически выделяется. |
AutoSize | Если равно значению True, то высота редактора автоматически подгоняется по высоте текста. |
BevelEdges | Вложенные свойства beLeft, beTop, beRight и beBottom определяют видимость соответственно левой, верхней, правой и нижней сторон рельефной рамки. |
BevelInner | Внутренний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelKind | Вид рельефной рамки: bkNone — рамки нет, bkTile — рамка с четкими скосами, bkSoft — рамка со сглаженными скосами, bkFlat — плоская рамка (без скосов). |
BevelOuter | Внешний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
CharCase | Преобразует текст к прописным или строчным буквам: ecUpperCase – к прописным буквам, ecLowerCase – к строчным буквам, ecNormal –преобразование символов не выполняется. |
HideSelection | Если равно значению True, то при потере редактором активности выделение текста снимается. |
MaxLength | Максимальное количество символов, которое пользователь может ввести. Если оно равно 0, то пользователь может ввести текст неограниченной длины. |
OEMConvert | Если равно значению True, то символы текста преобразуются в кодовую таблицу OEM. |
PasswordChar | Если не равно #0, то указанный в этом свойстве символ отображается вместо каждого символа текста. Применяется для ввода пароля. |
ReadOnly | Если равно значению True, то пользователь не сможет изменить текст в редакторе. |
Text | Редактируемый текст. |
OnChange | Происходит при изменении текста. |
Редактируемый текст содержится в свойстве Text. Его максимальная длина определяется значением свойства MaxLength.
Иногда компонент Edit используется для отображения нередактируемого текста. Для этого свойство ReadOnly устанавливается в значение True. Вы спросите, чем он в таком состоянии лучше компонента Label? А тем, что может получать фокус ввода. Кроме того, текст в поле ввода можно выделить и поместить в Буфер Обмена.
Компонент Edit легко приспособить для ввода паролей. Для этого достаточно установить в свойстве PasswordChar вместо символа #0 какой-нибудь другой символ, обычно символ звездочки (*). Символ, заданный в свойстве PasswordChar отображается вместо реально вводимых символов, что мешает подсмотреть пароль посторонним.
Шаг 15. В форме SettingsForm компонент Edit применяется для ввода текстового сообщения будильника. Выберите его в окне свойств и в значении свойства Text впишите "Reminder !" (рисунок 9.33). Этот текст будет появляться в редакторе при появлении окна диалога.
Рисунок 9.33. Компонент Edit используется для ввода текстового сообщения будильника
Шаг 16. Изменение текста во время работы программы приводит к возникновению в компоненте Edit события OnChange. Обрабатывая это событие, можно, например, устроить работу окна диалога таким образом, что кнопка OK будет недоступна, если в редакторе нет текста. Чтобы реализовать такое поведение нашего диалога, определите для компонента MessageEdit следующий обработчик события OnChange:
procedure TAlarmDetailsForm.MessageEditChange(Sender: TObject); begin OkButton.Enabled := Length(MessageEdit.Text) <> 0; end; |
Выполните компиляцию программы и проверьте ее работу.
Прежде чем продолжить обсуждение примера Alarms, сделаем несколько замечаний по поводу редактирования многострочного текста. Редактор многострочного текста представлен компонентом Memo (рисунок 9.34).
Рисунок 9.34. Компонент Memo
Характерные свойства компонента Memo описаны в таблице 9.10.
Свойство | Описание |
---|---|
Align | Способ выравнивания компонента в пределах содержащего компонента. |
Alignment | Выравнивание текста: taLeftJustify – прижат к левой границе, taRightJustify – прижат к правой границе, taCenter – центрирован. |
HideSelection | Если равно значению True, то при потере редактором фокуса ввода выделение текста снимается. |
Lines | Текст в виде массива строк. |
MaxLength | Максимальное количество символов, которое пользователь может ввести. Если оно равно 0, то пользователь может ввести текст неограниченной длины. |
OEMConvert | Если равно значению True, то символы текста преобразуются в кодовую таблицу OEM. |
ReadOnly | Если равно значению True, то пользователь не сможет изменить текст в редакторе. |
ScrollBars | Управляет видимостью полос прокрутки: ssNone – полосы прокрутки скрыты, ssBoth – полосы прокрутки видны, ssHorizontal – видна лишь горизонтальная полоса прокрутки, ssVertical – видна лишь вертикальная полоса прокрутки. |
WantReturns | Если равно значению True, то клавиша Enter начинает в редакторе новую строку. Иначе нажатие клавиши Enter ассоциируется с нажатием стандартной кнопки окна диалога и для перевода строк применяется сочетание клавиш Ctrl+Enter. |
WantTabs | Если равно значению True, то клавиша Tab вставляет в текст символ табуляции, вместо того чтобы передать фокус ввода следующему компоненту. |
WordWrap | Если равно значению True, то работает перенос слов. |
OnChange | Происходит при изменении текста. |
Компонент Memo похож на Edit, но в отличие от него хранит не одну строку текста, а множество строк. Доступ к строкам обеспечивает свойство Lines, представляющее собой объект класса TStrings (см. главу 3). С помощью свойства Lines строки можно добавлять, вставлять, удалять и т.д. Свойство Lines доступно в окне свойств, поэтому на стадии проектирования вы можете заполнить компонент Memo некоторым исходным текстом (рисунок 9.35). Этот текст увидит пользователь при появлении формы на экране. Ввод исходного текста осуществляется в специальном редакторе текста (String list editor), которое вызывается нажатием кнопки с многоточием в поле значения свойства Lines.
Рисунок 9.35. Окно для ввода многострочного текста, отображаемого компонентом Memo
Компонент Memo часто имеет одну или две полосы прокрутки (вертикальную и горизонтальную). Их появление зависит от значения свойства ScrollBars.
В нашем приложении Alarms компонент Memo не нужен, но вам он безусловно пригодится в других программах.
Поскольку компонент Edit не проверяет, что вводит пользователь, он не удобен для ввода данных строго определенного формата, например телефонных номеров, времени и др. На этот случай разработчики среды Delphi предусмотрительно создали компонент MaskEdit. Он находится в палитре компонентов на вкладке Additional (рисунок 9.36).
Рисунок 9.36. Компонент MaskEdit
Компонент MaskEdit представляет собой поле ввода, которое вынуждает пользователя вводить данные в строго заданном формате. Во многом аналогичный компоненту Edit, он отличается от последнего тем, что имеет свойство EditMask и не имеет свойств HideSelection и OEMConvert.
Свойство EditMask задает шаблон (маску) для ввода символов текста. Шаблон имеет вид текстовой строки, его символы называются форматными и управляют тем, что вводит пользователь: буквы или цифры, в каком порядке, сколько и т.д. Мы не будем утомлять вас подробным описанием форматных символов, при необходимости обратитесь к справочному руководству. Нас интересует только шаблон для ввода времени с точностью до минуты.
Шаг 17. Выберите компонент TimeMaskEdit, затем в окне свойств перейдите к свойству EditMask и щелчком кнопки с многоточием откройте специальный редактор для этого свойства — Input Mask Editor (рисунок 9.37):
Рисунок 9.37. Выбор шаблона для компонента MaskEdit
В этом окне вы можете ввести шаблон и проверить его работу. Часто используемые шаблоны, например шаблоны телефонных номеров, даты, времени и некоторые другие, можно просто выбрать среди уже имеющихся образцов. Этой возможностью мы и воспользуемся. Выберите в списке Sample Masks пункт Short Time и щелкните на кнопке OK. Шаблон для ввода времени задан и имеет вид !90:00;1;_ . Обратите внимание, как при этом изменился компонент TimeMaskEdit (рисунок 9.38):
Рисунок 9.38. Компонент MaskEdit настроен для ввода времени
Шаг 18. Теперь давайте сделаем так, чтобы при появлении окна диалога поле ввода TimeMaskEdit не было пустым, а содержало текущее время. Для этого создайте у формы обработчик события OnCreate:
procedure TAlarmDetailsForm.FormCreate(Sender: TObject); begin TimeMaskEdit.Text := FormatDateTime('hh:mm', Time); end; |
Готово. Запустите программу и убедитесь, что она работает, как вы того ожидаете.
Раскрывающийся список (combo box) позволяет пользователю выбрать значение из большого множества альтернатив. Он представляет собой поле ввода, к которому прикреплен раскрывающийся список значений. Редактор служит для ввода нового значения, а список — для выбора существующего значения. Количество элементов в списке может быть произвольным, причем элементы можно динамически добавлять, удалять, заменять и т.д. Элементами списка обычно служат текстовые строки, но могут быть и графические рисунки (в последнем случае их редактирование невозможно).
Раскрывающийся список представлен компонентом ComboBox, который находится в палитре компонентов на вкладке Standard (рисунок 9.39):
Рисунок 9.39. Компонент ComboBox
Характерные свойства компонента ComboBox собраны в таблице 9.11.
Свойство | Описание |
---|---|
AutoCloseUp | Если равно True, то при вводе пользователем текста, который уже существует в списке Items, раскрытый список значений автоматически закрывается. |
AutoComplete | Если равно True, то компонент предугадывает вводимый пользователем текст на основе списка Items. |
AutoDropDown | Если равно True, то при вводе текста автоматически раскрывается список существующих значений. |
BevelEdges | Вложенные свойства beLeft, beTop, beRight и beBottom определяют видимость соответственно левой, верхней, правой и нижней сторон рельефной рамки. |
BevelInner | Внутренний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelKind | Вид рельефной рамки: bkNone — рамки нет, bkTile — рамка с четкими скосами, bkSoft — рамка со сглаженными скосами, bkFlat — плоская рамка (без скосов). |
BevelOuter | Внешний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
CharCase | Автоматическое преобразование регистра букв: ecLowerCase — к строчным буквам, ecUpperCase — к заглавным буквам, ecNormal — без преобразования. |
DropDownCount | Количество одновременно видимых элементов раскрывающегося списка. |
ItemHeight | Высота отдельного элемента списка, когда значение свойства Style равно csOwnerDrawFixed. |
ItemIndex | Порядковый номер выбранного в списке элемента, начиная с нуля. |
Items | Элементы раскрывающегося списка. |
MaxLength | Максимальное количество символов, которое пользователь может ввести в строке редактора. Если оно равно 0, то пользователь может ввести текст неограниченной длины. |
Sorted | Если равно True, то элементы списка сортируются в алфавитном порядке. |
Style | Стиль отображения выпадающего списка (см. табл. 7.12). |
Text | Текст в строке редактора. |
OnChange | Происходит при вводе текста или выборе значения из списка. Не происходит при программном изменении свойства Text. |
OnCloseUp | Происходит при закрытии списка значений. |
OnDrawItem | Происходит при рисовании элемента раскрывающего списка, но только в том случае, если свойство Style содержит значение csOwnerDrawFixed или csOwnerDrawVariable. |
OnDropDown | Происходит при раскрытии списка значений. |
OnMeasureItem | Происходит перед рисованием элемента раскрывающего списка для расчета его высоты. Требует, чтобы свойство Style содержало значение csOwnerDrawVariable. |
OnSelect | Происходит при выборе значения в раскрывающемся списке. |
Раскрывающийся список умеет отображать себя по-разному в зависимости от значения свойства Style (см. таблицу 9.12).
Значение | Описание |
---|---|
csSimple | Редактор и постоянно отображаемый список. |
csDropDown | Редактор и ассоциированный с ним раскрывающийся список. |
csDropDownList | Раскрывающийся список без редактора. Все элементы списка имеют одинаковую высоту, которая рассчитывается автоматически. |
csOwnerDrawFixed | Раскрывающийся список без редактора. Все элементы списка имеют одинаковую высоту, заданную в свойстве ItemHeight. |
csOwnerDrawVariable | Раскрывающийся список без редактора. Элементы списка имеют разную высоту. |
В двух последних случаях в компоненте ComboBox происходит событие OnDrawItem. Вы можете его перехватить и рисовать каждый элемент выпадающего списка как вам вздумается. Если элементы списка имеют разную высоту (стиль csOwnerDrawVariable), то компонент генерирует событие OnMeasureItem, чтобы узнать высоту каждого элемента. Стили csOwnerDrawFixed и csOwnerDrawVariable применяются в тех случаях, когда элементы списка должны быть рисунками.
Шаг 19. Раскрывающиеся списки пригодились нам в диалоге Alarm Details для выбора дня недели (компонент WeeklyComboBox). Поскольку все дни недели заранее известны, выберите в свойстве Style значение csDropDownList (рисунок 9.40).
Рисунок 9.40. Для выбора дня недели применяется компонент ComboBox в стиле csDropDownList
Шаг 20. Теперь компонент WeeklyComboBox нужно заполнить списком исходных значений. Выберите его в форме, затем в окне свойств перейдите к свойству Items и нажмите кнопку с многоточием в поле значения. В появившемся окне введите список строк, как показано на рисунке 9.41.
Рисунок 9.41. В этом окне вводятся элементы списка — дни недели
Щелкните кнопку OK — список значений компонента WeeklyComboBox задан.
Шаг 21. Выбранный элемент раскрывающегося списка определяется значением свойства ItemIndex. Начальное значение свойства равно -1, что означает — ни один элемент не выбран. Однако в компоненте WeeklyComboBox должно быть выбрано то значение, которое соответствуют текущей дате. С этой целью нужно доработать у формы обработчик события OnCreate. В итоге он будет иметь следующий вид:
procedure TAlarmDetailsForm.FormCreate(Sender: TObject); begin// Установка текущего времени TimeMaskEdit.Text := FormatDateTime('hh:mm', Time); // Установка текущего дня недели WeeklyComboBox.ItemIndex := DayOfWeek(DatePicker.Date) - 1; end; |
Реализация метода основана на том, что компонент DatePicker при создании формы сразу содержит текущую дату.
На этом с визуальной частью диалога Alarm Details покончено. Правда, мы ничего не сказали о компоненте DateTimePicker (рисунок 9.42).
Рисунок 9.42. Компонент DateTimePicker
Впрочем, он уже работает так, как требуется. В нем нет ничего сложного, и мы надеемся, что вы разберетесь с ним по таблице 9.13.
Свойство | Описание |
---|---|
BevelEdges | Вложенные свойства beLeft, beTop, beRight и beBottom определяют видимость соответственно левой, верхней, правой и нижней сторон рельефной рамки. |
BevelInner | Внутренний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelKind | Вид рельефной рамки: bkNone — рамки нет, bkTile — рамка с четкими скосами, bkSoft — рамка со сглаженными скосами, bkFlat — плоская рамка (без скосов). |
BevelOuter | Внешний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelWidth | Ширина скосов рельефной рамки. |
CalAlignment | Способ выравнивания раскрывающегося диалога: dtaLeft — по левому краю, dtaRight — по правому краю. |
CalColors | Цвета: BackColor — ни на что не влияет, существует для унификации настройки цвета с другими компонентами; MonthBackColor — цвет фона раскрывающего диалога; TextColor — цвет текста; TitleBackColor — цвет фона заголовка; TitleTextColor — цвет текста заголовка; TrailingTextColor — цвет текста дат, не принадлежащих текущему месяцу. |
Checked | Значение переключателя, который отображается, если свойство ShowCheckbox содержит значение True. |
Date | Выбранная дата. |
DateFormat | Формат даты: dfShort — короткий, dfLong — длинный. |
DateMode | Режим работы компонента: dmComboBox — по щелчку кнопки со стрелкой раскрывается окно с месячным календарем, dmUpDown — вместо кнопки со стрелкой показывается пара кнопок со стрелками вверх и вниз, щелчки на которых прокручивают день, месяц или год. |
Format | Формат даты и времени. |
Kind | Если равно значению dtkDate, то компонент предназначен для выбора даты, а если значению dtkTime, то для выбора времени. |
MaxDate | Максимальное значение даты, которое может выбрать пользователь. |
MinDate | Минимальное значение даты, которое может выбрать пользователь. |
ParseInput | Если равно значению True, то по мере ввода значения пользователем происходит событие OnUserInput. |
ShowCheckbox | Показывает переключатель (флажок). Значение переключателя определяется свойством Checked. |
Time | Выбранное время. |
OnChange | Происходит при изменении значения даты и времени. |
OnCloseUp | Происходит при сворачивании раскрывающегося диалога. |
OnUserInput | Происходит по мере ввода данных пользователем. |
В очередной раз выполните компиляцию программы и запустите ее. Откройте окно диалога Alarm Details и хорошенько его потестируйте (рисунок 9.43).
Рисунок 9.43. Тестирование окна Alarm Details
Все компоненты работают правильно. Можете поздравить себя с очередным достижением. Вы создали важную часть приложения Alarms — окно диалога, а заодно разобрались с множеством новых компонентов.
Окно диалога есть, но пользы от него пока нет. Все дело в том, что мы научились устанавливать параметры будильника, но не научились их принимать и хранить.
Шаг 22. Для хранения параметров будильника нам нужна новая структура данных, очевидно класс объектов. Немного поразмыслив, приходим к следующему описанию:
type TAlarm = classprivate Handled: Boolean; public MsgText: string; DateTime: TDateTime; PlaySound: Boolean; Recurring: Integer; function GetAlarmStr: string; procedure CheckTime; end; |
Поясним назначение полей и методов. Поле MsgText предназначено для хранения текстового сообщения. В поле DateTime записывается время и дата сигнала. Значение поля PlaySound показывает, требуется ли звуковое сопровождение сообщения. Поле Recurring определяет периодичность работы будильника и принимает следующие значения:
В первых двух случаях поле DateTime хранит только время, а в последнем еще и дату. Такое ухищрение позволяет организовать компактное хранение данных. Флаг Handled, объявленный в секции private, является служебным и позволит избежать повторных срабатываний, когда будильник уже прозвенел. Метод GetAlarmStr мы определили для удобства. Он будет формировать строку сообщения, содержащую время и текст напоминания. Метод CheckTime проверит, пора ли выдать сигнал и если да, то сделает это.
Шаг 23. Поместите описание класса TAlarm в раздел interface модуля AlarmDetails. Затем в разделе implementation наберите текст методов GetAlarmStr и CheckTime:
function TAlarm.GetAlarmStr: string; begin Result := FormatDateTime('hh:mm ', DateTime) + MsgText; end; procedure TAlarm.CheckTime; var Hour1, Min1, Sec1, MSec1: Word; Hour2, Min2, Sec2, MSec2: Word; Match: Boolean; begin// Декодировать текущее время DecodeTime(Time, Hour1, Min1, Sec1, MSec1); // Раскодировать текущее время будильника DecodeTime(DateTime, Hour2, Min2, Sec2, MSec2); // Проверить, что текущее время совпадает с временем будильникаcase Recurring of 0: // для ежедневной периодичности Match := (Hour1 = Hour2) and (Min1 = Min2); 1..7: // для еженедельной периодичности Match := (Hour1 = Hour2) and (Min1 = Min2) and (Recurring = DayOfWeek(Date)); 8: // для конкретной даты Match := (Hour1 = Hour2) and (Min1 = Min2) and (Int(DateTime) = Date); else Match := False; end; // Решить вопрос о выдаче сигнала будильникомif Match thenbeginifnot Handled then// сигнал!begin Handled := True; // предотвратить повторные срабатыванияif PlaySound then Beep; MessageDlg(GetAlarmStr, mtWarning, [mbOk], 0); end; endelse Handled := False; // обеспечить будущие срабатыванияend; |
Для правильной работы будильника метод CheckTime должен вызываться не реже одного раза в минуту. Чем чаще вызывается метод, тем меньше инерционность будильника, но тем больше пустых опросов, а значит выше загруженность операционной системы. Компромиссная частота — два раза в секунду. Так как в одну и ту же минуту метод CheckTime будет вызван несколько раз, то для избежания повторных срабатываний используется флаг Handled. Будильник выдает сообщение только в том случае, если текущее время совпадает с временем, на которое будильник установлен и при условии, что в данную минуту он еще не звенел.
Шаг 24. Давайте теперь позаботимся о передаче данных в окно диалога перед его запуском и о приеме данных после завершения. Удобнее всего, чтобы за это отвечало само окно диалога, т.е. форма AlarmDetailsForm. С этой целью определите в классе TAlarmDetilasForm два метода — GetData и SetData. Методы следует поместить в секцию public:
type TAlarmDetailsForm = class(TForm) ... publicprocedure GetData(Alarm: TAlarm); procedure SetData(Alarm: TAlarm); end; |
В разделе implementation наберите программный текст методов:
procedure TAlarmDetailsForm.GetData(Alarm: TAlarm); beginwith Alarm dobegin// Получить из диалога текст сообщения будильника MsgText := MessageEdit.Text; // Получить из диалога время срабатывания будильника DateTime := StrToTime(TimeMaskEdit.Text); // Получить из диалога состояние переключателя звука PlaySound := SoundCheckBox.Checked; // Получить из диалога периодичность срабатывания будильника if EverydayRadioButton.Checked then Recurring := 0 elseif WeeklyRadioButton.Checked then Recurring := WeeklyComboBox.ItemIndex + 1 else{ DateRadioButton.Checked }begin Recurring := 8; DateTime := DatePicker.Date + DateTime; end; end; end; procedure TAlarmDetailsForm.SetData(Alarm: TAlarm); beginwith Alarm dobegin// Установить в окне диалога текст сообщения будильника MessageEdit.Text := MsgText; // Установить в окне диалога время будильника TimeMaskEdit.Text := FormatDateTime('hh:mm', DateTime); // Установить в окне диалога состояние переключателя звука SoundCheckBox.Checked := PlaySound; // Установить в окне диалога периодичность будильникаcase Recurring of 0: // ежедневно EverydayRadioButton.Checked := True; 1..7: // еженедельноbegin WeeklyRadioButton.Checked := True; WeeklyComboBox.ItemIndex := Recurring - 1; end; 8: // в конкретный деньbegin DateRadioButton.Checked := True; DatePicker.Date := Int(DateTime); end; end; end; end; |
Метод GetData просто заполняет поля переданного в параметре объекта Alarm значениями, которые установлены в компонентах окна диалога. Метод SetData выполняет обратные действия, заполняя компоненты окна диалога значениями, которые содержатся в полях объекта Alarm.
На этом с разработкой модуля AlarmDetails покончено и окно диалога Alarm Details полностью готово к использованию. Дальше необходимо обеспечить формирование, редактирование и визуализацию списка будильников. Эта задача решается с помощью компонента ListBox.
Компонент ListBox отображает список элементов, которые пользователь может просматривать и выбирать, но не может непосредственно модифицировать. По умолчанию элементами списка являются строки, но могут быть и графические объекты. Элементы могут располагаться в одну или несколько колонок и автоматически сортироваться. При необходимости обеспечивается возможность прокрутки списка. Компонент ListBox находится в палитре компонентов на вкладке Standard (рисунок 9.44).
Рисунок 9.44. Компонент ListBox
Его характерные свойства собраны в таблице 9.14.
Свойство | Описание |
---|---|
Align | Способ выравнивания компонента в пределах содержащего компонента. |
AutoComplete | Если равно True, то можно быстро выбрать элемент, если начать набирать его текст на клавиатуре. |
BevelEdges | Вложенные свойства beLeft, beTop, beRight и beBottom определяют видимость соответственно левой, верхней, правой и нижней сторон рельефной рамки. |
BevelInner | Внутренний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BevelKind | Вид рельефной рамки: bkNone — рамки нет, bkTile — рамка с четкими скосами, bkSoft — рамка со сглаженными скосами, bkFlat — плоская рамка (без скосов). |
BevelOuter | Внешний скос рельефной рамки: bvNone — скос отсутствует, bvLowered — скос внутрь, bvRaised — скос наружу; bvSpace — скос заменяется отступом. |
BorderStyle | Определяет, имеет ли список рамку. |
Columns | Количество колонок в списке. |
ExtendedSelect | Если равно значению True, то пользователь может выбрать в списке диапазон элементов (однако лишь в том случае, если MultiSelect тоже равно значению True). |
IntegralHeight | Если равно значению True, то высота списка автоматически уменьшается, чтобы быть кратной высоте элемента. |
ItemHeight | Высота элемента списка, когда значение свойства Style равно lbOwnerDrawFixed. |
Items | Элементы списка. |
MultiSelect | Если равно значению True, то пользователь может выбрать в списке несколько элементов. |
ScrollWidth | Логическая ширина списка в пикселях. Если значение свойства ScrollWidth больше значения свойства Width, то появляется горизонтальная полоса прокрутки. В противном случае полоса прокрутки не показывается. |
Sorted | Если равно значению True, то элементы списка сортируются в алфавитном порядке. |
Style | Стиль отображения списка (см. табл. 7.14). |
OnData | Предназначено для формирования списка элементов перед рисованием. Происходит только в том случае, если свойство Style содержит значение lbVirtual или lbVirtualOwnerDraw. |
OnDataFind | Происходит, когда пользователь пытается быстро перейти к элементу, набирая текст элемента на клавиатуре. Обработчик этого события должен на основании введенной пользователем строки вернуть номер соответствующего элемента. Возникает только в том случае, если свойство Style содержит значение lbVirtual или lbVirtualOwnerDraw. |
OnDataObject | Происходит при обращении к массиву Objects в списке Items, но только в том случае, если свойство Style содержит значение lbVirtual или lbVirtualOwnerDraw. Обработчик события должен вернуть соответствующий элементу объект. |
OnDrawItem | Происходит при рисовании отдельно взятого элемента списка, но только в том случае, если свойство Style содержит одно из следующих значений: lbOwnerDrawFixed, lbOwnerDrawVariable, lbVirtualOwnerDraw. |
OnMeasureItem | По замыслу разработчиков событие происходит при расчете высоты отдельно взятого элемента списка перед его рисованием на экране и лишь в том случае, если свойство Style содержит значение lbOwnerDrawVariable. Однако из-за дефекта в модуле StdCtrls событие OnMeasureItem не срабатывает. |
Особенности хранения и отображения элементов списка определяются свойством Style, возможные значения которого описаны в таблице 9.15.
Значение | Описание |
---|---|
LbStandard | Все элементы списка имеют одинаковую высоту, которая рассчитывается исходя из размера шрифта. |
LbOwnerDrawFixed | Все элементы списка имеют одинаковую высоту, заданную в свойстве ItemHeight. За рисование элементов отвечает программист, который должен создать обработчик события OnDrawItem. |
lbOwnerDrawVariable | По замыслу разработчиков элементы списка имеют разную высоту, определяемую в обработчике события OnMeasureItem (из-за дефекта в модуле StdCtrls событие не срабатывает). За рисование элементов отвечает программист, который должен создать обработчик события OnDrawItem. |
LbVirtual | Элементы списка хранятся отдельно от компонента и запрашиваются с помощью события OnData. За рисование элементов отвечает компонент. |
LbVirtualOwnerDraw | Элементы списка хранятся отдельно от компонента и запрашиваются с помощью события OnData. За рисование элементов отвечает программист, который должен создать обработчик события OnDrawItem. |
Шаг 25. Давайте воспользуемся компонентом ListBox для организации списка будильников. Активизируйте форму MainForm, а затем опустите на нее компонент ListBox. Переименуйте компонент в AlarmListBox и скорректируйте его местоположение и размеры. Затем установите свойство TabOrder в значение 0, чтобы при отображении формы список первым получил фокус ввода (рисунок 9.45).
Рисунок 9.45. Компонент ListBox применяется для организации списка будильников
Решим теперь вопрос хранения будильников в компоненте AlarmListBox. Для хранения элементов служит свойство Items. Свойство Items — это объект класса TStrings, в нем свойство-массив Strings хранит отображаемые строки, а свойство-массив Objects — ассоциированные со строками объекты. В нашем примере массив Strings будет хранить выдаваемые по сигналу сообщения, а массив Objects — соответствующие им объекты класса TAlarm.
Теоретически все понятно, осталось реализовать все это практически. Создание, редактирование и удаление будильника осуществляется по щелчкам на кнопках NewButton, EditButton и DeleteButton соответственно. Поэтому в них требуется создать обработчики события OnClick.
Шаг 26. В кнопке New... обработчик события OnClick уже существует, но его необходимо доработать:
procedure TMainForm.NewButtonClick(Sender: TObject); var Alarm: TAlarm; begin AlarmDetailsForm := TAlarmDetailsForm.Create(Self); try// Выполнить диалогif AlarmDetailsForm.ShowModal = mrOK thenbegin// Создать новый объект будильника Alarm := TAlarm.Create; // Получить параметры будильника из диалога AlarmDetailsForm.GetData(Alarm); // Добавить будильник в список и выбрать его AlarmListBox.ItemIndex := AlarmListBox.Items.AddObject( Alarm.GetAlarmStr, Alarm); end; finally AlarmDetailsForm.Free; end; end; |
Метод NewButtonClick создает окно диалога Alarm Details и выполняет его в монопольном режиме. Если диалог завершается щелчком кнопки OK, создается новый объект будильника и в него переносятся данные из окна диалога. Затем этот объект добавляется в список AlarmList и его номер присваивается свойству списка ItemIndex. В результате новый элемент становится выделенным.
Вы, разумеется, хотите проверить работу новоиспеченного метода. Сейчас мы так и сделаем, но прежде нужно решить небольшой вопрос. Дело в том, что при уничтожении блока списка освобождаются только строки, но не освобождаются ассоциированные с ними объекты. Хотя память объектов так или иначе освобождается при завершении приложения, мы рекомендуем всегда освобождать память явно. Это считается "хорошим тоном" программирования и иногда позволяет выявить скрытые ошибки. Освобождение использованных в форме динамических данных осуществляется в обработчике события OnDestroy. Для формы MainForm он должен быть таким:
procedure TMainForm.FormDestroy(Sender: TObject); var I: Integer; beginfor I := 0 to AlarmListBox.Items.Count - 1 do AlarmListBox.Items.Objects[I].Free; end; |
После того как вы написали этот обработчик, выполните компиляцию программы и запустите ее. Попытайтесь добавить в список несколько будильников. Если это у вас получилось, перейдем к следующему шагу — программированию реакции на нажатия кнопок Edit... и Delete.
Шаг 27. Создайте в компоненте EditButton обработчик события OnClick:
procedure TMainForm.EditButtonClick(Sender: TObject); var Alarm: TAlarm; SavedIndex: Integer; begin AlarmDetailsForm := TAlarmDetailsForm.Create(Self); try// Получить выбранный будильникwith AlarmListBox do Alarm := TAlarm(Items.Objects[ItemIndex]); // Установить управляющие элементы диалога в соответствии с// параметрами будильника AlarmDetailsForm.SetData(Alarm); // Выполнить диалогif AlarmDetailsForm.ShowModal = mrOK thenbegin// Получить из диалога новые параметры будильника AlarmDetailsForm.GetData(Alarm); with AlarmListBox dobegin// Запомнить номер выбранного в списке элемента SavedIndex := ItemIndex; // Изменить текст элемента// При этом элемент перестает быть выбранным Items.Strings[ItemIndex] := Alarm.GetAlarmStr; // Восстановить номер выбранного в списке элемента ItemIndex := SavedIndex; end; end; finally AlarmDetailsForm.Free; end; end; |
Этот метод создает окно диалога Alarm Details, инициализирует его компоненты данными из выбранного в списке объекта будильника, а затем выполняет диалог в монопольном режиме. Если диалог завершился щелчком на кнопке OK, то данные из окна диалога переносятся обратно в объект будильника и соответственно изменяется отображаемая в блоке списка строка. Так как в результате последнего действия в списке пропадает полоса выбора (свойство ItemIndex получает значение -1), номер выделенного элемента предварительно сохраняется в локальной переменной SavedIndex, а затем восстанавливается.
Шаг 28. Осталось создать обработчик события OnClick в компоненте DeleteButton:
procedure TMainForm.DeleteButtonClick(Sender: TObject); beginwith AlarmListBox dobegin// Разрушить объект будильника Items.Objects[ItemIndex].Free; // Удалить из списка соответствующую объекту строку Items.Delete(ItemIndex); end; end; |
Метод DeleteButtonClick удаляет объект будильника и соответствующую ему строку в списке.
Обработчики событий для всех кнопок заданы, однако не спешите запускать приложение. Необходимо позаботиться о том, чтобы кнопки Edit... и Delete были доступны или недоступны в зависимости от того, выделен в списке элемент или нет. Как бы это сделать попроще? Первое решение, которое напрашивается — это вставить необходимые проверки в обработчики событий кнопок. Это неплохое решение, но оно больше подходит тем, кто привык решать задачу в лоб. Мы пойдем другим путем, воспользовавшись событием OnIdle объекта Application.
В объекте Application происходит событие OnIdle в период простоя программы, например во время ожидания пользовательского ввода. Благодаря этому событию программа может выполнять некоторую фоновую работу, которая в нашем случае заключается в управлении состоянием кнопок.
Для создания обработчика события OnIdle объекта Application воспользуемся уже знакомым вам компонентом ApplicationEvents (см. главу 8).
Шаг 29. Поместите в форму компонент ApplicationEvents, дайте ему одноименный идентификатор и создайте обработчик события OnIdle:
procedure TMainForm.ApplicationEventsIdle(Sender: TObject; var Done: Boolean); begin EditButton.Enabled := AlarmListBox.ItemIndex <> -1; DeleteButton.Enabled := AlarmListBox.ItemIndex <> -1; Done := True; // предотвращает непрерывную генерацию события OnIdleend; |
В передаваемом по ссылке параметре Done метод возвращает результат своей работы. Значение True показывает, что метод нужно вызывать не постоянно в течении простоя приложения, а только по одному разу в начале каждого периода простоя.
А сейчас выполните компиляцию, запустите программу и тщательно протестируйте работу главной формы (рисунок 9.46).
Рисунок 9.46. В этом окне создается список будильников
Будильники можно создавать, добавлять, удалять. Нам осталось сделать последний шаг — заставить будильники "звонить". Для этого нужно периодически вызывать метод CheckTime у каждого помещенного в список объекта TAlarm. Периодические по времени действия выполняются с помощью таймера, о котором мы дальше и поговорим.
Таймер — это системный генератор событий, который периодически сообщает программе о завершении заданного промежутка времени. Интервал времени между событиями таймера может устанавливаться в диапазоне от 1 до 65535 миллисекунд. Используя таймер, учитывайте, что интервалы между этими событиями оказываются неточными из-за накладных расходов механизма обработки событий Windows.
В библиотеке VCL прием событий таймера обеспечивает компонент Timer. Он расположен в палитре компонентов на вкладке System (рисунок 9.47). Им мы и воспользуемся для "оживления" будильников в приложении Alarms.
Рисунок 9.47. Компонент Timer
Шаг 30. Активизируйте форму MainForm. Затем поместите в нее компонент Timer (рисунок 9.48). Если хотите, дайте ему любое имя.
Рисунок 9.48. Компонент Timer — на форме
Шаг 31. Интервал времени между событиями таймера задается в миллисекундах как значение свойства Interval. Изначально интервал равен 1000 миллисекунд (1 секунда). Частота контроля будильников должна быть два раза в секунду, поэтому установите свойство Interval в значение 500.
Шаг 32. Через заданные в свойстве Interval промежутки времени в компоненте Timer происходит событие OnTimer (единственное событие этого компонента). Для контроля за будильниками нам нужно создать обработчик этого события:
procedure TMainForm.TimerTimer(Sender: TObject); var I: Integer; beginfor I := 0 to AlarmListBox.Items.Count - 1 dowith AlarmListBox.Items.Objects[I] as TAlarm do CheckTime; end; |
Смысл выполняемых действий очевиден: у каждого объекта в списке будильников вызывается метод CheckTime. Таким образом, каждый будильник периодически проверяет свое время и, если нужно, выдает предупреждение (рисунок 9.49).
Рисунок 9.49. Когда будильник срабатывает, звучит сигнал и на экран выдается сообщение
Выполните компиляцию и запустите программу, чтобы проверить работу всех элементов программы. Ура! Все работает, терпение и упорство вознаграждены.
Программа Alarms имеет один существенный недостаток: после его завершения все установленные будильники теряются, так что при следующем запуске программы их приходится создавать снова. Для решения этой проблемы необходимо, чтобы между сеансами работы программы будильники хранились в конфигурационном файле на диске.
Сохранение и восстановление конфигурации осуществляется в Windows с помощью так называемых файлов настроек. Файл настроек (initialization file) — это текстовый файл, состоящий из секций. Секция начинается с имени, заключенного в квадратные скобки. В каждой секции содержатся определения некоторых связанных по смыслу параметров, представленные в виде пар Имя=Значение. Примером файла настроек может служить файл настроек проекта в системе Delphi. В нашем проекте это файл Alarms.dof.
Структуру файла настроек для программы Alarms выберем так, чтобы каждому будильнику соответствовала отдельная секция. Число секций, т.е. будильников, будем хранить в параметре AlarmCount секции Global Options. Вот как могло бы выглядеть содержимое файла:
[Global Options] AlarmCount=3 [Alarm1] Message=Dinner ! Time=13:00 PlaySound=1 Recurring=0 [Alarm2] Message=Tennis training Time=16:00 PlaySound=1 Recurring=1 [Alarm3] Message=My favourite TV-show... Time=22:30 PlaySound=0 Recurring=8 Date=02/25/96 |
Чтение и запись файла настроек осуществляется с помощью объектов TIniFile (заметьте, они не являются компонентами). Класс TIniFile описан в модуле IniFiles. Этот модуль необходимо самостоятельно добавить в вызывающий модуль с помощью оператора uses. При создании объекта TIniFile ему в конструктор передается имя INI-файла. Позже это имя можно узнать, обратившись к свойству FileName. Если в имени файла маршрут не был указан, считается что INI-файл находится в каталоге системы Windows.
Чтение переменных из INI-файла выполняется с помощью описанных ниже методов. В этих методах название секции передается в параметре Section, имя переменной – в параметре Ident, а значение по умолчанию – в параметре Default.
При чтении значений из INI-файла может оказаться, что заданный идентификатор или секция отсутствует. В этом случае ошибки не происходит, а функции ReadBool, ReadInteger и ReadString возвращают значение, переданное в параметре Default.
Кроме методов чтения существуют также методы записи переменных INI-файла, которые описаны ниже. В этих методах название секции передается в параметре Section, имя переменной – в параметре Ident, а значение переменной — в параметре Value.
Если в момент записи значения оказывается, что заданные секция и (или) идентификатор отсутствуют, они создаются.
Удаление секций INI-файла осуществляется с помощью метода EraseSection, в который передается единственный параметр — название секции.
Шаг 33. Давайте воспользуемся описанными методами для сохранения и восстановления будильников в программе ALARMS. Работу по сохранению и восстановлению параметров одного будильника лучше всего поручить классу TAlarm. Для этого добавьте в его описание два новых метода — LoadFromIniFile и SaveToIniFile.
type TAlarm = class ... procedure LoadFromIniFile(IniFile: TIniFile; const Section: string); procedure SaveToIniFile(IniFile: TIniFile; const Section: string); end; |
Метод LoadFromIniFile предназначен для чтения из INI-файла полей объекта, а метод SaveToIniFile — для записи в INI-файл полей объекта. Секция INI-файла, с которой работают эти методы, передается в параметре Section.
Шаг 34. Наберите программный код методов в разделе implementation:
procedure TAlarm.LoadFromIniFile(IniFile: TIniFile; const Section: string); beginwith IniFile dobegin// Прочитать текст сообщения MsgText := ReadString(Section, 'Message', 'Reminder !'); // Прочитать строковое значение времени и// преобразовать его в формат TdateTime DateTime := StrToTime(ReadString(Section, 'Time', TimeToStr(Time))); // Прочитать состояние переключателя звука PlaySound := ReadBool(Section, 'PlaySound', True); // Прочитать значение периодичности Recurring := ReadInteger(Section, 'Recurring', 0); if Recurring = 8 then// Прочитать строковое значение даты и// преобразовать его в формат TDateTime DateTime := StrToDate( ReadString(Section, 'Date', DateToStr(Date))) + DateTime; end; end; procedure TAlarm.SaveToIniFile(IniFile: TIniFile; const Section: string); beginwith IniFile dobegin// Записать текст сообщения WriteString(Section, 'Message', MsgText); // Преобразовать время в строку и записать строку в INI-файл WriteString(Section, 'Time', FormatDateTime('hh:mm', DateTime)); // Записать значение переключателя звука WriteBool(Section, 'PlaySound', PlaySound); // Записать значение периодичности WriteInteger(Section, 'Recurring', Recurring); if Recurring = 8 then// Преобразовать дату в строку и записать строку в INI-файл WriteString(Section, 'Date', DateToStr(DateTime)); end; end; |
Шаг 35. Перейдем теперь от сохранения и восстановления одного будильника к загрузке и восстановлению всего списка. Эти действия следует выполнять соответственно при создании и уничтожении главной формы программы, т.е. в событиях OnCreate и OnDestroy. Создайте форме MainForm обработчик события OnCreate и доработайте обработчик события OnDestroy (не забудьте подключить модуль IniFiles):
procedure TMainForm.FormCreate(Sender: TObject); var IniFile: TIniFile; Alarm: TAlarm; AlarmCount, I: Integer; begin IniFile := TIniFile.Create('Alarms.ini'); try// Прочитать число будильников AlarmCount := IniFile.ReadInteger('Global Options', 'AlarmCount', 0); // Прочитать список будильниковfor I := 1 to AlarmCount dobegin// Создать будильник Alarm := TAlarm.Create; // Прочитать параметры будильника из соответствующей секции Alarm.LoadFromIniFile(IniFile, 'Alarm' + IntToStr(I)); // Добавить будильник в список AlarmListBox.Items.AddObject(Alarm.GetAlarmStr, Alarm); end; finally IniFile.Free; end; end; procedure TMainForm.FormDestroy(Sender: TObject); var IniFile: TIniFile; I: Integer; begin IniFile := TIniFile.Create('Alarms.ini'); try// Записать число будильников IniFile.WriteInteger('Global Options', 'AlarmCount', AlarmListBox.Items.Count); // Записать список будильниковfor I := 0 to AlarmListBox.Items.Count - 1 dowith AlarmListBox.Items.Objects[I] as TAlarm do// Записать параметры будильника в соответствующую секцию SaveToIniFile(IniFile, 'Alarm' + IntToStr(I + 1)); finally IniFile.Free; for I := 0 to AlarmListBox.Items.Count - 1 do AlarmListBox.Items.Objects[I].Free; end; end; |
Вот пожалуй и все. Сохраните проект, выполните его компиляцию и запустите программу. Создайте несколько будильников, закройте программу, а затем запустите ее снова... Будильники на месте. Кстати, утилиту Alarms можно поместить в папку StartUp, тогда она всегда будет у вас под рукой.
В сложных компьютерных системах предусмотрена настройка десятков различных параметров, которые пользователь просто обязан установить. Это означает либо перегруженность диалоговых окон, либо слишком большое их количество. Решение этой проблемы заключается в организации многостраничных окон диалога. Они создаются с помощью компонента, который изображается в виде множества снабженных закладками страниц, называемых вкладками (рисунок 9.50):
Рисунок 9.50. Пример вкладок в окне диалога
Вкладки относятся к разряду сложных компонентов, но в целом они упрощают пользовательский интерфейс, заменяя несколько связанных по смыслу форм. А чем меньше форм, тем легче пользователю ориентироваться в программе.
Создание вкладок рассмотрим на следующем примере. Представьте, что вкладками являются экзаменационные билеты по трем научным дисциплинам: математике, физике, химии. Каждая вкладка содержит один вопрос с возможными вариантами ответа. Пользователь должен на каждой вкладке выбрать правильный вариант ответа и завершить ввод щелчком на кнопке Result. При всех правильных ответах он получит оценку "отлично", при двух ответах — "хорошо", при ответе лишь на один вопрос — "удовлетворительно", а при всех неправильных ответах — "плохо".
Шаг 1. Приступим к реализации примера. Начните новый проект и установите для главной формы следующие свойства:
Размеры формы подберите по своему усмотрению.
Шаг 2. Теперь поместите в форму компонент PageControl. Вы найдете его в палитре компонентов на вкладке Win32 (рисунок 9.51).
Рисунок 9.51. Компонент PageControl
Характерные свойства компонента PageControl кратко описаны в таблице 9.16.
Свойство | Описание |
---|---|
ActivePage | Активная вкладка (страница). |
Align | Способ выравнивания компонента в пределах содержащего компонента. |
DockSite | Определяет, используется ли компонент PageControl для стыковки других компонентов. |
HotTrack | Подсвечивает закладку при наведении на нее указателя мыши. |
Images | Список значков, отображаемых на закладках. Свойство Images используется совместно со свойством ImageIndex компонентов TabSheet. Компонент PageControl автоматически назначает каждой закладке номер значка в соответствии с очередностью добавления вкладок, однако программист может вручную указать номер значка. |
MultiLine | Располагает закладки в несколько рядов. |
OwnerDraw | Позволяет программно рисовать закладки в обработчике события OnDrawTab. Если свойство OwnerDraw равно значению False, то закладки имеют стандартный вид и событие OnDrawTab не происходит. |
Pages | Массив вкладок (страниц). Каждая вкладка является объектом класса TTabSheet. Свойство Pages доступно только из программы. |
PageCount | Общее количество вкладок. Доступно только из программы. |
RaggedRight | Если равно значению True, то при включенном режиме MultiLine закладки не выравниваются на ширину компонента. |
ScrollOpposite | Способ организации рядов закладок. Если равно значению False, то все ряды расположены вместе, например вверху. Если равно значению True, неактивные ряды переносятся на другую сторону компонента, например вниз. |
Style | Стиль закладок: tsTabs — обычные трехмерные закладки, tsFlatButtons — плоские закладки, tsButtons — закладки в виде кнопок. |
TabIndex | Номер выбранной закладки (первая закладка имеет номер 0). |
TabPosition | Местоположение закладок: tpTop — сверху, tpRight — справа, tpLeft — слева, tpBottom — снизу. |
TabWidth, TabHeight | Ширина и высота закладок. Если эти свойства равны нулю, то размеры закладок подбираются автоматически, исходя из размеров надписей. |
OnChange | Происходит после смены закладки. |
OnChanging | Происходит перед сменой закладки. |
OnDrawTab | Происходит при рисовании закладки на экране. Требует, чтобы свойство OwnerDraw содержало значение True. |
OnGetImageIndex | Обработчик этого события должен вернуть номер значка для отображаемой закладки. |
OnGetSiteInfo | Происходит, когда у компонента запрашивается место для стыковки. |
Шаг 3. Первоначально компонент PageControl не содержит ни единой вкладки. Для создания вкладки щелкните правой кнопкой мыши на компоненте и выберите в контекстном меню команду New Page (рисунок 9.52).
Рисунок 9.52. Создание новой вкладки в компоненте PageControl
Будет создана первая вкладка с заголовком TabSheet1 (рисунок 9.53).
Рисунок 9.53. В компоненте PageControl создана первая вкладка
Каждая вкладка в компоненте PageControl представлена объектом класса TTabSheet. Свойства отдельной вкладки устанавливаются в окне свойств. Характерные свойства кратко описаны в таблице 9.17.
Свойство | Описание |
---|---|
BorderWidth | Ширина внутреннего отступа. |
Caption | Надпись на закладке. |
Highlighted | Подсветка закладки цветом. |
ImageIndex | Номер значка в списке Images компонента PageControl. Значок отображается рядом с названием закладки. Отрицательное значение свойства ImageIndex говорит о том, что для закладки значок не задан. |
PageControl | Ссылка на компонент PageControl, которому принадлежит вкладка. Доступно только из программы. |
PageIndex | Номер вкладки в массиве Pages компонента PageControl. |
TabIndex | Номер вкладки среди видимых вкладок. Если вкладка не видна, то свойство TabIndex равно -1. Свойство доступно только программно и только для чтения. |
TabVisible | Определяет, видна ли закладка. |
OnHide | Происходит при переключении на другую вкладку. |
OnShow | Происходит при активизации вкладки. |
Шаг 4. Перейдите к окну свойств и замените текст закладки, вписав в свойстве Caption значение Mathematics. Действуя аналогично, добавьте вкладки Physics и Chemistry (рисунок 9.54).
Рисунок 9.54. В компоненте PageControl созданы три вкладки
Шаг 5. Ну вот, у нас уже есть три пустых вкладки и можно приступать к наполнению их содержанием. Сначала щелчком мыши активизируйте вкладку Mathematics. Затем поместите на нее группу взаимоисключающих переключателей — компонент RadioGroup. Заголовок группы будет содержать условие вопроса, а текст переключателей — возможные варианты ответа. Вопрос по математике будет из области тригонометрии (рисунок 9.55).
Рисунок 9.55. Содержимое первой вкладки
Шаг 6. Чтобы группа переключателей выглядела так, как на рисунке, подберите ей соответствующие размеры и установите значения следующих свойств:
Caption = The right expression is (правильным является выражение)
Items = sin 50( < cos 50(
sin 50( > cos 50(
sin 50( = cos 50(
ItemIndex = 0 (номер варианта принимаемый по умолчанию)
Tag = 1 (номер правильного варианта, считая от нуля)
Свойство Tag не несет смысловой нагрузки в компонентах среды Delphi. Поэтому его можно использовать по своему усмотрению. Помещая в него номер правильного ответа, мы заботимся об упрощении подсчета оценки. Когда пользователь укажет ответ, мы сравним значения свойств ItemIndex и Tag. Ответ будет считаться правильным лишь в том случае, если номера совпадут.
Шаг 7. Билет по математике готов, приступим к подготовке билета по физике. Активизируйте вкладку Physics и поместите на нее компонент RadioGroup. Подберите для него подходящие размеры и установите следующие свойства:
Caption = When the ice in water dissolves then (когда лед в воде тает)
Items = the level of water becomes higher (уровень воды поднимается)
the level of water becomes lower (уровень воды понижается)
the level of water remains unchanged (уровень воды остается неизменным)
ItemIndex = 0 (номер варианта принимаемый по умолчанию)
Tag = 2 (номер правильного варианта, считая от нуля)
Результат должен быть таким, как на рисунке 9.56:
Рисунок 9.56. Содержимое второй вкладки
Шаг 8. Осталось создать билет по химии. Мы надеемся, что после всех предыдущих испытаний это не составит для вас труда. Кратко поясним, что нужно сделать. Активизируйте вкладку Chemistry и поместите на нее компонент RadioGroup. Подберите для него подходящие размеры и установите следующие свойства:
Caption = The right way of mixing acid and water is (чтобы разбавить кислоту, нужно)
Items = to add acid to water (добавить кислоту в воду)
to add water to acid (добавить воду в кислоту)
ItemIndex = 0 (номер варианта принимаемый по умолчанию)
Tag = 0 (номер правильного варианта, считая от нуля)
После всех ваших действий вкладка Chemistry будет выглядеть так, как на рисунке 9.57.
Рисунок 9.57. Содержимое третьей вкладки
Шаг 9. Все вкладки с экзаменационными билетами вроде бы готовы, но как вы считаете, какая из них будет активной при запуске программы? Конечно та, которая осталась активной при проектировании, т.е. вкладка Chemistry. А надо, чтобы первой оказалась вкладка Mathematics. Поэтому активизируйте ее щелчком мыши. Кроме того, установите у формы свойство ActiveControl в значение RadioGroup1. Компонент, указанный в свойстве ActiveControl первым получает фокус ввода при появлении формы на экране.
Шаг 10. Теперь вас ждет самая ответственная работа — выставление пользователю оценки. Для этого поместите в форму две кнопки (компонент Button). Первая кнопка предназначена для выдачи результата экзамена, сделайте ее свойства такими:
Вторая кнопка служит для закрытия окна, ее сделайте такой:
Рисунок 9.58. Кнопка Result выдает оценку, кнопка Close завершает диалог
Шаг 11. Создайте для кнопок следующие обработчики события OnClick:
procedure TExamForm.ResultButtonClick(Sender: TObject); const MarkText: array[0..3] ofstring = ('Неудовлетворительно', 'Удовлетворительно', 'Хорошо', 'Отлично'); var Mark: Integer; begin Mark := 0; if RadioGroup1.ItemIndex = RadioGroup1.Tag then Mark := Mark + 1; if RadioGroup2.ItemIndex = RadioGroup2.Tag then Mark := Mark + 1; if RadioGroup3.ItemIndex = RadioGroup3.Tag then Mark := Mark + 1; ShowMessage(MarkText[Mark]); end; procedure TExamForm.CloseButtonClick(Sender: TObject); begin Close; end; |
Оценка вычисляется элементарно. Сначала предполагается, что она равна нулю (ни одного правильного ответа), а затем она уточняется в соответствии с тем, дал ли пользователь правильный ответ по математике, физике и химии. Под конец вызывается процедура ShowMessage, которая выдает в маленьком окне диалога заключение экзаменатора (рисунок 9.59).
Рисунок 9.59. По щелчку на кнопке Result выставляется оценка
После компиляции и запуска программы предложите своим родственникам или друзьям пройти экзамен. Мы надеемся, что все они получат оценку "отлично".
Поупражняйтесь с компонентом PageControl, например, измените размеры закладок (свойства TabHeight и TabWidth), добавьте больше страниц. Когда закладки перестанут умещаться в одной строке, обнаружится, что их можно прокручивать (рисунок 9.60). Кнопки прокрутки появляются автоматически.
Рисунок 9.60. Вкладки могут прокручиваться с помощью кнопок со стрелками
Если это вам не нравится, закладки можно расположить в несколько рядов, установив свойство MultiLine в значение True (рисунок 9.61):
Рисунок 9.61. Вкладки размещены в несколько рядов
А можно ли получить страницы без закладок? Да, для этого в компонентах TTabSheet нужно установить свойство TabVisible в значение False. Заметьте, это свойство не управляет видимостью вкладки, а влияет лишь на ее заголовок — закладку. Переключение между такими страницами становится вашей заботой и осуществляться программно.
В реальной задаче может потребоваться отследить переключения между страницами. Для этого в компоненте PageControl предусмотрены события OnChanging и OnChange. Первое событие — это запрос на переключение страницы, а второе — уведомление о том, что страница переключилась.
Для создания многостраничных окон диалога иногда используется еще один компонент — TabControl, который расположен в палитре компонентов по соседству с компонентом PageControl (рисунок 9.62).
Рисунок 9.62. Компонент TabControl
Характерные свойства компонента TabControl описаны в таблице 9.18.
Свойство | Описание |
---|---|
Align | Способ выравнивания компонента в пределах содержащего компонента. |
DockSite | Определяет, используется ли компонент TabControl для стыковки других компонентов. |
HotTrack | Подсвечивает закладку при наведении на нее указателя мыши. |
Images | Список значков, отображаемых на закладках. Каждая закладка получает значок в соответствии со своим порядковым номером. |
MultiLine | Располагает закладки в несколько рядов. |
MultiSelect | Если равно значению True, то пользователь может выбрать сразу несколько закладок, удерживая клавишу Ctrl. Работает только в том случае, если свойство Style содержит значение tsFlatButtons или tsButtons. |
OwnerDraw | Позволяет программно рисовать закладки в обработчике события OnDrawTab. Если свойство OwnerDraw равно значению False, то закладки имеют стандартный вид и событие OnDrawTab не происходит. |
RaggedRight | Если равно значению True, то при включенном режиме MultiLine закладки не выравниваются на ширину компонента. |
ScrollOpposite | Способ организации рядов закладок. Если равно значению False, то все ряды расположены вместе, например вверху. Если равно значению True, неактивные ряды переносятся на другую сторону компонента, например вниз. |
Style | Стиль закладок: tsTabs — обычные трехмерные закладки, tsFlatButtons — плоские закладки, tsButtons — закладки в виде кнопок. |
Tabs | Закладки в виде списка строк. |
TabIndex | Номер выбранной закладки. Если ни одна закладка не выбрана, то значение свойства равно -1. |
TabPosition | Местоположение закладок: tpTop — сверху, tpRight — справа, tpLeft — слева, tpBottom — снизу. |
TabWidth, TabHeight | Ширина и высота закладки. Если эти свойства равны нулю, то ширина и высота каждой закладки подбирается автоматически по ширине и высоте содержащегося на ней текста. |
OnChange | Происходит после смены закладки. |
OnChanging | Происходит перед сменой закладки. |
OnDrawTab | Происходит при рисовании закладки на экране. Требует, чтобы свойство OwnerDraw содержало значение True. |
OnGetImageIndex | Обработчик этого события должен вернуть номер значка для отображаемой закладки. |
OnGetSiteInfo | Происходит, когда у компонента запрашивается место для стыковки. |
Компонент TabControl — это фактически одна страница с множеством закладок. Компонент применяется в том случае, если страницы имеют одинаковый вид, а их переключение влечет лишь изменение отображаемых данных. А ведь так произошло с нашими экзаменационными билетами — все страницы содержали по одному единственному компоненту RadioGroup.
Каждая вкладка в компоненте PageControl потребляет системные ресурсы. Используя компонент TabControl вместо компонента PageControl, мы значительно снизим потребление оперативной памяти в нашем последнем примере, правда, за счет времени и сил, затраченных на программирование. Давайте не поленимся и переделаем пример с экзаменационными билетами так, чтобы в нем использовался компонент TabControl.
Шаг 12. Удалите из формы ExamForm компонент PageControl и поместите на его место компонент TabControl.
Рисунок 9.63. Компонент TabControl заменил в форме компонент PageControl
Шаг 13. В окне свойств выберите свойство Tabs и щелкните кнопку с многоточием. На экране появится редактор строк.
Рисунок 9.64. Список закладок для компонента TabControl
Шаг 14. Введите названия закладок и щелчком кнопки OK закройте окно. Закладки появятся на экране (рисунок 9.65).
Рисунок 9.65. В компоненте TabControl созданы три закладки
Шаг 15. Теперь внутрь компонента TabControl поместите группу взаимоисключающих переключателей и придайте ей соответствующие размеры и положение (рисунок 9.66).
Рисунок 9.66. Группа переключателей RadioGroup1 заготовлена для экзаменационного вопроса с вариантами ответа
Единственная группа взаимоисключающих переключателей будет поочередно играть роль билета по математике, по физике и по химии в зависимости от выбранной закладки.
На этом визуальная часть проектирования закончена. Все остальное придется программировать вручную.
Шаг 16. Сначала нужно позаботится о хранении содержания вопросов и их ответов, поэтому добавьте следующие описания в текст модуля MainUnit, поместив их перед всеми обработчиками событий:
const Questions: array[0..2] ofstring = ('Правильным является выражение', 'Когда лед в воде тает', 'Чтобы разбавить кислоту, нужно'); Answers: array[0..2, 0..2] ofstring = (('sin 50° < cos 50°', 'sin 50° > cos 50°', 'sin 50° = cos 50°'), ('уровень воды поднимается', 'уровень воды понижается', 'уровень воды остается неизменным'), ('добавить кислоту в воду', 'добавить воду в кислоту', '')); ValidAnswers: array[0..2] of Integer = (1, 2, 0); |
Шаг 17. Для промежуточного хранения ответов пользователя воспользуемся инициализированной переменной-массивом:
var UserAnswers: array[0..2] of Integer = (0, 0, 0); |
Шаг 18. Значения элементов этого массива должны изменяться, когда пользователь выбирает ответ, поэтому создайте компоненту RadioButton1 обработчик события OnClick:
procedure TExamForm.RadioGroup1Click(Sender: TObject); begin UserAnswers[TabControl1.TabIndex] := RadioGroup1.ItemIndex; end; |
Шаг 19. При смене закладки должен изменяться вопрос экзаменационного билета и возможные варианты ответов. Для этого создайте в компоненте TabControl1 обработчик события OnChange:
procedure TExamForm.TabControl1Change(Sender: TObject); var I: Integer; begin// Отобразить новый вопрос RadioGroup1.Caption := Questions[TabControl1.TabIndex]; // Стереть прежние варианты ответа RadioGroup1.Items.Clear; // Добавить новые варианты ответа в группу переключателейfor I := 0 to 2 doif Length(Answers[TabControl1.TabIndex, I]) > 0 then RadioGroup1.Items.Add(Answers[TabControl1.TabIndex, I]); // Установить ответ, принимаемый по умолчанию RadioGroup1.ItemIndex := UserAnswers[TabControl1.TabIndex]; end; |
Шаг 20. Все готово? Не совсем. Нужно заполнить компонент RadioGroup1 данными первого билета при появлении формы на экране. Проще всего это можно сделать, вставив вызов метода TabControl1Change в обработчик события создания формы:
procedure TExamForm.FormCreate(Sender: TObject); begin TabControl1Change(TabControl1); end; |
Шаг 21. Последний штрих — доработка метода выставления оценки:
procedure TExamForm.ResultButtonClick(Sender: TObject); const MarkText: array[0..3] ofstring = ('Неудовлетворительно', 'Удовлетворительно', 'Хорошо', 'Отлично'); var Mark: Integer; I: Integer; begin Mark := 0; for I := 0 to 2 doif UserAnswers[I] = ValidAnswers[I] then Mark := Mark + 1; ShowMessage(MarkText[Mark]); end; |
А теперь выполните компиляцию и запустите программу. С точки зрения пользователя оно не будет отличаться от программы, созданной ранее с помощью компонента PageControl. Думаем, что получив такой практический опыт, вы сможете сами сделать вывод о том, какой из двух компонентов (PageControl или TabControl) и в каких случаях следует использовать.
Нелегкая тропа создания окон диалога пройдена. Вы в этом деле стали настоящим гуру. Не верите? Да, этот так, ибо вы познали:
Напоследок позволим себе дать два совета: не заставляйте программу болтать лишнее и не загромождайте экран сложными окнами диалога без особой необходимости. Помните, что хороший пользовательский интерфейс должен быть простым.
Сообщений 2 Оценка 45 [+1/-0] Оценить |