Read only on Litres

This book cannot be downloaded as a file but can be read in our app or online on the website.

Read the book: «Разработка пользовательского интерфейса на основе технологии Windows Presentation Foundation»

Font::

Предисловие

Книга знакомит читателя с основными приемами разработки пользовательского интерфейса на основе системы Windows Presentation Foundation (WPF), входящей в платформу .NET, начиная с версии 3.0. Данная система была создана с целью снять ряд серьезных ограничений, имевшихся у ее предшественницы – системы разработки интерфейса Windows Forms, изначально входившей в состав платформы .NET. При этом наряду с сохранением концепций, лежащих в основе Windows Forms (в частности, механизма обработки событий), система WPF была дополнена новыми технологиями, позволяющими разрабатывать интерфейсы, имеющие существенно более широкие графические возможности и, что не менее важно в современном мире планшетов и смартфонов, допускающими автоматическую адаптацию к особенностям устройств, на которых запущено приложение.

Новой важной чертой системы WPF для разработчиков приложений стало явное разграничение программного кода и макета приложения, которое было обеспечено включением в состав проекта XML-файлов, позволяющих описывать визуальный интерфейс на особом декларативном языке разметки XAML (eXtensible Application Markup Language). Следует отметить, что в настоящее время подобный подход реализован в большинстве систем разработки интерфейсов.

Разработчики приложений на основе технологии WPF получили в свое распоряжение новые концепции, основанные на свойствах зависимостей, маршрутизируемых событиях и привязке данных. Созданные в рамках WPF иерархии классов и визуальных компонентов, в том числе компонентов с содержимым и группирующих компонентов, обеспечивающих динамическую компоновку своих дочерних элементов, позволили гибко комбинировать интерфейсные элементы, обеспечивая подходящее визуальное представление на экране любого размера.

Принципиально новой по сравнению с Windows Forms стала графическая подсистема, основанная на технологии DirectX и позволяющая реализовывать качественную и быструю двумерную и трехмерную графику и анимацию. Был осуществлен переход на аппаратно-независимую систему единиц измерения (вместо экранных пикселов), обеспечивший одинаковый внешний вид приложения на экране с любым разрешением.

Не последнее место в списке преимуществ технологии WPF занимает удобство разработки WPF-приложений, которое обеспечивается средствами среды программирования Microsoft Visual Studio (версии 2008–2015). Традиционные методы визуальной разработки интерфейса, имевшиеся уже в технологии Windows Forms, были дополнены методами, основанными на непосредственном редактировании xaml-файлов, определяющих макет графического приложения. Подобное редактирование (как и редактирование программного кода) существенно упрощается благодаря контекстным подсказкам, автоматической проверке синтаксиса, средствам автозавершения и другим средствам, встроенным в редакторы кода и xaml-файлов.

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

При этом, однако, возникают две проблемы. Первая обусловлена сложностью системы WPF и обширностью ее средств, которые невозможно охватить в рамках одного курса. Вторая проблема связана с недостатком учебной литературы. Если ограничиться изданиями, переведенными на русский язык, то можно отметить лишь [7–9] (при включении в рассмотрение английских книг их общее количество увеличится лишь в 2–3 раза). Из этих изданий на роль учебника может отчасти претендовать только книга Чарльза Петцольда [7], написанная с большим методическим мастерством (что характерно для всех работ этого автора) и содержащая много примеров законченных программ. Две другие книги являются, скорее, справочниками, включающими небольшие фрагменты иллюстративного кода.

Предлагаемая книга представляет собой попытку решения обеих проблем. С одной стороны, она посвящена лишь основным возможностям технологии WPF, которые вполне можно освоить за семестровый курс, а с другой – излагает материал в «практической» форме, упрощающей его усвоение. Изложение построено в виде подробного описания ряда законченных программных проектов, каждый из которых посвящен одному из аспектов технологии WPF. Многочисленные комментарии содержат дополнительные сведения, связанные с изучаемыми возможностями. Во многих случаях приводится сравнение рассматриваемых средств WPF с аналогичными средствами библиотеки Windows Forms. В процессе разработки проектов авторы намеренно допускают типичные ошибки, характерные для начинающих разработчиков интерфейсов, подробно объясняют их причины и приводят способы исправления. Подобный вариант изложения учебного материала «на примерах» ранее с успехом применялся авторами при чтении ими курсов по разработке пользовательских интерфейсов с использованием библиотеки VCL системы программирования Borland Delphi [1, 2] и предшественницы WPF – библиотеки Windows Forms платформы .NET [3].

Учебник состоит из описаний 19 проектов. Его содержимое можно разбить на три части. В первой части рассматриваются базовые возможности библиотеки WPF: создание проекта для WPF-приложения, работа с xaml-файлами, использование группирующих компонентов, управление программой посредством обработчиков событий (проект EVENTS), приемы работы с окнами в WPF-приложении, организация взаимодействия между окнами, особенности диалоговых окон (проект WINDOWS), совместное использование обработчиков событий, события клавиатуры (проект CALC), таймеры в WPF-приложении (проект CLOCK), возможности полей ввода, организация проверки правильности введенных данных (проект TEXTBOXES), события мыши (проект MOUSE), механизм перетаскивания Drag & Drop (проект ZOO), работа с курсорами и иконками, создание ресурсов приложения, совместное использование средств WPF и Windows Forms (проект CURSORS).

Вторая часть содержит описание разработки одного большого проекта TEXTEDIT, разбитое на 5 этапов (версий проекта). В ней основное внимание уделяется особенностям тех стандартных интерфейсных элементов, без которых не обходится практически ни одно приложение: меню и различных видов его команд (версии 1 и 2), контекстных меню (версия 3), панели инструментов (версия 4), статусной панели (версия 5). Кроме того, в версии 1 подробно рассматриваются особенности организации работы с файлами (открытие, сохранение, контроль за сохранением внесенных изменений), а также описываются приемы работы с командами WPF; в версии 2 рассказывается о том, как создавать новые команды WPF; в версии 3 рассматриваются особенности реализации команд редактирования; а в версиях 4 и 5 особое внимание уделяется различным аспектам механизма привязки компонентов и, кроме того, описываются действия по созданию новых свойств зависимости.

Третья часть содержит проекты, связанные с дополнительными возможностями WPF – работа с цветами и кистями и использование компонентов TrackBar (проект COLORS), работа со списками и использование стилей при определении макета приложения (проект LISTBOXES), работа с флажками и наборами флажков (проект CHECKBOXES), работа с иерархическими списками, реализация дерева каталогов и списка файлов, использование компонентов GridSplitter и Image, применение реестра Windows для хранения настроек приложения (проект IMGVIEW), работа с табличными списками и использование заставок и градиентных кистей (проект TRIGFUNC), создание компонентов во время выполнения программы (проект HTOWERS).

Завершает книгу раздел с 48 учебными заданиями, разбитыми на 5 групп. Каждая группа содержит однотипные задания; первая группа включает задания на организацию взаимодействия между окнами приложения, вторая – на синхронизацию компонентов и совместное использование обработчиков событий, третья – на реализацию режима перетаскивания Drag & Drop, четвертая – на создание программ, управляемых таймером, пятая – на использование стандартных диалоговых окон и работу с реестром. Большое количество заданий позволяет легко формировать из них наборы индивидуальных заданий одинакового уровня сложности.

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

Предполагается, что читатель книги уже изучил базовый курс по программированию. Знание основ языка C#, а также языка XML, является желательным, но не обязательным, так как ознакомиться с конструкциями этих языков можно в процессе чтения книги. Тем не менее полезной может оказаться книга [5], освещающая практически все аспекты языка C# и стандартных библиотек платформы .NET, а также книга [4], содержащая описание основных типов стандартной библиотеки (в том числе классов, связанных с обработкой строк и файлов), объектной модели языка C# и технологии LINQ, в том числе интерфейса LINQ to XML.

В качестве среды программирования используется Microsoft Visual Studio 2015, однако все проекты можно реализовать и в более ранних версиях этой среды (начиная с версии 2008). Применение новых возможностей языка C#, появившихся в его версии 6.0 и доступных только в Visual Studio версий 2015 и выше, всегда особо отмечается и сопровождается альтернативными вариантами кода для предыдущих версий.

Соглашения по оформлению фрагментов программного кода и xaml-файлов приводятся в первом проекте EVENTS. В нем же описываются основные действия по созданию и редактированию проекта для WPF-приложения.

1. События: EVENTS

Рис. 1. Окно приложения EVENTS


1.1. Создание проекта для WPF-приложения

Для того чтобы создать проект в среде программирования Visual Studio, выполните команду File | New | Project (Ctrl+Shift+N), в появившемся окне New Project выберите в левой части вариант Visual C#, а в правой части – вариант WPF Application, в поле ввода Name укажите имя проекта (в нашем случае EVENTS), а в поле ввода Location укажите каталог, в котором будет создан каталог проекта. Желательно снять флажок Create directory for solution, чтобы не создавался промежуточный каталог для решения (каталог для решения удобно использовать в ситуации, когда решение содержит несколько проектов; в нашем случае решение всегда будет содержать единственный проект). После указания всех настроек нажмите кнопку «OK».

