Определите обработчик события Click для кнопки button5:
Результат. При нажатии кнопки «=» указанное выражение вычисляется и отображается на экране (в метке label2). В качестве операнда при любой операции можно указывать число 0; при делении на 0 результатом является «–бесконечность» или «бесконечность» (в зависимости от знака первого операнда) или «NaN» («не число»), если первый операнд также равен 0. В случае если поля ввода содержат текст, который нельзя преобразовать в вещественное число, то выводится результат «ERROR».
Комментарии
1. При выполнении операций над числами типа double ошибок времени выполнения не возникает, однако результатом может быть одно из «особых» значений: double.NegativeInfinity (–∞), double.PositiveInfinity (+∞) и double.NaN («не число»).
2. Для преобразования строки в число можно использовать методы Parse и TryParse соответствующего числового типа. Метод TryParse следует применять, если возможна ситуация, когда требуемое преобразование окончится неудачей (при использовании метода Parse такая ситуация приведет к возбуждению исключения, для обработки которого потребуется писать дополнительный код; кроме того, обработка исключения требует существенно больше времени, чем обычная проверка с помощью условного оператора).
3. В методе button5_Click демонстрируются важные особенности, связанные с типом string. Во-первых, тип string можно использовать в качестве переключателя в операторе switch, во-вторых, для типа string определена операция +, в которой в качестве другого операнда (причем не обязательно второго) можно указывать выражение любого типа; при этом данное выражение автоматически преобразуется к типу string с помощью метода ToString, определенного для любого типа платформы .NET.
Ошибка. Отмеченный в конце предыдущего пункта недочет теперь приводит к неправильной работе программы. После нажатия на кнопку «=» символ «=» указывается между полями ввода; таким образом, информация о выбранной операции стирается, и при последующем нажатии кнопки «=» всегда выводится нулевой результат (для восстановления нормальной работы надо повторно выбрать требуемую операцию, нажав на связанную с ней кнопку). Обратите внимание на то, что в данном варианте программы при наступлении события Click для кнопки «=» выполняются два обработчика: button5_Click, который связан непосредственно с этой кнопкой, и button1_Click, связанный с ее родительским компонентом StackPanel. Поскольку событие Click является пузырьковым, вначале выполняется обработчик button5_Click.
Исправление. В начало метода button5_Click добавьте оператор
Комментарий
Добавленный оператор помечает событие как обработанное, поэтому при передаче информации о данном событии вверх по иерархии родительских компонентов остальные обработчики не запускаются.
Итак, если для одного и того же туннелируемого или пузырькового события определен обработчик и в дочернем, и в родительском компоненте, то будут выполнены оба эти обработчика (причем порядок их вызова определяется категорией события). Однако имеется возможность прервать цепочку вызовов обработчиков данного события – для этого достаточно в одном из обработчиков пометить событие как обработанное описанным выше способом. Следует также заметить, что если туннелируемое событие (например, PreviewTextInput) помечено как обработанное, то не вызываются и все обработчики парного к нему пузырькового события (для события PreviewTextInput парным является TextInput). Соответствующий пример будет приведен в п. 3.4.
Обратите внимание на добавленные символы подчеркивания в свойствах Content.
Результат. Кнопка «=» (button5) сделана кнопкой по умолчанию и отображается в окне особым образом (рис. 13); эквивалентом ее нажатия является нажатие на клавишу Enter. Символы, указанные на кнопках, подчеркиваются; это является признаком того, что с каждой кнопкой связана клавиша-ускоритель Alt+«подчеркнутый символ». Следует иметь в виду, что в последних версиях Windows символы, с которыми связываются клавиши-ускорители, подчеркиваются только в случае, если предварительно нажать клавишу Alt.
Рис. 13. Окно приложения CALC с подчеркнутыми символами в подписях кнопок
Комментарий
В WPF-проектах для выделения символов, с которыми требуется связать клавишу-ускоритель, необходимо указать перед ними символ подчеркивания «_» (в той редкой ситуации, когда символ подчеркивания требуется использовать в надписи на компоненте, надо ввести этот символ дважды). Заметим, что в проектах Windows Forms для связи символа с клавишей-ускорителем использовался символ «&». Он был заменен на символ «_», поскольку в xaml-файле (как и в любом XML-файле) символ «&» интерпретируется особым образом.
Ошибка. После нажатия на любую кнопку с арифметической операцией все последующие вычисления возвращают значение, равное 0 (поскольку первым символом метки label1 теперь является символ подчеркивания '_', не предусмотренный в операторе switch). Кроме того, символ операции, изображенный между полями ввода, тоже подчеркивается.
Исправление. Измените оператор в методе button1_Click следующим образом:
Комментарий
Для удаления одного или нескольких начальных символов строки достаточно вызвать метод TrimStart, указав удаляемые символы в качестве параметров (если параметры не указывать, то удаляются пробельные символы). Имеется также метод TrimEnd, удаляющий конечные символы, и метод Trim, удаляющий как начальные, так и конечные символы. Чтобы в нашем случае можно было использовать данный метод, необходимо выполнить явное преобразование свойства Content к типу string.
Недочет. Теперь, когда программа содержит средства для быстрого выполнения действий с помощью клавиатуры, более наглядно проявляется недочет, который имелся в ней с самого начала: при запуске данной программы в ней отсутствует компонент, имеющий фокус. Для того чтобы фокус появился на первом поле ввода (и при этом в нем отобразился вертикальный курсор), необходимо либо щелкнуть мышью на этом поле, либо нажать клавишу Tab. Было бы удобнее, если бы фокус устанавливался на первое поле ввода сразу после запуска программы.
Исправление. Добавьте в конструктор класса MainWindow оператор:
Определите обработчик события PreviewTextInput для MainWindow:
Кроме того, измените метод button1_Click следующим образом:
Результат. Теперь для ввода любой операции достаточно нажать соответствующую клавишу (поскольку клавиша «–» может использоваться для ввода отрицательных чисел, в качестве ускорителя для кнопки «–» выбрана комбинация Shift+«–», соответствующая символу подчеркивания «_»). При вводе чисел игнорируются все клавиши, кроме цифровых, «–», «,» и Backspace (для обозначения символа, генерируемого клавишей Backspace, в C# можно использовать управляющую последовательность '\b'; нажатие этой клавиши обеспечивает удаление символа, расположенного слева от курсора в активном поле ввода).
Комментарии
1. При реализации описанных возможностей мы воспользовались тем, что событие PreviewTextInput является туннелируемым, т. е. вначале оно обрабатывается в родительском компоненте верхнего уровня (окне), а затем «спускается» по иерархии подчинения к тому компоненту, в котором возникло. Это позволило уже на уровне окна проанализировать введенный текст и выполнить требуемые действия по изменению арифметической операции (и, кроме того, «не пропустить» дальше те символы, которые не имеет смысла использовать в арифметических операндах). Напомним, что для прекращения последующих вызовов обработчиков данного события необходимо пометить событие как обработанное, положив свойство e.Handled равным true. Следует сказать, что пометка события как обработанного позволяет отменить вызов и стандартных обработчиков событий, связанных с компонентами (в нашем случае был отменен вызов стандартного обработчика события TextInput, обеспечивающего добавление набранного на клавиатуре символа в поле ввода).
2. Для имитации возникновения события, связанного с нажатием кнопок 1–4, в методе Window_PreviewTextInput вызывается обработчик данного события button1_Click. При этом необходимо указать нужную кнопку. Проще всего передать ее в качестве первого параметра обработчика, однако такой подход требует корректировки действий, содержащихся в методе button1_Click.
В операторе, добавленном в метод button1_Click, делается попытка привести параметр sender к типу Button. Если эта попытка успешна, то соответствующая кнопка помещается в переменную s. Если же указанное преобразование нельзя выполнить, то операция as возвращает значение null. Это означает, что обработчик был вызван родительским компонентом, а «истинный» адресат события содержится в свойстве e.Source, которое в этом случае приводится к типу Button и сохраняется в переменной s. Все описанные действия удалось реализовать в единственном операторе благодаря операции a ?? b, которая возвращает значение a, если оно не равно null, и b в противном случае.
Недочет 1. Если нажать клавишу пробела, находясь на одном из полей ввода, то пробел будет введен в это поле.
Это связано с тем, что пробел в WPF-приложениях обрабатывается особым образом: несмотря на то, что он является отображаемым символом и, казалось бы, нажатие на него должно приводить к возникновению события TextInput (и предшествующего ему события PreviewTextInput), этого не происходит. Таким образом, если мы хотим заблокировать ввод пробелов, это придется сделать с помощью дополнительного обработчика.
Исправление. Определите для компонента StackPanel, содержащего поля ввода, обработчик события PreviewKeyDown:
Комментарий
При нажатии пробела возникают только события KeyDown и KeyUp (и связанные с ними события PreviewKeyDown и PreviewKeyUp), которые реагируют на нажатие любых клавиш, в том числе и не приводящих к генерации отображаемых символов. Мы перехватываем это событие на уровне родителя обоих полей ввода, поэтому оно не доходит до них и пробелы в полях ввода не отображаются.
Заметим, что перехватывать событие на более высоком уровне (на уровне вертикальной панели StackPanel или на уровне окна), не следует, так как в этом случае нажатие пробела не дойдет и до других компонентов окна, в частности, до кнопок. В результате станет недоступной возможность нажать кнопку, выделив ее и нажав клавишу пробела.
Недочет 2. В нашей программе предполагается, что десятичным разделителем является запятая, тогда как при других региональных настройках в системе Windows может использоваться другой разделитель.
Исправление. Измените фрагмент последнего оператора в методе Window_PreviewTextInput:
Комментарий
Статическое свойство CurrentCulture класса CultureInfo, определенного в пространстве имен System.Globalization, позволяет получить информацию о региональных настройках, используемых операционной системой, в частности о числовых форматах. Необходимость в указании индекса [0] обусловлена тем, что свойство NumberDecimalSeparator имеет строковый тип, который не совместим по присваиванию с символьным типом. Заметим, что свойство NumberDecimalSeparator доступно только для чтения, однако имеется возможность изменить региональные настройки в целом для конкретного приложения (см. по этому поводу комментарий в проекте CLOCK, п. 4.1).
Добавьте в метод button1_Click следующий оператор:
Кроме того, определите для поля ввода textBox1 обработчик события TextChanged, а также свяжите этот обработчик с полем ввода textBox2:
Результат. При изменении операции или содержимого текстовых полей результат предыдущего вычисления стирается. Это важная возможность, позволяющая предотвратить рассогласование отображаемых данных. При ее отсутствии возможна ситуация, когда после выполнения, например, вычислений вида 3 + 2 (с результатом 5), пользователь изменит первый операнд на 2, получив на экране текст 2 + 2 = 5.
Комментарии
1. Здесь мы использовали более традиционный способ связывания одного обработчика события с несколькими компонентами – путем указания этого обработчика в xaml-файле в описании каждого компонента.
Впрочем, в данном случае тоже можно было воспользоваться механизмом маршрутизируемых событий и после создания обработчика textBox1_TextChanged для компонента textBox1 не копировать соответствующий атрибут в компонент textBox2, а переместить его в родительский компонент StackPanel, снабдив префиксом TextBox:
2. Указание обработчиков событий, подобных событию TextChanged, непосредственно в xaml-файле может приводить к неожиданным ошибкам. Например, если закомментировать условный оператор в методе textBox1_TextChanged
то при запуске программы возникнет исключение NullReferenceException («Ссылка на объект не указывает на экземпляр объекта»). Это связано с тем, что в WPF событие TextChanged возникает сразу после конструирования поля ввода (при присваивания свойству Text начального значения). Но в момент создания поля ввода textBox1 метка label2 еще не существует, поскольку компоненты создаются в порядке их указания в xaml-файле, что и приводит к возникновению исключения.
Мы избежали этой ошибки, добавив проверку в обработчик. Исправить подобную ошибку можно и другим способом: не добавляя проверку в метод textBox1_TextChanged, удалить оба атрибута TextChanged="textBox1_TextChanged" в xaml-файле и вместо этого добавить в конец конструктора MainWindow операторы
Благодаря этим операторам связывание события TextChanged с обработчиками будет происходить уже после создания всех компонентов окна, и попытки обращения к неинициализированной метке не произойдет.
При анализе данного исправления возникает естественный вопрос: можно ли в программном коде связать требуемый обработчик с общим родителем обоих полей ввода – панелью StackPanel (подобно тому, как это делается в xaml-файле – см. комментарий 1)? Этому препятствуют два обстоятельства: во-первых, данная панель не имеет имени, с помощью которого к ней можно было бы обратиться в коде, и, во-вторых, в компоненте StackPanel отсутствует событие TextChanged. Первую проблему легко решить, добавив к описанию панели в xaml-файле атрибут x:Name. Вторая проблема решается благодаря наличию у любого компонента метода AddHandler, позволяющего связать с компонентом обработчик события даже в случае, если это событие для компонента не предусмотрено. Между прочим, первую из отмеченных проблем можно вообще не решать, если связать обработчик с родителем более высокого уровня – окном MainWindow. Таким образом, вместо указанных выше двух операторов достаточно добавить в конструктор окна следующий вызов метода AddHandler:
Обратите внимание на необходимость указания суффикса Event в первом параметре метода AddHandler и на более сложный способ определения второго параметра, требующий вызова конструктора класса, связанного с данным типом обработчиков событий.
Рис. 14. Окно приложения CLOCK
В список директив using в начале файла MainWindow.xaml.cs добавьте директиву:
В описание класса MainWindow добавьте поле
В конструктор класса добавьте следующие операторы:
Опишите в классе MainWindow обработчик события Tick для таймера (этот обработчик придется ввести полностью, вместе с его заголовком, так как заготовку для него нельзя создать с помощью окна Properties или xaml-файла):
Результат. При работе программы в ее окне отображается текущее время (рис. 15).
Рис. 15. Окно приложения CLOCK (первый вариант)
Комментарии
1. Для работы с датами и временем в библиотеке .NET предусмотрена структура DateTime. Ее статическое свойство Now, доступное только для чтения, возвращает текущую дату и время (по системным часам компьютера). Текущую дату без времени (время соответствует полуночи) можно получить с помощью статического свойства Today. Для преобразования даты/времени к их стандартным строковым представлениям можно использовать следующие методы структуры DateTime:
• ToShortDateString – дата в кратком формате «d», например «27.01.1756»;
• ToLongDateString – дата в полном формате «D», например «27 января 1756 г.»);
• ToShortTimeString – время в кратком формате «t», например «10:55»;
• ToLongTimeString – время в полном формате «T», например «10:55:15».
Метод ToString без параметров возвращает дату/время в формате «G» (дата в кратком формате, время в полном). Формат отображения даты/времени можно явно указать в методе ToString; например, в нашей программе можно было бы использовать такой вариант: DateTime.Now.ToString("T").
Упомянем еще некоторые форматы для даты/времени: «g» – дата и время в кратком формате, «F» – дата и время в полном формате, «f» – дата в полном формате, время в кратком, «M» или «m» – формат «месяц, день», «Y» или «y» – формат «месяц, год».
При форматировании дат используются текущие региональные настройки (в нашем случае – настройки для России), хотя имеется перегруженный вариант метода ToString, где можно явно указать требуемую региональную настройку. Можно также сменить региональную настройку для приложения в целом; для этого достаточно установить новое значение свойства CurrentCulture для объекта Thread.CurrentThread из пространства имен System.Threads. Например, для того чтобы установить для нашего приложения региональные настройки, соответствующие американскому варианту английского языка, достаточно добавить в конструктор следующий оператор:
При этом вариант отображения текущего времени в окне изменится (рис. 16).
Рис. 16. Окно приложения CLOCK с измененными региональными настройками
Заметим, что настройки для России имеют имя «ru-RU».
2. В отличие от библиотеки Windows Forms, где предусмотрен специальный невизуальный компонент Timer, в библиотеке WPF приходится использовать «обычный» объект типа DispatcherTimer (из пространства имен System.Windows.Threading), явным образом задавая в программе все его свойства и события. Обратите внимание на то, что свойство Interval (время между срабатываниями таймера) имеет тип TimeSpan (этот тип подробно описывается в последнем комментарии к следующему пункту).
3. Особенностью макета данного приложения является использование рамки Border вокруг метки с текстом текущего времени. Для отображения времени мы использовали специальную текстовую метку TextBlock, содержимым которой (в отличие от «обычной» метки Label) может быть только текст. В качестве имени для этого компонента мы выбрали label1 как более кратное и наглядное по сравнению с textBlock1.
Недочет. В течение первой секунды после запуска программы в метке сохраняется исходный текст «00:00:00», так как событие Tick возникает первый раз только через промежуток времени timer1.Interval, равный в нашем случае 1000 миллисекундам.
Исправление. Добавьте вызов обработчика для таймера в конструктор окна MainWindow:
В классе MainWindow определите обработчики, уже добавленные в него в результате указания атрибутов Click в xaml-файле:
Кроме того, добавьте в класс MainWindow новое поле
а также дополните обработчик timer1_Tick:
Результат. При установке флажка Timer во включенное состояние программа переходит в режим секундомера, причем секундомер сразу запускается, отображая на экране секунды и десятые доли секунд (рис. 17). Запуск и остановка секундомера осуществляются по нажатию кнопки Start/Stop, сброс секундомера – по нажатию кнопки Reset. Доступны клавиши-ускорители: Alt+T (смена режима «часы/секундомер»), Alt+S (старт/остановка секундомера), Alt+R (сброс секундомера).
Рис. 17. Окно приложения CLOCK в режиме секундомера
Комментарии
1. Свойство IsChecked компонента CheckBox имеет в WPF тип bool?, т. е. может принимать три значения: true, false и null (последний вариант используется для флажков с тремя состояниями), поэтому в условии оператора if приходится выполнять приведение свойства checkBox1.IsChecked к типу bool (вместо этого можно использовать сравнение checkBox1.IsChecked = true).
2. Для формирования текста метки в режиме секундомера используется метод Format класса string, возвращающий строку, которая содержит фиксированные фрагменты и строковые представления различных объектов, отформатированные требуемым образом. Первым параметром данного метода является форматная строка, содержащая как обычный текст, так и форматные настройки для остальных параметров (количество подобных форматируемых параметров может быть произвольным). Форматные настройки заключаются в фигурные скобки {}; в нашем случае использованы простейшие форматные настройки, в которых задается только порядковый номер параметра, выводимого в указанной позиции форматной строки (в подобной простейшей ситуации для форматирования данного параметра автоматически вызывается его метод ToString). Параметры нумеруются от 0.
В версии C# 6.0, используемой в Visual Studio 2015, для формирования строк с различными «внешними» параметрами вместо метода Format удобнее применять так называемые интерполированные строки. В интерполированной строке перед открывающей ее двойной кавычкой указывается символ $; а параметры задаются в ней в фигурных скобках. С использованием интерполированной строки оператор задания текста метки в режиме секундомера можно представить в следующем виде:
Таким образом, интерполированная строка представляет собой форматную строку метода Format, в которой вместо порядкового номера выводимого параметра указывается сам этот параметр.
Недочет. При изменении режима изменяется ширина окна, «подстраиваясь» под текущий размер текста, выводимого на метке. Однако в данном случае изменение размеров окна не представляется оправданным. В частности, оно нарушит выравнивание окна по центру экрана. Кроме того, в режиме секундомера окно будет изменять размер во многих ситуациях, например, при переходе от 9 секунд к 10, от 99 секунд к 100, а также при сбросе значения секундомера.
Исправление. Добавьте к элементу Border в xaml-файле новый атрибут:
Результат. Теперь ширина окна остается неизменной в любом режиме.
Ошибка. Кажущаяся правильность работы секундомера обманчива. В этом можно убедиться, если не останавливать секундомер в течение некоторого времени (выполняя при этом другие действия на компьютере), после чего сравнить результат с точным временем. Причина заключается в том, что событие Tick наступает примерно через каждые 100 мс; кроме того, надо учитывать, что данное событие наступает только при отсутствии других событий, которые требуется обработать программе. Если программу выполняет какой-либо обработчик длительное время, то в течение этого времени информация секундомера не будет обновляться, а затем отсчет времени продолжится с прежнего значения. Для правильной реализации секундомера надо связать его с часами компьютера (используя метод Now).
Исправление. В описании класса MainWindow удалите описание поля t и добавьте описание новых полей:
Поле startTime будет содержать время начального запуска секундомера; поле pauseTime – время последней остановки секундомера, а поле pauseSpan – суммарную длительность всех остановок, выполненных после начального запуска.
Метод checkBox1_Click измените следующим образом (приведен только тот фрагмент метода, который требует изменения):
Аналогичные изменения внесите в метод button2_Click:
Добавьте в метод button1_Click операторы:
И откорректируйте метод timer1_Tick:
Комментарий
Структура TimeSpan предназначена для хранения относительных промежутков времени. Промежутки времени измеряются в днях, часах, минутах, секундах и миллисекундах, причем для получения значения каждого из этих компонентов можно использовать соответствующие свойства структуры TimeSpan: Day, Hour, Minute, Second, Millisecond (заметим, что эти же свойства имеются и у структуры DateTime; кроме того, у структуры DateTime есть свойства Year и Month). Для задания нулевого промежутка времени проще всего воспользоваться полем TimeSpan.Zero, доступным только для чтения. И структура DateTime, и структура TimeSpan имеют также поля для чтения, определяющие их наименьшие и наибольшие возможные значения – MinValue и MaxValue.
Операции сложения и вычитания определяются для структур DateTime и TimeSpan следующим образом: сумма или разность значений типа TimeSpan имеет тип TimeSpan; сумма или разность значений типа DateTime и TimeSpan (в указанном порядке) имеет тип DateTime; разность значений типа DateTime имеет тип TimeSpan; складывать значения типа DateTime нельзя. Поскольку для промежутков времени допускаются отрицательные значения, для структуры TimeSpan определена также операция «унарный минус».
Для создания объектов типа DateTime и TimeSpan с требуемыми значениями проще всего воспользоваться одним из предусмотренных для них конструкторов. Конструктор без параметров возвращает для DateTime минимальную дату (полночь 1 января 1 года н. э.), а для TimeSpan – нулевой промежуток времени. В остальных конструкторах DateTime необходимо указывать год, месяц, день (и можно указать дополнительно время в часах, минутах и секундах, возможно, дополненное параметром со значением миллисекунд). В конструкторах TimeSpan необходимо указывать час, минуту и секунду. В качестве дополнительного начального параметра можно указать число дней. Если указано число дней, то можно указать дополнительный последний параметр – количество миллисекунд.
О других методах и свойствах структур DateTime и TimeSpan можно прочесть, например, в [4, гл. 2] и [5, гл. 6].