bannerbannerbanner
Разработка пользовательского интерфейса на основе технологии Windows Presentation Foundation

М. Э. Абрамян
Разработка пользовательского интерфейса на основе технологии Windows Presentation Foundation

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

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

1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18 
Рейтинг@Mail.ru