В результате будет создан каталог EVENTS, содержащий все файлы одноименного проекта, в том числе файл решения EVENTS.sln, файл проекта EVENTS.csproj, а также файлы для двух основных классов проекта, созданных автоматически: класса MainWindow, представляющего главное окно программы, и класса App, обеспечивающего запуск программы, в ходе которого создается и отображается на экране экземпляр главного окна.

Для каждого класса создаются два файла: с расширением xaml, котрый содержит часть определения класса в специальном формате, и с расширением cs (перед которым тоже содержится текст xaml), содержащий часть определения класса на языке C#. Файл с расширением xaml (xaml-файл) имеет формат XML (eXtensible Markup Language – расширяемый язык разметки). Аббревиатура XAML (произносится «зэмл» или «замл») означает, что используется специализированный вариант языка XML: eXtensible Application Markup Language – расширяемый язык разметки для приложений.

Приведем содержимое файлов, связанных с классом App и созданных в Visual Studio 2015.

App.xaml:



App.xaml.cs:



Анализ этих файлов показывает, что класс App наследуется от стандартного класса Application, а также что в cs-файле никакой новой функциональности в класс App не добавляется (обратите внимание на то, что при определении класса App в cs-файле указывается модификатор partial, означающий, что часть определения этого класса содержится в другом файле). Созданный класс (как и другие классы проекта, создаваемые автоматически) связывается с пространством имен EVENTS, совпадающим с именем проекта.

Перед анализом содержимого xaml-файла следует предварительно описать основные правила, по которым формируется любой XML-файл. Подобные файлы состоят из иерархического набора вложенных друг в друга именованных XML-элементов, причем каждый элемент может иметь любое количество XML-атрибутов и дочерних элементов. Элементы оформляются в виде тегов; открывающий тег элемента имеет вид <имя_элемента список_атрибутов>, а закрывающий тег – </имя_элемента>. Между этими тегами располагается содержимое элемента, которое может представлять собой обычный текст и/или другие (дочерние) элементы (а также другие XML-узлы, которые мы не будем обсуждать, так как в xaml-файле они не используются). Число уровней вложенности элементов может быть любым. Если элемент не имеет содержимого, то он может представляться в виде одного комбинированного тега вида <имя_элемента список_атрибутов />. Атрибуты в списке определяются следующим образом: имя_атрибута="значение_атрибута"; значение обязательно заключается в кавычки (одинарные или двойные). Все атрибуты одного элемента должны иметь различные имена, в то время как его дочерние элементы могут иметь совпадающие имена. Регистр в именах учитывается; имена как атрибутов, так и элементов могут содержать только буквы, цифры, символы «.» (точка), «-» (дефис) и «_» (подчеркивание) и начинаться либо с буквы, либо с символа подчеркивания. Пробелы в именах не допускаются. Перед именами элементов и атрибутов могут указываться префиксы пространств имен, отделяемые от собственно имени двоеточием (в файле App.xaml имеются два таких атрибута: xmlns:x и xmlns:local). Любой XML-файл должен содержать единственный XML-элемент верхнего уровня, называемый корневым элементом (в файле App.xaml это элемент Application).

В той части определения класса App, которая размещается в xaml-файле, содержится единственная, но очень важная настройка – указание на класс, экземпляр которого будет создан при запуске программы. Это атрибут StartupUri элемента Application, его значение равно MainWindow.xaml. Фактически данный атрибут является свойством класса Application. Как и другие свойства, его можно настроить либо непосредственно в тексте xaml-файла, либо в окне свойств Properties, которое отображает доступные для редактирования свойства текущего объекта из xaml-файла (если в редакторе отображается не xaml-, а cs-файл, то окно Properties является пустым).

При указании или изменении свойств в xaml-файле очень помогает предусмотренная в редакторе xaml-файлов возможность контекстной подсказки при выборе значений свойств. Окно Properties удобно в том отношении, что позволяет просмотреть все доступные свойства текущего объекта. В xaml-файле отображаются только те свойства, значения которых отличаются от значений по умолчанию для данного объекта. Чтобы добавить в xaml-файл новое свойство, достаточно в окне Properties указать для данного свойства значение, отличное от значения по умолчанию.

В процессе компиляции программы все xaml-файлы конвертируются в специальный двоичный формат и затем обрабатываются совместно с cs-файлами проекта.

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

Комментарий

На протяжении всей книги мы будем придерживаться следующих соглашений об отступах в текстах xaml- и cs-файлов. В xaml-файле каждый вложенный элемент набирается с отступом в 2 пробела относительно родительского элемента (причина столь небольшого отступа заключается в том, что глубина вложенности элементов в xaml-файлах может быть достаточно большой); если список атрибутов в открывающем теге элемента не умещается в одной строке, то он переносится на следующую строку с отступом в 4 пробела относительно начала открывающего тега. Для cs-файла ситуация обратная: вложенные конструкции набираются с отступом в 4 пробела (как в редакторе кода среды Visual Studio), а при переносе длинного оператора на новую строку используется отступ в 2 пробела.

Приведем файлы, связанные с классом MainWindow; именно эти файлы автоматически загружаются в редактор после создания (или открытия) проекта.

MainWindow.xaml:



MainWindow.xaml.cs:




В дальнейшем при выводе текста xaml-файлов мы не будем указывать атрибуты корневого элемента, предшествующие атрибуту Title, поскольку они генерируются автоматически и не требуют изменения.

Одновременно с отображением xaml-файла для класса окна на экране выводится окно дизайнера – визуального редактора. Следует заметить, что при разработке WPF-приложений визуальный редактор используется не так активно, как при разработке приложений, использующих библиотеку Windows Forms. Это связано с тем, что относительное расположение компонентов в окне WPF-приложения обычно не определяется явным образом, с указанием абсолютных оконных координат, а вычисляется по специальным правилам, связанным с особенностями тех или иных группирующих компонентов (называемых также панелями). Таким образом, окно дизайнера используется преимущественно для того, чтобы быстро определить, как те или иные изменения, внесенные в xaml-файл или сделанные с помощью окна свойств, повлияют на внешний вид окна. С помощью выпадающего списка можно настраивать масштаб для окна дизайнера.

Обратите внимание на то, что по умолчанию в окно приложения уже включен группирующий компонент Grid – наиболее универсальный из группирующих компонентов, позволяющий размещать свои дочерние компоненты в нескольких строках и столбцах. Кроме того, для класса MainWindow определены три свойства: Title, Height и Width. Гораздо большее число свойств приведено в окне Properties (как уже было отмечено выше, их отсутствие в xaml-файле объясняется тем, что данный файл содержит только свойства, значения которых отличаются от значений по умолчанию). При просмотре списка свойств в окне Properties можно использовать либо режим, при котором «родственные» свойства объединяются в группы, либо режим, при котором свойства располагаются в алфавитном порядке. Кроме того, с помощью поля ввода, расположенного над списком свойств, можно выполнять фильтрацию этого списка, отображая только те свойства, в именах которых содержится указанная строка. Например, после ввода в это поле текста Height в списке свойств останутся лишь три свойства: Height, MaxHeight и MinHeight.

В файле MainWindow.xaml.cs содержится частичное определение класса MainWindow, включающее конструктор без параметров, в котором вызывается метод InitializeComponent, обеспечивающий начальную инициализацию всех компонентов окна. Все действия с компонентами можно выполнять только после их начальной инициализации, поэтому пользовательский код добавляется в конструктор после вызова данного метода.

Комментарий

Большинство XML-атрибутов в xaml-файле относятся либо к атрибутам свойств (и определяют соответствующие свойства объектов), либо к атрибутам событий (и позволяют связать события с методами-обработчиками). XML-элементы тоже можно разбить на две категории: это элементы-объекты, имена которых совпадают с типом определяемого объекта, и элементы-свойства, имеющие составные имена вида тип.свойство (элементы-свойства используются в ситуации, когда свойство нельзя определить с помощью единственного атрибута).

Кроме того, для каждого типа компонентов WPF определено особое свойство, которое можно задать в xaml-файле, указав его в виде одного или нескольких дочерних элементов-объектов (примером такого свойства является свойство Content; в частности, в приведенном выше файле MainWindow.xaml свойство Content окна Window равно компоненту Grid). В подобной ситуации имя свойства вообще не указывается. Несколько дочерних элементов-объектов можно указывать, если определяемое свойство является свойством-коллекцией (примером такого свойства является свойство Children, имеющееся у всех группирующих компонентов-панелей, например, компонента Grid или используемого в следующем пункте компонента Canvas).

1.2. Добавление компонентов и настройка их свойств

