Разработка пользовательского интерфейса на основе технологии Windows Presentation Foundation
Шрифт:
Комментарии
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: