C/C++ Пишем программу часы-будильник- календарь
в среде Borland C++ Builder 5.0-6.0
На сайте http://prog-begin.net.ru
эту статью разместил 17.04.2005 Dr_Andrew
Статья написана в рамках проекта проекты::редактор_кода.
В ней на примере создания простой, но полезной программы будильника-календаря
разбираются функции и компоненты доступа к свойствам времени
и даты. Даются перспективы превращения программы в полноценный
органайзер.
В те далёкие времена, когда я издевался над Linux, меня
«зацепила» маленькая, но очень приятная программка, встроенная
в KDE – Kalarm. Помимо вывода в заданное время простых текстовых
сообщений, программка обладала способностью проигрывать в
это самое время выбранную вами мелодию. Это привело к тому,
что я презрел обычный будильник и утро встречал только с песней!
После того, как в связи с некоторыми обстоятельствами я был
вынужден снести Linux с диска, мне очень не хватало этой программы.
Так в чём же дело! Сейчас мы попытаемся слепить «на коленке»
нечто подобное! Как два плюса написать! ;)
Итак, создайте новый проект Borland C++ Builder. Задайте
в инспекторе объектов значения свойств главной формы следующим
образом:
BorderIcons – biMaximize – false (чтобы пользователь не мог
развернуть окно программы на весь экран), BorderStyle – bsSingle
(чтобы пользователь не мог растянуть границы окна мышью),
Caption – Часы-календарь, Name – MainForm, Position – poDesktopCenter
(чтобы окно программы при запуске располагалось в центре рабочего
стола), ShowHint – true (чтобы у всех компонентов формы свойство
показа подсказок также было true).
Поместите на форму компонент GroupBox (вкладка панели инструментов
Standard). Подкорректируйте его размеры так, чтобы он занимал
чуть меньше половины ширины формы. Задайте следующие значения
его свойств в инспекторе объектов:
Caption – Текущее время:, Font – Style – fsBold (жирный шрифт),
Name – TimeBox.
Выделите TimeBox на форме и поместите в него Panel (вкладка
Standard). Её свойство Caption послужит нам для вывода текущего
времени. Откорректируйте размеры панели так, чтобы она размещалась
в центре TimeBox. Задайте следующие значения свойств панели
в инспекторе объектов:
AutoSize – false, BevelInner – bvLowered, очистите значение
свойства Caption, щёлкните по кнопке с многоточием рядом с
полем свойства Font, чтобы задать значения свойств шрифта:
размер – 24, жирный, Name – TimePanel.
Прежде чем перейти к программированию, отвлечёмся ещё немного
на проектирование формы. Выделите наш TimeBox и нажмите клавиши
Ctrl+C, чтобы скопировать его в буфер обмена. После этого
нажмите клавиши Ctrl+V, чтобы добавить на форму ещё один GroupBox
с содержащейся в нём панелью. Заметим, что в новых компонентах
все внесённые нами изменения свойств сохранились (кроме, естественно,
имени – Name, т.к. в программе не может быть двух переменных
с одинаковым именем). Замечу, что способ копировать – вставить
очень удобен при создании нескольких объектов с одинаковыми
свойствами (например, группы кнопок). Только стоит помнить,
что этим приёмом можно пользоваться лишь на этапе проектирования:
нельзя копировать кнопки (и вообще компоненты) с уже написанными
обработчиками событий, т.к. попытки последние изменить приведут
к неработоспособности программы.
Перетащите новый GroupBox с содержащейся в нём панелью под
TimeBox. Задайте следующие значения свойств GroupBox:
Caption – Время оповещения:, Name – AlarmBox.
Переименуйте содержащуюся в нём панель (изменив свойство
Name) в AlarmPanel.
Теперь можно приступать к выводу на панель TimePanel текущего
времени. Для этого нам понадобится компонент Timer (вкладка
System). Задайте его имя (Name) как Timer, остальные свойства
оставьте по умолчанию. Таймер позволяет нам генерировать событие
OnTimer с частотой, определяемой его свойством Interval. По
умолчанию оно равно 1000 миллисекунд (или одной секунде).
Это нас вполне устроит. Когда мы вызываем обработчик события
OnTimer, происходит сброс таймера и отсчёт промежутка времени,
определённого свойством Interval, начинается снова. В качестве
обработчика события у нас выступит вывод текущего времени
на панель с использованием ей свойства Caption.
Чтобы получить текущее время, в среде Borland C++ Builder
имеется функция Time(); для получения – текущей даты – Date().
Обе эти функции возвращают значение типа TDateTime. Рассмотрим
его подробнее. Этот тип представляет собой число с плавающей
точкой, целая часть которого содержит число дней, отсчитанных
от 0 часов 30 декабря 1899 года, а дробная часть равна части
24-часового дня, отсчитанная от полудня (12 часов). Т.е. дробная
часть характеризует время и не относится к дате. Подобный
способ представления данных не слишком-то удобен для практического
использования, поэтому в среде Borland C++ Builder имеется
ряд функций для преобразования типа TDateTime в более удобоваримую
форму – строку (AnsiString). Функция DateToStr() выводит в
строку дату, TimeToStr() – время, а DateTimeToStr() – дату
и время, причём форма представления данных определяется установкой
глобальных переменных (зависит от настроек Вашей операционной
системы). Для более гибкого представления данных существуют
другие способы, которые мы рассмотрим ниже. Пока же внесём
строчку в обработчик события OnTimer нашего таймера:
void __fastcall TMainForm::TimerTimer(TObject *Sender)
{
TimePanel->Caption=TimeToStr(Time());
}
Мы преобразовали текущее время, полученное функцией Time()
в строку и присвоили её свойству Caption панели. Поскольку
подобное присваивание будет происходить каждую секунду по
событию OnTimer, мы получили простейшие электронные часы.
Теперь пришла пора подумать о будильнике. Что нам понадобится
для этого? Во-первых, какой-нибудь объект, в котором мы будем
задавать значение времени «побудки» (оповещения), медиаплейер
для воспроизведения мелодии оповещения, 2 кнопки для задания
времени и отмены и диалог открытия файла для выбора музыкального
файла.
Помимо этого, нам нужны переменные для хранения следующих
значений: информации о том, включён или выключен будильник
(т.е. тип bool), переменная для хранения текущего часа, переменная
для хранения текущих минут, переменная для хранения часа оповещения
и переменная для хранения минуты оповещения (все типа int).
Перейдите в редактор кода и нажмите клавиши Ctrl+F6. Откроется
вкладка с исходным текстом заголовочного файла главной формы
(MainUnit.h). В раздел private класса главной формы добавьте
следующие переменные:
bool bIsTimerOn; // Таймер включён?
int iHours; // Текущий час.
int iMinutes; // Текущая минута.
int iAlarmHours; // Час оповещения.
int iAlarmMinutes; // Минута оповещения.
Возникает вопрос, а для чего нам столько переменных? Разве
нельзя сравнить непосредственно текущее время, полученное
от функции Time() и время оповещения, заданное в упомянутом
выше объекте, ведь оно, очевидно, также будет относиться к
типу TDateTime? К сожалению нет. Как Вы помните, тип TDateTime
представляет собой число с плавающей точкой, которые являются
неточными или приближёнными числами. Поэтому, хотя сравнение
на преобладание (больше / меньше) этих чисел и допустимо (но
не рекомендуется), то сравнение на равенство / неравенство
невозможно, несмотря на то, что компилятор не выдаст сообщение
об ошибке, и программа откомпилируется без проблем. При попытке
сравнения на равенство двух чисел с плавающей запятой поведение
программы предсказать невозможно. Обычно она просто не работает.
Поэтому полученное время ещё предстоит преобразовать.
Но вначале нам это время следует задать. Сделаем мы это с
использованием компонента DateTimePicker, расположенного на
вкладке Win32 палитры компонентов. Компонент может работать
в двух режимах, определяемых свойством Kind: ввода времени
(значение dtkTime) и ввода даты (dtkDate). Сейчас нам нужно
именно первое значение. Поместите на AlarmPanel компонент
DateTimePicker и откорректируйте его размеры так, чтобы он
красиво располагался в центре панели. Задайте следующие значения
его свойств в инспекторе объектов:
Hint – Установите в таймере время для оповещения, Kind –
dtkTime, Name – TimePicker, очистите значение свойства Time
(оно автоматически установится в 0:00:00.
Поместим на форму также 2 кнопки типа TButton с вкладки Standard.
Свойства первой кнопки зададим такими:
Caption – Задать, Hint – Задать время оповещения, установленное
в таймере, Name – AlarmBttn.
У второй:
Caption – Отменить, Hint – Отменить оповещение, Name – CancelAlarmBttn.
Также на форму поместим компонент MediaPlayer с вкладки System
и OpenDialog с вкладки Dialogs.
Значения свойств медиаплейера зададим следующими:
AutoEnable – true, AutoOpen – false (открывать музыкальный
файл будем сами), DeviceType – dtAutoSelect (плейер автоматически
определяет тип устройства для воспроизведения), установите
в false свойства EnabledButtons и VisibleButtons для кнопок
btNext, btPrev, btStep, btBack, btRecord (они нам не понадобятся),
Name – MediaPlayer.
Проделаем эту манипуляцию и с объектом типа TOpenDialog:
DefaultExt - *.mp3 (расширение по умолчанию - *.mp3), Filter
– Все поддерживаемые типы / *.mp3; *.wav; *.mid; *.rmi, Name
– AlarmOpenDialog, Options – установите в true ofExtensionDifferent,
ofPathMustExist, ofFileMustExist, Title – открыть мелодию
для оповещения.
Что ж, теперь мы вовсю готовы для кодинга!
Напишем обработчик события главной формы OnActivate, в котором
переменной bIsTimerOn присвоим значение false. Это наш флаг
для таймера, что будильник выключен (bIsTimerOn=false; //
Будильник выключен).
Разумеется, мы могли бы для этого воспользоваться вторым
таймером (их на форме может быть несколько), но зачем объявлять
объект, когда можно обойтись одной переменной!
Что же дальше? Мы должны спросить себя: «а как работает приличный
будильник?» Ответ прост: пользователь задаёт время для оповещения,
выбирает мелодию звонка и подтверждает операцию. Время пользователь
будет выбирать в компоненте TimePicker, о чём его и предупреждает
подсказка, а 2 другие операции можно реализовать в обработчике
события OnClick кнопки AlarmBttn:
void __fastcall TMainForm::AlarmBttnClick(TObject *Sender)
{
do
{
if(AlarmOpenDialog->Execute()) // Если диалог открытия
запущен…
{
MediaPlayer->FileName=AlarmOpenDialog->FileName;
// …выберем файл для воспроизведения медиаплейером.
}
if(MediaPlayer->FileName=="") Application->MessageBox("Вы
должны выбрать мелодию для оповещения!", "Будильник",
MB_OK+MB_ICONWARNING);
}
while(MediaPlayer->FileName=="");
iAlarmHours=StrToInt(FormatDateTime("h", TimePicker->Time));
iAlarmMinutes=StrToInt(FormatDateTime("n", TimePicker->Time));
MediaPlayer->Open();
bIsTimerOn=true; // Таймер будильника запущен.
}
Здесь мы видим довольно садистский код. ;)) Пока пользователь
не выбрал файл для воспроизведения, ему будет выводиться окошко
с предупреждающим сообщением. И лишь когда имя файла будет
не пусто, медиаплейер откроет файл, а будильник будет запущен
(bIsTimerOn=true;). Впрочем, возможен и менее радикальный
вариант: если пользователь нажал в окне выбора файла кнопку
отмены, мы просто выводим ему сообщение, что будильник не
включён, т.к. не выбрана мелодия для воспроизведения. Разумеется,
в этом случае bIsTimerOn=false; и мы обходимся без цикла.
Кроме того, мы задаём час и минуту побудки, переведя их в
целые числа (для возможности сравнения с текущим временем)
и сохранив значения в объявленных ранее переменных iAlarmHours
и iAlarmMinutes. Для этого мы воспользовались свойством Time
компонента TimePicker, в котором пользователь задал время
оповещения. Оно и было сохранено в свойстве Time (тип значения
TDateTime). Затем мы преобразовали пользовательское время
в строку с использованием функции FormatDateTime(). Она гораздо
удобнее, нежели функции доступа к свойствам даты-времени,
упомянутые выше, т.к. позволяет форматировать вывод данных.
В качестве первого аргумента функция принимает строку – флаг
формата вывода данных, а в качестве второго – время или дату
(тип значения TDateTime), которую нужно преобразовать.
Для времени спецификаторы вывода могут быть следующими:
h – печать часа без начального нуля (0-23);
hh – печать часа с начальным нулём (00-23);
n – печать минут без начального нуля (0-59);
nn – печать минут с начальным нулём (0-59);
s – печать секунд без начального нуля (0-59);
ss – печать минут с начальным нулём (0-59).
Тем самым код FormatDateTime("h", TimePicker->Time);
становится понятен: мы переводим время из свойства TimePicker->Time
в «удобоваримую» форму в виде строки. После этого строку преобразуем
в целое, воспользовавшись функцией StrToInt, и сохраняем результат
в переменных:
iAlarmHours=StrToInt(FormatDateTime("h", TimePicker->Time));
iAlarmMinutes=StrToInt(FormatDateTime("n", TimePicker->Time));
Теперь нагрузим работой наш бедный таймер, которому каждую
секунду (если, конечно запущен будильник bIsTimerOn=true;),
помимо отрисовки текущего времени, предстоит сравнивать его
с заданным, чтобы решить, не пора ли врубить музыку:
void __fastcall TMainForm::TimerTimer(TObject *Sender)
{
TimePanel->Caption=TimeToStr(Time());
if(bIsTimerOn==true) // Если будильник включён…
{
iHours=StrToInt(FormatDateTime("h", Time()));
iMinutes=StrToInt(FormatDateTime("n", Time()));
// …получим текущее время…
if((iHours==iAlarmHours) && (iMinutes>=iAlarmMinutes))
// …и если оно совпало с заданным пользователем…
{
MediaPlayer->Play(); // …играем!
}
if(MediaPlayer->Mode==mpPlaying) bIsTimerOn=false;
// Если медиаплейер уже играет, то будильник можно выключить.
}
}
Всё! Будильник готов! Теперь утром Вас разбудит не надоедливый
звон, а приятная мелодия… колыбельная в исполнении Лоретти,
например! ;) Теперь остались последние штрихи к портрету:
возможность пользователю отменить действо, если он по каким-либо
причинам передумал будиться (по утрам, работая с реальным
будильником мы обычно с размаху промахиваемся по этой кнопочке
:)).
void __fastcall TMainForm::CancelAlarmBttnClick(TObject *Sender)
{
bIsTimerOn=false;
if(MediaPlayer->Mode==mpPlaying) MediaPlayer->Stop();
TimePicker->Time=StrToTime("0:00:00"); // Сброс
на ноль.
}
Завершающим мазком в нашей картине послужит создание календаря.
Для этого можно воспользоваться в принципе уже известным нам
компонентом DateTimePicker, но мы пойдём другим путём! :)
Мы возьмём компонент с той же вкладки – MonthCalendar, похожий
на DateTimePicker в режиме ввода дат. Для того, чтобы компонент
выглядел несколько «притопленным» на форме (тенденция Microsoft
отказываться в своих продуктах от объёмности элементов управления
меня пугает), воспользуемся квадратом Bevel с вкладки Additional.
Установим значения его свойств следующим образом:
Height – 156 (чтобы в него поместился календарь), Name –
MonthBevel, Width – 193.
Теперь в квадрате можно уютно разместить наш календарик и
заняться изменением его свойств:
Name – MonthCalendar, ShowToday – true, ShowTodayCircle –
true (для того, чтобы текущая дата на календаре выделялась
и обводилась кружочком (впрочем, в WindowsXP это уже квадратик,
но из песни слов не выкинешь! :)).
Календарь хорош всем, но у него один недостаток: он отражает
текущую дату, но не пишет полного названия дня недели (для
особо непонятливых :)). Что ж, этот недостаток мы готовы исправить.
Добавьте на форму компонент Label с вкладки Standard. Изменим
(и это уже в последний раз!) его свойства:
Aligment – taCenter (чтобы надпись располагалась по центру),
AutoSize – false (дабы чёртова этикетка не меняла свой размер,
как ей вздумается; напротив, растяните её побольше да так
и оставьте), очистите свойство Caption (его мы будем заполнять
программно), Name – DayOfWeekLabel.
Для заполнения свойства Caption этикетки воспользуемся уже
знакомой нам функцией FormatDateTime, но работать будем уже
не со временем, а с датой. Вот некоторые спецификаторы формата
вывода дат у этой функции:
d – печать дня без начального нуля (1-31),
dd – печать дня с начальным нулём (01-31),
ddd – печать дня в виде аббревиатуры (пн-вс),
dddd – печать дня в виде его полного названия (понедельник
– воскресенье),
m – печать месяца без начального нуля (1-12),
mm – печать месяца с начальным нулём (01-12),
mmm – печать месяца в виде аббревиатуры (янв-дек),
mmmm – печать полного названия месяца (Январь-Декабрь),
yy – печать двух последних цифр года (00-99),
yyyy – полная печать года (0000-9999).
Понятно, что код отображения текущего дня недели в этикетке
будет таким:
DayOfWeekLabel->Caption=FormatDateTime("День недели
- dddd", Date());
Но куда добавить эту строчку? Первое (самое привычное) решение
– это инициализация переменных класса формы при её активации:
void __fastcall TMainForm::FormActivate(TObject *Sender)
{
bIsTimerOn=false; // Будильник выключен.
MonthCalendar->Date=Date(); // В календаре – текущая
дата.
DayOfWeekLabel->Caption=FormatDateTime("День недели
- dddd", Date());
// Текущий день недели.
}
Программа будет откомпилирована без ошибок и даже на первый
взгляд правильно работать. Здесь кроется одна из тех трудноуловимых
ошибок, которые называются ошибками хода выполнения программы
или логическими. Компилятор в этом случае нам не помощник,
и ошибку может выявить либо сам программист, либо тестер,
либо – к своему несчастью! – пользователь. На жаргоне программистов
она называется bug (баг – англ. жучок). В чём же дело? А дело
в том, что после 0 часов значение DayOfWeekLabel->Caption
перестанет соответствовать действительности! Поэтому необходимо
ввести проверку дня недели на валидность, ещё больше загрузив
беднягу таймер (ничего, у нас процессор мощный и «мозгов»
хватает :)), внеся строчку отображения дня недели (и, кстати,
присвоения даты компоненту MonthCalendar) в событие OnTimer:
void __fastcall TMainForm::TimerTimer(TObject *Sender)
{
TimePanel->Caption=TimeToStr(Time());
DayOfWeekLabel->Caption=FormatDateTime("День недели
- dddd", Date());
MonthCalendar->Date=Date(); // В календаре – текущая
дата.
и т.д.
Ну что ж, простейший электронный календарь-часы готов. Разумеется,
эта программа – только скелет, функциональность которой можно
значительно расширить, превратив в полноценный органайзер:
добавить поддержку вывода текстовых сообщений-напоминалок
в определённый день и час (для этого нужно реализовать запись
в файл), небольшую базу данных с таблицами «контакты», «задачи»,
«встречи» и т.п., т.е. замутить супер-пупер-мега записную
книжку (салют, Таня! :)). Но тут моя муза скромно умолкает,
оставляя полную свободу творчества читателю. Дерзайте! И не
забывайте присылать свои исходники для размещения на сайте:
народ вас не забудет!!! :)