Разрабатываемое нами приложение отличается от традиционных WPF-приложений тем, что мы хотим произвольным образом перемещать отдельные компоненты в пределах окна. В подобной ситуации вместо группирующего компонента Grid удобнее пользоваться компонентом Canvas. Поэтому нам необходимо изменить «внешний» компонент окна и, кроме того, добавить на новый внешний компонент кнопку Button.

Эти действия можно выполнить двумя способами: с помощью окна дизайнера, удалив в нем лишние компоненты и добавив новые путем их перетаскивания с панели компонентов Toolbox, и с помощью непосредственного редактирования xaml-файла.

Опишем первый способ.

Вначале необходимо выделить в окне дизайнера компонент Grid, щелкнув на нем мышью. То, что выделен именно компонент Grid, можно проверить по тексту xaml-файла (в котором также будет выделен элемент <Grid>) или по окну Properties (где указываются свойства выделенного компонента). После выделения компонента его надо удалить, нажав клавишу Delete. Обратите внимание на то, что в результате такого удаления элемент Window в xaml-файле будет представлен в виде комбинированного тега <Window … />, поскольку теперь он не содержит дочерних элементов.

Затем необходимо добавить в окно компонент Canvas. Для этого надо развернуть панель Toolbox, которая обычно располагается у левой границы окна Visual Studio в свернутом состоянии. Если данная панель отсутствует, то ее можно отобразить с помощью команды меню View | Toolbox (Ctrl+W, X). Для быстрого поиска нужного компонента на панели Toolbox достаточно ввести начальную часть его имени в поле ввода, расположенное в верхней части панели. Например, в нашем случае достаточно ввести текст Can, чтобы на панели отобразился единственный компонент Canvas. Можно обойтись и без быстрого поиска, просто выбрав данный компонент в списке All WPF Controls. После выбора компонента Canvas достаточно перетащить его в окно дизайнера. В результате компонент Canvas появится в окне и соответствующий текст будет добавлен в xaml-файл (при этом будет восстановлено представление элемента Window в xaml-файле в виде двух тегов – открывающего <Window> и закрывающего </Window>):



Здесь и в дальнейшем мы часто будем опускать фрагменты xaml-файла, оставшиеся неизменными, указывая вместо них символ многоточия «…». Измененную часть xaml-файла мы выделили полужирным шрифтом.

Примерный вид окна дизайнера приведен на рис. 2.


Рис. 2. Окно дизайнера после добавления компонента Canvas


Разумеется, нам не требуется такое размещение компонента Canvas. Необходимо, чтобы он занимал всю клиентскую область окна. Для того чтобы добиться этого, достаточно просто удалить в xaml-файле все атрибуты элемента Canvas (удаляемые фрагменты будем изображать перечеркнутыми):



Новый вид окна дизайнера приведен на рис. 3.


Рис. 3. Окно дизайнера после удаления атрибутов компонента Canvas


Как правило, после добавления в окно какого-либо компонента путем его перетаскивания из панели Toolbox, всегда требуется выполнить действия, связанные с удалением «лишних» атрибутов.

Теперь добавим на компонент Canvas кнопку Button, зацепив ее мышью на панели Toolbox и перетащив в окно. После появления кнопки в окне следует перетащить ее в левый верхний угол окна (при подобном перетаскивании кнопка будет автоматически «притянута» к области, расположенной на расстоянии 10 единиц от левой и верхней границы клиентской области окна, – рис. 4).


Рис. 4. Окно дизайнера после добавления компонента Button


Содержимое xaml-файла изменится следующим образом:



Мы видим, что теперь элемент Canvas тоже оформляется в виде парных тегов, так как он содержит дочерний элемент – кнопку.

Обсудим атрибуты, автоматически добавленные к элементу Button. Атрибут с именем x:Name определяет имя, с помощью которого можно обращаться к данному компоненту в cs-файле. Это имя будет являться одним из свойств класса MainWindow. Обратите внимание на то, что элемент Canvas аналогичного имени не содержит. Это означает, что в классе MainWindow мы не сможем обращаться по имени к компоненту Canvas. Если это является неудобным, то всегда можно определить имя (или с помощью окна свойств, в котором свойство Name указывается первым, или непосредственно в xaml-файле).

Свойство Content определяет содержимое кнопки. В качестве значения свойства Content может указываться не только строка, но и любой компонент. Более того, на кнопку можно поместить группирующий компонент, в котором, в свою очередь, можно разместить любое количество других компонентов. Это позволяет создавать в WPF-приложении сложные интерфейсные элементы, конструируя их из базовых. Например, можно создать кнопку, содержащую не только текст, но и изображение (в дальнейшем мы воспользуемся этой возможностью – см., например, проект ZOO, п. 7.7).

Следует также обратить внимание на то, что для кнопки не указано свойство Height (хотя свойство Width имеется). Если свойство Height отсутствует, то высота компонента определяется по размерам его содержимого, что в большинстве случаев является оптимальным. Можно было бы удалить и свойство Width, тогда все размеры кнопки будут подстроены под ее содержимое, однако обычно свойство Width указывается, поскольку желательно, чтобы все кнопки в приложении имели одинаковую ширину.

В отличие от компонентов из библиотеки Windows Forms, компоненты библиотеки WPF не имеют свойств Top и Left, определяющих позицию, в которой они размещаются. Это связано с тем, что явное указание позиции компонентов в окне WPF обычно не требуется (положение компонентов определяется другими их свойствами, а также свойствами содержащих их группирующих компонентов). Однако для любого компонента можно задать свойства Top и Left, «полученные» от класса Canvas. Если данный компонент будет размещен на одном из компонентов типа Canvas, то эти полученные свойства будут учтены при определении его позиции. Возможность подобной «передачи» свойств от одного компонента к другому является одним из аспектов особого механизма, реализованного в WPF и связанного с так называемыми свойствами зависимости (dependency properties). Почти все свойства компонентов WPF являются свойствами зависимости, что позволяет их использовать при реализации различных возможностей, доступных в WPF, например, для привязки свойств или определения стилей. Частным случаем свойств зависимости являются присоединенные свойства (attached properties), которые, будучи определенными в одном классе, могут использоваться в другом. Свойства Top и Left компонента Canvas – типичный пример присоединенных свойств.

Присоединенные свойства панели Canvas особенно просто указывать в xaml-файле. Однако к ним можно обращаться и в программном коде, что будет продемонстрировано далее.

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



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

Отредактируем полученный xaml-файл: изменим заголовок окна на текст «Прыгающие кнопки», надпись на кнопке – на «Закрыть», ее имя – на button1 (поскольку в дальнейшем мы добавим к окну еще одну кнопку). Кроме того, укажем для окна свойство WindowStartupLocation, положив его равным CenterScreen (это значение обеспечивает автоматическое центрирование окна программы при ее запуске):



Хотя свойства можно настраивать с помощью окна Properties, обычно бывает удобнее это делать непосредственно в xaml-файле. Более того, даже добавлять новые свойства в xaml-файл не составляет труда, так как при вводе уже нескольких начальных символов свойства появляется список всех свойств, начинающихся с этих символов, что позволяет быстро завершить ввод имени, нажав клавишу Tab, после чего в xaml-файл будет не только добавлено полное имя свойства, но и вставлены символы ="", а если свойство принимает фиксированный набор значений, то сразу отобразится список этих значений, из которых можно выбрать требуемый (мы могли это заметить, определяя свойство WindowStartupLocation).

В дальнейшем при описании действий, которые требуется выполнить для добавления в окно новых компонентов или изменения их свойств, мы будем просто указывать новое содержимое xaml-файла, выделяя в нем полужирным шрифтом новые или измененные фрагменты. Иногда (достаточно редко) мы будем также дополнительно помечать фрагменты, которые требуется удалить, оформляя их в виде перечеркнутого текста. Аналогичные способы выделения будем использовать и для фрагментов программного кода на языке C#.

Комментарий

При редактировании xaml-файла оказываются удобными две возможности, связанные с автоматическим добавлением или удалением закрывающих тегов.

(1) Если ввести новый открывающий тег, включая все его атрибуты, то после ввода закрывающей угловой скобки к открывающему тегу будет добавлен закрывающий, а курсор разместится между тегами.

Пример. Предположим, что был введен следующий текст (позиция курсора помечена символом |):

Если теперь ввести символ «>», то текст изменится следующим образом (символ | по-прежнему указывает на позицию курсора):

(2) Если перед закрывающей угловой скобкой открывающего тега ввести символ «/» (превратив этим действием тег в комбинированный), то соответствующий закрывающий тег будет удален из xaml-файла (при этом все дочерние элементы преобразованного элемента, если они имеются, станут элементами того же уровня, что и преобразованный элемент).

Пример. Если в тексте, полученном в предыдущем примере, перевести курсор на одну позицию влево

и ввести символ «/», то текст изменится следующим образом:


Результат. После запуска программы (для которого достаточно нажать клавишу F5) в центре экрана появится ее окно с кнопкой «Закрыть» (рис. 5).


Рис. 5. Окно приложения EVENTS (первый вариант)


Нажатие на кнопку пока не приводит ни к каким действиям, однако уже сейчас для пользователя доступны все стандартные действия, связанные с управлением окном (сворачиванием, разворачиванием, закрытием, изменением размеров и положения).

Комментарий

При запуске WPF-приложения из среды Visual Studio в режиме Debug поверх окна отображается черная панель с дополнительными средствами отладки (рис. 6).

Рис. 6. Панель с дополнительными отладочными средствами XAML

Поскольку мы не будем использовать эти средства, имеет смысл скрыть панель. Для этого следует выполнить команду меню Tools | Options, в появившемся диалоговом окне Options выбрать раздел Debugging и в этом разделе снять флажок Enable UI Debugging Tools for XAML.

1.3. Связывание события с обработчиком

Теперь мы хотим связать определенное действие с нажатием кнопки button1. Для этого можно выполнить следующие шаги:

1) выделите в окне дизайнера кнопку button1;

2) в окне Properties перейдите к разделу со списком событий, нажав на кнопку с изображением молнии: ;

3) выберите в разделе со списком событий строку Click и выполните на ее пустом поле ввода двойной щелчок мышью;

4) в результате активизируется вкладка редактора с файлом MainWindow.xaml.cs, где появится заготовка для нового метода класса MainWindow – обработчик события Click для компонента button1:


5) в эту заготовку надо ввести код, который будет выполняться при нажатии кнопки button1; мы добавим в нее единственный оператор:



Заметим, что соответствующее изменение будет внесено и в xaml-файл:



Именно благодаря заданию атрибута Click в xaml-файле метод button1_Click будет связан с событием Click компонента button1 (при отсутствии такого атрибута метод button1_Click будет считаться обычным методом класса, для выполнения которого требуется его явный вызов).

Описанный выше способ создания нового обработчика события был реализован еще для библиотеки Windows Forms. Однако в WPF-проекте имеется более быстрый способ определения нового обработчика, не требующий использования окна Properties. Необходимо ввести имя события как атрибут соответствующего элемента в xaml-файле (в нашем случае в элемент Button надо ввести текст «Click=», причем достаточно набрать несколько начальных символов имени события и воспользоваться для завершения набора выпадающим списком) и после появления рядом с набранным атрибутом выпадающего списка с текстом «New Event Handler» выбрать этот текст (если он еще не выбран) и нажать клавишу Enter. При этом в xaml-файл будет добавлено имя обработчика (в нашем случае button1_Click), а в cs-файле будет создана заготовка для обработчика с этим именем, хотя перехода к ней не произойдет, чтобы дать возможность продолжить редактирование xaml-файла. Если в программе уже имеются обработчики, совместимые с тем событием, имя которого введено в xaml-файле, то в выпадающем списке наряду с вариантом «New Event Handler» будут приведены и имена всех таких обработчиков, что позволит быстро связать события для нескольких компонентов с одним обработчиком (хотя для подобного связывания имеется более удобная возможность, основанная на механизме маршрутизируемых событий и описанная в проекте CALC).

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

В дальнейшем вместо детального описания действий по созданию обработчиков событий мы будем просто приводить измененный фрагмент xaml-файла с новыми атрибутами и текст самого обработчика, выделяя добавленные (или измененные) фрагменты полужирным шрифтом:




Текст button1_Click мы не только выделяем полужирным шрифтом, но и подчеркиваем, чтобы отметить то обстоятельство, что этот текст будет автоматически сгенерирован редактором xaml-файлов после ввода текста Click= и выбора из появившегося списка варианта «New Event Handler» (напомним, что при этом в cs-файле будет создан новый обработчик с указанным именем).

Добавим к нашему проекту еще один обработчик – на этот раз для компонента Canvas (обратите внимание на то, что если обработчик создается для компонента, не имеющего имени, то в имени обработчика по умолчанию используется имя класса этого компонента):




Кроме того, необходимо задать фон для компонента Canvas:



Результат. Теперь после запуска программы при щелчке на любом месте окна кнопка «Закрыть» услужливо прыгает на указанное место. Нажатие на кнопку «Закрыть» приводит к завершению программы и возврату в среду Visual Studio.

Комментарии

1. Любой компонент WPF реагирует на нажатие мыши только в том случае, если имеет непустой фон. По умолчанию фон некоторых компонентов является пустым (в окне Properties в этом случае свойство Background имеет значение No Brush или вообще не содержит никакого текста). Следует заметить, что, несмотря на вид атрибута Background в xaml-файле, фон определяется не цветом, а особым классом WPF – кистью (имеется абстрактный класс Brush, от которого порождается несколько классов-потомков). Указание цветовой константы означает, что будет использоваться сплошная кисть типа SolidBrush с заливкой данного цвета. В последних проектах, описанных в данной книге (TRIGFUNC и HTOWERS), мы познакомимся с градиентной кистью, имеющей две разновидности – LinearGradientBrush и RadialGradientBrush.

2. Важной характеристикой любого события, связанного с мышью, является позиция мыши. Для определения этой позиции в обработчиках мыши предусмотрен специальный метод, вызываемый для второго параметра обработчика e: e.GetPosition. Данный метод имеет обязательный параметр, задающий компонент, относительно которого определяется позиция мыши. Мы указали параметр this; это означает, что позиция будет определяться относительно левого верхнего угла клиентской области окна. Заметим, что все размеры в WPF задаются в так называемых аппаратно-независимых единицах (одна единица равна 1/96 дюйма) и представляются в виде вещественных чисел типа double (в то время как в библиотеке Windows Forms размеры задавались в экранно-зависимых пикселах и представлялись целыми числами).

3. Два последних оператора в обработчике Canvas_MouseDown демонстрируют способ, позволяющий задать в программе присоединенные свойства Left и Top, полученные компонентом от его родителя типа Canvas. Обратите внимание на то, что методы SetLeft и SetTop являются статическими и должны вызываться не для конкретного объекта типа Canvas, а для самого класса. Имеются парные методы Canvas.GetLeft(c) и Canvas.GetTop(c), позволяющие определить текущие значения свойств Left и Top для компонента c, расположенного на компоненте Canvas (эти методы будут использованы во фрагменте программы, добавленном в п. 1.5 при исправлении недочета).

Для задания присоединенных свойств можно также использовать «универсальный» метод SetValue, имеющийся у всех компонентов. Например, два последних оператора в обработчике Canvas_MouseDown можно изменить следующим образом:

Обратите внимание на то, что при указании присоединенного свойства в методе SetValue надо использовать его имя с суффиксом Property (на самом деле это и есть «настоящее» имя статического присоединенного свойства, поскольку подобный суффикс имеют все статические свойства зависимости). Интересно отметить, что с помощью метода SetValue с компонентом можно связывать любые свойства зависимости, определенные у любых типов компонентов (для получения значений этих свойств предназначен метод GetValue, пример использования которого приводится в последнем комментарии к п. 1.5).

4. Для определения текущих размеров компонента в программе надо обращаться к свойствам ActualWidth и ActualHeight. Свойства Width и Height для этого использовать нельзя, так как они обычно содержат лишь «рекомендованные» значения размеров, которые учитываются группирующими компонентами при компоновке своих дочерних компонентов (в частности, возможны рекомендованные значения «бесконечность» или NaN). В нашем случае свойства ActualWidth и ActualHeight кнопки используются для того, чтобы отцентрировать кнопку относительно курсора мыши.

1.4. Отсоединение обработчика от события

В начало описания класса MainWindow (перед конструктором public MainWindow()) добавьте новое поле:



В окно добавьте новую кнопку button2, сделайте ее свойство Content пустой строкой и определите для этой кнопки два обработчика:





Результат. «Дикая» кнопка с пустым заголовком не дает на себя нажать, «убегая» от курсора мыши. Для того чтобы ее «приручить», надо переместить на нее курсор, держа нажатой клавишу Ctrl. После щелчка на дикой кнопке она приручается: на ней появляется заголовок «Изменить», и она перестает убегать от курсора мыши. Следует заметить, что приручить кнопку можно и с помощью клавиатуры, переместив на нее фокус с помощью клавиш со стрелками (или клавиши Tab) и нажав на клавишу пробела.

Недочет. Если попытаться «приручить» кнопку, переместив на нее фокус и нажав клавишу пробела, то перед приручением она прыгает по окну, пока не будет отпущена клавиша пробела. Причины такого поведения непонятны, поскольку нажатие клавиши пробела не должно приводить к активизации события, связанного с перемещением мыши. Следует, однако, отметить, что нажатие пробела обрабатывается в WPF особым образом, и по этой причине оно может приводить к таким странным эффектам.

Исправление. Дополните условие в методе button2_MouseMove:




Прирученная кнопка пока ничего не делает. Это будет исправлено в следующем пункте.

Комментарии

1. В данном пункте демонстрируется возможность отсоединения метода-обработчика от события, с которым он ранее был связан. Для этого используется операция –=, слева от которой указывается событие, а справа – тот обработчик, который надо отсоединить от события.

2. В обработчике button2_MouseMove определяются текущие размеры компонента Canvas, чтобы обеспечить случайное перемещение дикой кнопки только в пределах этого компонента (метод r.NextDouble() возвращает случайное вещественное число в полуинтервале [0; 1), при этом вычитание числа 5 гарантирует, что дикая кнопка будет видна на экране хотя бы частично). Заметим, что программа правильно реагирует на изменение размера окна: дикая кнопка всегда перемещается в пределах его текущего размера. Это обеспечивается благодаря тому, что панель Canvas по умолчанию занимает всю клиентскую область своего родителя-окна.

Поскольку мы не присвоили компоненту Canvas имя, нам пришлось обращаться к нему через его родителя, вызвав для окна его свойство Content и, кроме того, выполнив явное приведение типа с помощью операции as. Вместо приведения к типу Canvas можно было бы выполнить приведение к типу FrameworkElement – первому типу в иерархии наследования компонентов, в котором определены свойства, связанные с размерами. Можно было выполнить приведение к типу Panel – непосредственному потомку FrameworkElement, который является предком всех группирующих компонентов. Заметим, что выполнить приведение класса Canvas к типу Control не удастся, так как группирующие компоненты к данному типу не относятся (потомками Control являются, в частности, компоненты, имеющие свойство Content, например кнопки).

3. Следует обратить внимание на способ, с помощью которого в обработчике button2_MouseMove проверяется, нажата ли клавиша Ctrl. Обычно дополнительная информация о произошедшем событии передается в обработчик с помощью второго параметра e. Например, в обработчике button2_MouseMove с помощью данного параметра (типа MouseEventArgs) можно определить текущую позицию мыши (вызвав метод e.GetPosition) или состояние кнопок мыши (вызвав, например, свойство e.LeftButton и сравнив его с одним из его возможных значений – MouseButtonState.Pressed или MouseButtonState.Released). Однако информацию о нажатых в данный момент клавишах параметр e типа MouseEventArgs не содержит. Тем не менее подобную информацию можно получить с помощью статического свойства IsKeyDown класса Keyboard. Это же свойство мы использовали при исправлении обнаруженного недочета, чтобы в случае, если нажата клавиша пробела, обработчик button2_MouseMove не выполнял никаких действий.

4. Обратите также внимание на то, что поле r в классе WainWinbdow не только описывается, но и сразу инициализируется (с помощью конструктора Random() без параметров). В какой момент выполняется данная инициализация? По правилам языка C#, явно указанные операторы инициализации всех полей класса автоматически помещаются в начало любого конструктора данного класса. Таким образом, поле r будет инициализировано в начале выполнения конструктора окна (перед выполнением оператора InitializeComponent(), указанного в теле данного конструктора). Разумеется, можно было бы поступить иначе: описать поле r типа Random без его инициализации

и поместить в конструктор окна оператор

1.5. Присоединение к событию другого обработчика

Для того чтобы прирученная кнопка при нажатии на нее выполняла какие-либо действия, можно добавить эти действия к уже имеющемуся обработчику button2_Click. Однако в этом случае обработчик должен проверять, в каком состоянии находится кнопка – диком или прирученном. Поступим по-другому: свяжем событие Click для прирученной кнопки с новым обработчиком. Такой подход позволит продемонстрировать в нашем проекте ряд особенностей, связанных с действиями по присоединению и отсоединению обработчиков.

Новый обработчик (назовем его button2_Click2) создадим «вручную», не прибегая к услугам окна Properties или xaml-файла. Для этого в конце описания класса MainWindow в файле MainWindow.xaml.cs (перед двумя последними скобками «}») добавим описание этого обработчика:



Чтобы подчеркнуть, что в данном случае никакая часть обработчика не создается автоматически, мы выделили весь текст обработчика полужирным шрифтом.

В метод button2_Click добавьте следующие операторы (здесь и далее в книге предполагается, что если место добавления не уточняется, то операторы надо добавлять в конец метода):



В метод Canvas_MouseDown добавьте операторы:



Результат. Прирученная кнопка теперь выполняет полезную работу – щелчок на ней приводит к разворачиванию окна программы на весь экран, а новый щелчок восстанавливает первоначальное состояние окна. Если же щелкнуть мышью на окне (не на кнопке), то услужливая кнопка «Закрыть» прибежит на вызов, а прирученная кнопка «Изменить» снова одичает, потеряет текст своего заголовка и начнет убегать от мыши.

Комментарий

Приведенные тексты методов показывают, что при смене обработчика недостаточно присоединить к событию новый обработчик; необходимо также отсоединить от события обработчик, ранее связанный с ним. Данное обстоятельство обусловлено тем важным фактом, что к одному и тому же событию можно последовательно присоединить несколько обработчиков (для этого достаточно применить к этому событию несколько раз оператор +=). Следует отметить, что данная возможность для событий визуальных компонентов применяется крайне редко (достаточно отметить, что с помощью окна Properties или xaml-файла присоединить к одному событию несколько обработчиков нельзя). В то же время при явном присоединении обработчиков эта особенность может приводить к появлению трудно выявляемых ошибок, если, например, один и тот же обработчик будет присоединен к событию несколько раз. Подобные проблемы можно проиллюстрировать с помощью нашей программы, если закомментировать заголовок оператора if в обработчике Canvas_MouseDown:

Если теперь после запуска программы несколько раз щелкнуть мышью на окне, а затем приручить кнопку button2, то при ее последующем нажатии окно несколько раз последовательно перейдет из развернутого состояния в стандартное и обратно. Это объясняется тем, что теперь каждый щелчок на окне присоединяет к событию Click кнопки button2 новый экземпляр обработчика button2_Click, и при нажатии на эту кнопку каждый экземпляр обработчика последовательно запускается на выполнение. Ситуация осложняется еще тем обстоятельством, что в программе невозможно выяснить, сколько и какие обработчики присоединены в настоящий момент к данному событию (а не зная этого, нельзя и обеспечить отсоединение от события всех его обработчиков).

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

Недочет. При выполнении программы может возникнуть ситуация, когда одна или обе кнопки не будут отображаться в окне (если, например, кнопки были перемещены на новое место при развернутом окне, после чего окно возвращено в исходное состояние).

Исправление. Определите для окна обработчик события SizeChanged:




Результат. Теперь в ситуации, когда при изменении размера окна его кнопки оказываются вне клиентской части, происходит перемещение этих кнопок на исходные позиции около левого верхнего угла окна.

Комментарии

1. В данном обработчике демонстрируется еще один способ доступа к компонентам окна, который удобен для организации перебора в цикле компонентов с похожими именами. Этот способ основан на применении метода FindName, который можно вызывать непосредственно для окна. Метод FindName возвращает компонент окна с указанным именем (или null, если компонент с таким именем в окне отсутствует).

2. Вместо статического метода GetLeft для получения значения присоединенного свойства Left можно было бы использовать более длинный, но и более универсальный вариант, использующий метод GetValue того компонента, к которому ранее было присоединено свойство: (double)b.GetValue(Canvas.LeftProperty). Аналогичным образом можно получить значение свойства Top (и любых других свойств зависимости, присоединенных к данному компоненту).

2. Работа с несколькими окнами: WINDOWS

Рис. 7. Окна приложения WINDOWS


2.1. Настройка визуальных свойств окон. Открытие окон в обычном и диалоговом режиме

После создания проекта к нему необходимо добавить два дополнительных окна. Для этого требуется выполнить команду Project | Add Window… и в появившемся диалоговом окне указать имя класса, который будет связан с новым окном. Достаточно использовать имена, предлагаемые по умолчанию – Window1 для первого окна, Window2 для второго.


Рис. 8. Макет окна MainWindow приложения WINDOWS


MainWindow.xaml (рис. 8):




Window1.xaml:



Window2.xaml:



В файле MainWindow.xaml.cs в начало описания класса MainWindow добавьте операторы:



Определите обработчики для класса MainWindow (эти обработчики указаны в файле MainWindow.xaml, и поэтому их заготовки уже должны содержаться в классе MainWindow; напомним, что для большей наглядности мы подчеркиваем в xaml-файле имена подобных обработчиков):




Результат. Программа включает три окна, демонстрирующие основные типы окон в графических Windows-приложениях: окно фиксированного размера (MainWindow), окно переменного размера (win1 типа Window1), диалоговое окно (win2 типа Window2). Главное окно MainWindow сразу отображается на экране при запуске приложения. Окна win1 и win2 (подчиненные окна) вызываются из главного окна нажатием соответствующей кнопки. При этом окно win1 отображается в обычном, а окно win2 – в модальном (диалоговом) режиме (если некоторое окно в приложении находится в диалоговом режиме, то до его закрытия нельзя переключаться на другие окна). Для завершения программы надо закрыть ее главное окно. При отображении главного окна место для его размещения выбирается операционной системой, окно win1 отображается около правого нижнего угла главного окна с небольшим наложением, окно win2 отображается в центре экрана.

Следует заметить, что полученная программа содержит серьезную ошибку, которая будет исправлена в следующем пункте.

Комментарии

1. Благодаря явному заданию значения false для свойств ShowInTaskbar подчиненных окон, кнопки для этих окон не отображаются на панели задач в нижней части экрана.

2. За возможность изменения размеров окна и отображение кнопок минимизации/максимизации на его заголовке отвечает свойство ResizeMode, которое может принимать следующие значения: NoResize (размер окна фиксирован, кнопки не отображаются), CanMinimize (размер окна фиксирован, доступна кнопка минимизации), CanResize (значение по умолчанию: окно может менять размер, доступны обе кнопки), CanResizeWithGrip (то же, что и CanResize, но в правом нижнем углу окна дополнительно отображается треугольный маркер; благодаря этому маркеру увеличивается область, которую можно зацепить мышью для изменения размеров окна). Для диалоговых окон дополнительно следует установить свойство WindowStyle равным ToolWindow; это обеспечивает скрытие иконки на заголовке окна (отображать иконки в диалоговых окнах не принято).

3. Присваивание свойству Owner некоторого окна w1, значения какого-либо другого окна w0 делает окно w1 подчиненным по отношению к главному окну w0. Подчиненное окно всегда отображается поверх главного (даже если главное окно является активным). Кроме того, при минимизации или закрытии главного окна его подчиненные окна также минимизируются (или, соответственно, закрываются). Следует заметить, что свойству Owner можно присвоить значение только такого окна, которое уже отображено на экране, поэтому указанные действия мы выполняем в обработчике события Loaded, которое возникает при первом отображении окна.

4. За начальное расположение окна на экране отвечает свойство WindowStartupLocation, равное по умолчанию значению Manual. При этом позицию окна можно задать явно с помощью свойств Left и Top или не задавать эти свойства, оставив определение начальной позиции на усмотрение операционной системы. В последнем случае «истинные» значения свойств Left и Top будут доступны только в момент первого отображения окна на экране. Как уже было отмечено в предыдущем комментарии, с этой ситуацией связано событие окна Loaded, поэтому начальное положение окна win1 определяется нами в обработчике данного события для главного окна.

5. Содержимое файла MainWindow.xaml демонстрирует традиционный для технологии WPF динамический способ размещения компонентов в окне, при котором их размеры и положение (а также иногда и размеры окна) определяются автоматически на основе указанных настроек. В данном случае в окне надо разместить две кнопки по вертикали. Такой способ размещения проще всего обеспечить с помощью группирующего компонента StackPanel (данный компонент имеет свойство Orientation с вариантами значений Vertical и Horizontal, причем первый вариант является значением по умолчанию).

Для указания полей – промежутков между компонентами – используется свойство Margin, которое может состоять из 1, 2 или 4 значений. Единственное значение определяет одинаковое поле (в аппаратно-независимых единицах, равных 1/96 дюйма) во всех направлениях, при наличии двух значений первое определяет поле слева и справа, а второе – сверху и снизу, при наличии четырех значений поля определяются в следующем порядке: левое, верхнее, правое, нижнее. Обратите внимание на то, что для того, чтобы обеспечить одинаковые промежутки (равные 10 единицам) как между компонентами, так и между компонентом и границей окна, следует задать поля, равные 5, как для группирующего (невидимого) компонента, так и для содержащихся в нем видимых компонентов-кнопок.

Помимо «внешних полей» (margins) для компонентов можно задавать «внутренние поля» (paddings), определяющие расстояние от границы компонента до его содержимого. Внутренние поля определяются свойством Padding, которое задается по тем же правилам, что и свойство Margin.

Следует также обратить внимание на значение свойства MinWidth, которое задано только для первой кнопки. Оно определяет минимальную ширину данного компонента и тем самым минимальную ширину всей панели StackPanel, причем все остальные компоненты на этой панели будут иметь такую же ширину. Таким образом, реальные размеры как кнопок, так и панели будут определяться размером шрифта, используемого для надписей на кнопках. Если шрифт велик настолько, что текст по ширине будет превосходить указанную минимальную ширину в 200 единиц, то свойство MinWidth будет проигнорировано и ширина кнопки станет больше 200 единиц; при этом кнопка по-прежнему будет иметь указанные внутренние и внешние поля.

В окнах, подобных главному окну из нашего проекта, желательно, чтобы их размер подстраивался под размер содержимого (в данном случае – панели StackPanel). Для этого предусмотрено свойство окна SizeToContent, которое мы положили равным WidthAndHeight (можно также подстраивать под размер содержимого только ширину или только высоту окна). По умолчанию данное свойство равно Manual, в этом случае не окно подстраивается под свое содержимое, а наоборот – компоненты подстраиваются под размер окна. Заметим, что если оставить в xaml-файле атрибуты Width и Height для окна, то в окне дизайнера окно будет иметь указанные размеры даже при наличии атрибута SizeToContent, равного WidthAndHeight, однако при выполнении программы явно указанные размеры окна будут игнорироваться.

2.2. Решение проблем, возникающих при повтором открытии подчиненных окон

Ошибка. После закрытия окна win1 или win2 попытка его повторного открытия приводит к исключению с диагностикой «Нельзя задать Visibility или вызвать Show, ShowDialog или WindowInteropHelper.EnsureHandle после закрытия окна»). Это связано с тем, что закрытие окна, открытого в любом режиме, приводит к его разрушению (заметим, что в библиотеке Windows Forms подобная ситуация имеет место только для окон, открытых в обычном режиме, разрушения же окон, открытых в диалоговом режиме, не происходит).

Исправление. Для классов Window1 и Window2 определите следующие одинаковые обработчики события Closing:



Window1.xaml.cs и Window2.xaml.cs:



Результат. Теперь окна win1 и win2 можно многократно закрывать и открывать в ходе выполнения программы.

Комментарии

1. Событие Closing относится к группе событий, которые возникают перед выполнением некоторого действия и позволяют отменить его (имена этих событий оканчиваются на -ing). Второй параметр e у обработчиков подобных событий имеет изменяемое свойство Cancel, которому следует присвоить значение true, если требуется отменить соответствующее действие. В приведенном обработчике отменяется закрытие окна; вместо этого оно просто удаляется с экрана методом Hide (аналогичного результата можно добиться, установив значение его свойства Visibility равным значению Visibility.Hidden). Заметим, что сделанное изменение не препятствует «настоящему» закрытию подчиненных окон при закрытии главного окна приложения.

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

2.3. Контроль за состоянием подчиненного окна. Воздействие подчиненного окна на главное

Для окна MainWindow измените обработчик button1_Click:




Для окна Window1 определите обработчик события IsVisibleChanged:




Результат. Заголовок кнопки button1 главного окна и действия при ее нажатии зависят от того, отображается на экране подчиненное окно win1 или нет. Подчиненное окно можно закрыть не только с помощью кнопки button1 главного окна, но и любым стандартным способом, принятым в Windows (например, с помощью комбинации клавиш Alt+F4); при любом способе закрытия подчиненного окна заголовок кнопки button1 будет изменен. Подчеркнем, что изменять надпись на кнопке button1 в обработчике button1_Click не следует именно по той причине, что закрыть подчиненное окно можно не только с помощью этой кнопки.

Комментарий

В то время как главное окно для доступа к подчиненному может просто обратиться к нему по имени, подчиненное окно так сделать не может, поскольку имя главного окна ей неизвестно (главное окно в нашем проекте имени вообще не имеет). Однако подчиненное окно может обратиться к главному, используя свое свойство Owner. Для доступа к конкретному компоненту главного окна, имеющему имя, мы воспользовались методом FindName. Можно было поступить по-другому: выполнить явное приведение объекта Owner к типу MainWindow и после этого обратиться к его свойству button1:

2.4. Окно с содержимым в виде обычного текста


В начало описания класса Window1 добавьте поле



В имеющийся в классе Window1 обработчик Window_IsVisibleChanged добавьте следующий фрагмент:



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

Комментарии

1. Добавленное в описание класса Window1 поле count при создании окна автоматически инициализируется нулем; в дальнейшем это поле можно вызывать из любого метода класса. Новые поля позволяют хранить дополнительную информацию о состоянии окна.

2. Напомним, что при использовании операции инкремента вида ++i вначале происходит увеличение значения переменной i на 1, а затем данная переменная используется в выражении. Для операции i++ действия выполняются в обратном порядке: вначале прежнее значение i используется в выражении, а затем это значение увеличивается на 1.

3. В данном случае содержимым окна является не группирующий, а «обычный» компонент. Особенностью использованного компонента TextBlock является то, что его содержимым может быть только строка (этот компонент не имеет свойства Content, зато имеет свойство Text типа string). Для обеспечения центрирования текста по обоим измерениям достаточно установить соответствующие значения свойств HorizontalAlignment и VerticalAlignment компонента TextBlock.

2.5. Модальные и обычные кнопки диалогового окна

Рис. 9. Макет окна Window2 приложения WINDOWS




В описание класса Window2 добавьте новое свойство, доступное только для чтения, и связанное с ним поле:



Определите три обработчика, которые уже указаны в xaml-файле:




Обратите внимание на то, что обработчик button2_Click должен иметь модификатор public (он выделен в тексте полужирным шрифтом).

В классе MainWindow дополните обработчик button2_Click:



Результат. Диалоговое окно win2 позволяет изменить заголовки главного и подчиненного окна. Заголовки окон изменяются либо при нажатии обычной кнопки «Применить», либо при нажатии модальной кнопки «OK» (в последнем случае диалоговое окно закрывается). Окно также закрывается при нажатии модальной кнопки «Отмена»; в этом случае заголовки окон не изменяются. Вместо кнопки «OK» можно нажать клавишу Enter, вместо кнопки «Отмена» – клавишу Esc.

Комментарии

1. Для того чтобы нажатие на кнопку «Отмена» приводило к закрытию диалогового окна (а также чтобы нажатие клавиши Esc интерпретировалось как нажатие на эту кнопку), для данной кнопки надо установить равным true свойство IsCancel. Для того чтобы кнопка «ОК» считалась кнопкой по умолчанию (и нажатие клавиши Enter интерпретировалось как нажатие на эту кнопку), для данной кнопки надо установить равным true свойство IsDefault. Заметим, что хотя кнопка «ОК» сделана кнопкой по умолчанию, для нее все равно необходимо определить обработчик события Click (для кнопки «Отмена» обработчик определять не требуется).

2. Доступ из окна win2 к свойству Title окна win1 возможен благодаря тому, что эти окна имеют общего владельца (Owner), который хранит список своих подчиненных окон (в порядке их добавления) в свойстве-коллекции OwnedWindows.

3. Включенное в класс Window2 свойство DialogRes позволяет определить способ закрытия диалогового окна: если окно было закрыто по нажатию кнопки «ОК», то свойство равно true, если окно было закрыто по нажатию кнопки «Отмена» (или каким-либо другим способом, например, по нажатию кнопки закрытия на заголовке окна), то свойство равно false. Это свойство проверяется в главном окне после возврата из метода ShowDialog.

Следует сказать, что метод ShowDialog из библиотеки WPF тоже возвращает значение, позволяющее определить, каким образом было закрыто диалоговое окно (это значение имеет тип bool?, т. е. может быть равно true, false и null, и определяется по значению стандартного свойства окна DialogResult того же типа), однако данный механизм корректно работает только в ситуации, когда диалоговое окно действительно закрывается, а не просто удаляется с экрана, как в нашем случае.

4. Явный вызов метода button2_Click класса Window2 в обработчике button2_Click класса MainWindow обеспечивает выполнение действий, связанных с нажатием на кнопку «Применить» (таким образом, данный вызов имитирует нажатие на кнопку). При вызове этого метода в качестве параметров указаны константы null, так как значения параметров в методе button2_Click класса Window2 не используются.

Для возможности вызова метода button2_Click класса Window2 из класса MainWindow модификатор доступа для данного метода необходимо изменить с private на public или internal (модификатор internal обеспечивает доступ к данному методу в пределах создаваемого проекта).

Заметим, что в данном случае можно было бы обойтись без модификации метода button2_Click класса MainWindow: достаточно просто вызывать метод button2_Click класса Window2 в уже имеющемся обработчике button1_Click этого же класса Window2:

При этом отпадает необходимость в изменении модификатора метода button2_Click с private на public, и, кроме того, можно вообще обойтись без свойства DialogRes.

5. Макет окна Window2 демонстрирует те же особенности компоновки, что и ранее обсуждавшийся макет главного окна, только в более сложном варианте. В нем, как и в главном окне, все компоненты размещаются с учетом их «истинных» размеров, причем размер окна подстраивается под размер компонентов. В данном случае вместо панели StackPanel используется более сложный группирующий компонент Grid, позволяющий размещать данные по строкам и столбцам. Следует обратить внимание на способ задания количества строк и столбцов (мы использовали простейший способ; в более сложных ситуациях можно явно указывать размеры некоторых строк и столбцов или настраивать их размеры с соблюдением требуемых пропорций – см. далее проект IMGVIEW).

Номер ячейки, которую должен занимать компонент, определяется присоединенными свойствами Grid.Row и Grid.Column (которые «делегируются» дочерним компонентам таким же образом, как и рассмотренные в проекте EVENTS свойства Left и Top компонента Canvas). Если для компонента не указывать свойства Grid.Row или Grid.Column, то их значение считается равным 0, т. е. соответствует первой строке или первому столбцу компонента Grid. Настраивая присоединенное свойство Grid.ColumnSpan, можно обеспечить «захват» компонентом нескольких столбцов (имеется также парное свойство Grid.RowSpan). Мы использовали свойство ColumnSpan для размещения набора кнопок по всей ширине нижней строки компонента Grid, сгруппировав их с помощью вспомогательной горизонтальной панели StackPanel и выровняв эту панель по правой границе родительского компонента Grid.

Обратить внимание на интересную особенность полученного макета. Поскольку для одного из полей ввода мы задали свойство MinWidth, ширина полей ввода не может стать меньше значения этого свойства, но может увеличиваться, если ее минимального размера недостаточно для отображения введенного текста. При этом будет пропорционально увеличиваться и ширина компонента Grid, и ширина всего окна, причем кнопки будут по-прежнему выровнены по правой границе.

Недочет. При первом отображении диалогового окна в нем отсутствует активный компонент (т. е. элемент, имеющий фокус). В дальнейшем при закрытии и последующем открытии диалогового окна в нем будет активным тот компонент, который имел фокус в момент закрытия. Оба эти обстоятельства затрудняют работу с диалоговым окном. В частности, при повторном открытии диалогового окна его активным компонентом с большой долей вероятности будет кнопка «ОК» или «Отмена» (если предыдущее закрытие окна было выполнено путем нажатия на эту кнопку), что потребует от пользователя лишних действий для перехода к тому полю ввода, которое он хочет изменить. Этот недочет будет исправлен в п. 2.6.

2.6. Установка активного компонента окна. Особенности работы с фокусом в библиотеке WPF

В классе Window2 добавьте в метод Window_IsVisibleChanged следующий оператор:



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

Комментарии

1. Отметим, что указанное действие по установке фокуса происходит при скрытии окна. В этом можно убедиться, если добавить перед оператором установки фокуса условие:


В то же время, если использовать вариант

то фокус на первом поле ввода при последующих открытиях окна устанавливаться не будет.

Подобное странное поведение объясняется двумя такими же странными особенностями библиотеки WPF. Во-первых, метод Focus обеспечивает установку фокуса для указанного компонента только в случае, если в момент вызова метода окно отображается на экране (хотя более естественным было бы реализовать метод таким образом, чтобы он в любом случае сохранял информацию об установке фокуса и учитывал ее при отображении окна). Заметим, что метод Focus возвращает логическое значение, которое равно true, если вызов метода действительно обеспечил успешную установку фокуса на данном элементе.

Во-вторых, при установке свойства IsVisible равным true событие IsVisibleChanged наступает до того момента, как окно появится на экране, и наоборот, при установке свойства IsVisible равным false событие IsVisibleChanged выполняется, когда окно еще отображается на экране. Подобное поведение тоже представляется нелогичным, поскольку не позволяет связать некоторые действия (например, установку фокуса) с тем моментом, когда окно в очередной раз отображается на экране.

Тем не менее при первом отображении диалогового окна поле ввода textBox1 все же получает фокус, хотя это, казалось бы, противоречит сказанному выше. Это связано с особенностями реализации механизма настройки фокуса в WPF: если в окне еще не установлен активный компонент, то первый вызов метода Focus обеспечит установку фокуса на требуемый компонент даже при невидимом окне, несмотря на то, что этот вызов вернет значение false. Если же активный компонент уже был установлен ранее, когда окно еще отображалось на экране, то последующие вызовы метода Focus при скрытом окне не позволят изменить фокус.

Описанные особенности демонстрируют сложность и некоторую непоследовательность реализации механизма работы с фокусом в WPF. Отметим, что в WPF имеются два вида фокуса: клавиатурный и логический. Для обработки клавиатурного фокуса можно использовать методы класса Keyboard. В частности, свойство Keyboard.FocusedElement, доступное только для чтения, позволяет определить элемент приложения, имеющий в данный момент фокус, а метод Keyboard.Focus(comp) позволяет установить фокус на компонент comp, но только в случае, если этот компонент отображается на экране и находится в активном в данный момент окне. Для работы с логическим фокусом предназначен класс FocusManager. В частности, он позволяет устанавливать различные области фокусировки, в каждой из которых может быть определен свой логический фокус, а также получать и изменять логический фокус для каждой области фокусировки fscope методами GetFocusedElement(fscope) и SetFocusedElement(fscope, comp). Если какая-либо область фокусировки теряет клавиатурный фокус, то, тем не менее, в ней сохраняется информация о том ее компоненте, который имеет логический фокус. Поэтому в дальнейшем данный компонент автоматически получит клавиатурный фокус, если фокус примет сама область. К сожалению, вся эта красивая схема работает только в случае, когда окно отображается на экране. Изменить активный компонент для скрытого окна, если в нем уже имеется активный компонент, описанными выше средствами невозможно. В частности, даже если для скрытого окна попытаться установить логический фокус на другой компонент, при отображении этого окна фокус получит тот компонент, который имел фокус в момент скрытия окна, а не тот, для которого (при скрытом окне) вызывался метод SetFocusedElement. Единственная ситуация, при которой возможна установка логического фокуса для скрытого окна, – это ситуация, при которой в окне ранее еще не было компонента, имеющего фокус. Мы уже отмечали, что в этой ситуации установить фокус можно проще: обычным вызовом метода Focus().

2.7. Запрос на подтверждение закрытия окна

В классе Window1 измените обработчик Window_Closing:




Результат. Перед закрытием подчиненного окна win1 отображается стандартное диалоговое окно «Подтверждение» с запросом на подтверждение закрытия (рис. 10). При выборе варианта «Нет» (который предлагается по умолчанию) закрытие подчиненного окна отменяется.


Рис. 10. Стандартное диалоговое окно


Комментарий

В обработчике использован один из наиболее полных вариантов метода Show класса MessageBox, позволяющий указать (1) текст запроса, (2) заголовок окна запроса, (3) набор кнопок для данного окна, (4) иконку в окне и (5) кнопку, предлагаемую по умолчанию. Любой параметр, кроме первого, может отсутствовать; при этом должны отсутствовать и все следующие за ним параметры. Если отсутствует второй параметр, то заголовок окна является пустым, если третий, то в окне отображается единственная кнопка «ОК», если четвертый – иконка в окне не отображается, если пятый – кнопкой по умолчанию является первая кнопка.

Недочет 1. При выборе в диалоговом окне варианта «Да» подчиненное окно закрывается, но главное окно не становится активным.

Данный недочет объясняется тем обстоятельством, что «владельцем» диалогового окна MessageBox является то окно, которое было активным в момент отображения на экране окна MessageBox (в нашем случае это подчиненное окно win1), и именно это окно должно активизироваться при закрытии окна MessageBox. Однако при выборе варианта «Да» окно win1 закрывается, и поэтому его активизация оказывается невозможной. В подобной ситуации ни одно окно на экране не будет активным, а главное окно нашей программы, скорее всего, будет скрыто окном среды Visual Studio. Одним из вариантов исправления подобного недочета является явное указание владельца окна MessageBox в дополнительном параметре, который должен располагаться первым в списке параметров. Например, в качестве этого параметра можно указать Owner. В этом случае при выборе варианта «Да» будет успешно активизировано главное окно. Однако это же окно будет активизироваться и при выборе варианта «Нет» (когда подчиненное окно останется на экране), что является неестественным.

Исправление. Замените оператор Hide() в методе Window_Closing класса Window1 на следующий составной оператор:



Недочет 2. Если в программе ни разу не отображалось подчиненное окно, то при закрытии главного окна выводится запрос на подтверждение закрытия подчиненного окна, хотя это окно на экране отсутствует.

Исправление. Добавьте в начало метода Window_Closing класса Window1 следующий фрагмент:


3. Совместное использование обработчиков событий и работа с клавиатурой: CALC

Рис. 11. Окно приложения CALC


3.1. Настройка коллективного обработчика событий

Рис. 12. Макет окна MainWindow




Для кнопки button1 создайте обработчик события Click (напомним, что для этого достаточно ввести в xaml-файле текст Click= и в появившемся выпадающем списке выбрать вариант «New Event Handler»:



Дополните созданный в cs-файле обработчик следующим образом:



После этого переместите текст Click="button1_Click" в открывающий тег родителя кнопки button1 (т. е. ближайшего к ней компонента StackPanel), дополнив имя Click префиксом Button:



Результат. Нажатие на любую кнопку приводит к отображению текста, указанного на этой кнопке, в метке label1 между полями ввода textBox1 и textBox2.

Комментарии

1. Поскольку при нажатии на любую из кнопок с обозначением арифметической операции следует выполнять однотипные действия, создавать для каждой кнопки особый обработчик события Click нецелесообразно. В приложениях Windows Forms в подобной ситуации создается один обработчик, который затем связывается с соответствующими событиями всех требуемых компонентов. Такой подход возможен и в WPF-приложениях. В нашем случае его можно реализовать, определив обработчик button1_Click для кнопки button1 и указав в xaml-файле атрибут Click="button1_Click" для всех четырех кнопок button1–button4. При этом оператор в обработчике button1_Click можно изменить, указав вместо e.Source параметр sender (оба варианта будут работать одинаково):

Однако в случае WPF-приложений можно использовать другой подход, который позволяет избежать явного связывания обработчика с событиями для нескольких компонентов. Подход основан на механизме маршрутизируемых событий (routed events), благодаря которому информация о возникших событиях может передаваться по цепочке компонентов. В WPF почти все стандартные события являются маршрутизируемыми. При этом все маршрутизируемые события делятся на три категории: прямые (direct events), которые ведут себя как обычные события .NET и не передаются по цепочке наследования (примером такого события является MouseEnter); туннелируемые (tunneling events), которые возникают в компоненте верхнего уровня и «спускаются» по цепочке его дочерних компонентов к компоненту, в котором фактически произошло действие, вызвавшее данное событие (например, событие PreviewTextInput, которое будет использовано далее в нашем проекте), и пузырьковые (bubbling events), которые «поднимаются» от компонента, где произошло событие, вверх по цепочке его родительских компонентов (например, событие MouseDown или использованное в данном пункте событие Click). Заметим, что в названиях всех туннелируемых событий используется префикс Preview и событие с таким префиксом наступает до наступления одноименного события без этого префикса.

На всем пути прохождения события оно может приводить к запуску связанных с ним обработчиков. Замечательной чертой механизма маршрутизируемых событий в WPF является то, что обработчик для маршрутизируемого события можно связать даже с тем родителем, для которого соответствующее событие не определено! Именно такая ситуации имеет место в нашем случае, поскольку для компонента StackPanel не предусмотрено событие Click. Тем не менее мы смогли связать с ним обработчик для события Click, которое может возникать в его дочерних компонентах (для этого нам потребовалось уточнить имя Click именем того компонента, для которого событие Click определено: Button.Click). Подобное поведение похоже на поведение присоединенных свойств (подробно рассмотренных в проекте EVENTS), поэтому в данной ситуации говорят о присоединенных событиях (attached events).

Теперь при возникновении события Click у любой из кнопок панели StackPanel оно «поднимется» к родителю-панели и приведет к вызову связанного с ним обработчика button1_Click. При этом в параметре sender обработчика будет указан компонент, в котором был вызван обработчик (в нашем случае панель StackPanel), а в свойстве Source второго параметра e будет указан компонент, в котором фактически произошло событие (в нашем случае одна из кнопок).

Описанный механизм имеет одну особенность, которую необходимо учитывать: при связывании события в родительском компоненте с некоторым обработчиком мы не можем указать ту часть набора дочерних компонентов, для которой надо использовать обработчик. Обработчик будет вызываться для всех дочерних компонентов, для которых предусмотрено соответствующее событие (а также и для самого родительского компонента, если для него тоже предусмотрено это событие). Не следует думать, что указание префикса Button при определении обработчика в компоненте StackPanel ограничит действие обработчика только компонентами Button. Обработчик будет вызван для дочернего компонента любого типа, если в нем произойдет событие Click.

В нашем случае указанная особенность приведет к недочету в программе (он описывается в конце данного пункта).

2. В макете нашего приложения нельзя естественным образом распределить компоненты по столбцам. В такой ситуации использование группирующего компонента Grid нецелесообразно; вместо него мы используем вложенный набор панелей StackPanel (внешняя панель с вертикальной ориентацией содержит две горизонтально ориентированные панели, причем для второй горизонтальной панели дополнительно устанавливается выравнивание по правой границе).

Недочет. При нажатии на кнопку «=» между полями ввода выводится знак равенства, что не имеет смысла. Этот недочет будет исправлен в следующем пункте.

3.2. Организация вычислений

Определите обработчик события 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.

3.3. Простейшие приемы ускорения работы с помощью клавиатуры


Обратите внимание на добавленные символы подчеркивания в свойствах 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 оператор:


3.4. Использование обработчика событий от клавиатуры

Определите обработчик события 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).

3.5. Контроль за изменением исходных данных

Добавьте в метод 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 и на более сложный способ определения второго параметра, требующий вызова конструктора класса, связанного с данным типом обработчиков событий.

4. Работа с датами и временем: CLOCK

Рис. 14. Окно приложения CLOCK


4.1. Отображение текущего времени


В список директив 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:


4.2. Реализация возможностей секундомера


В классе 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].

The free sample has ended.