WPF4The Definitive Guide

User Manual:

Open the PDF directly: View PDF PDF.
Page Count: 889

DownloadWPF4The Definitive Guide
Open PDF In BrowserView PDF
WPF4

Unle

WPF4
Unleashed

Adam Nathan

SAMS

WPF4
Подробное руководство

Адам Натан

Санкт-Петербург - Москва
2011

Адам Натан

WPF4. Подробное руководство
Перевод А.Слинкин
Главный редактор А. Галунов
Зав. редакцией
Н. Макарова
Редактор
Е. Тульсанова
Корректоры
С. Минин, О. Макарова
Верстка
Д. Орлова
Натан А.
WPF 4. Подробное руководство. - Пер. с англ. - СПб.: Символ-Плюс, 2011. - 880 с., ил.
ISBN 978-5-93286-196-7
Windows Presentation Foundation (WPF) — рекомендуемая технология реализации
пользовательских интерфейсов для Windows-приложений. Она позволяет создавать
такие функционально насыщенные и визуально привлекательные приложения, о
которых вы раньше не могли и мечтать. WPF дает возможность естественно
объединять в одной программе традиционные интерфейсы, трехмерную графику, аудио
и видео, анимацию, динамическую смену обложек, мультисенсорный ввод,
форматированные документы и распознавание речи.
Книгу Адама Натана, известного гуру в области WPF, отличают полнота освещения,
практические примеры и понятный язык. Издание содержит сведения
о
XAML — расширяемом языке разметки приложений; детально рассматриваются
функциональные возможности WPF: элементы управления, компоновка, ресурсы,
привязка к данным, стили, графика, анимация; уделено внимание новейшим средствам:
мультисенсорному вводу, усовершенствованной визуализации текста, новым
элементам управления, дополнениям языка XAML, программе Visual State Manager,
переходным функциям в анимации; рассматриваются трехмерная графика, синтез и
распознавание речи, документы и эффекты; демонстрируется создание популярных
элементов пользовательского интерфейса, например галерей и экранных подсказок, а
также создание более сложных механизмов организации пользовательского
интерфейса, например выдвигающихся и стыкуемых панелей, как в Visual Studio;
описывается, как создавать полноценные элементы управления WPF; демонстрируется
создание гибридных приложений, в которых WPF сочетается с Windows Forms, DirectX
и ActiveX; объясняется, как задействовать в WPF-приложении новые средства Windows
7, например списки переходов, и как обойти некоторые присущие WPF ограничения.
ISBN 978-5-93286-196-7
ISBN 978-0-672-33119-0 (англ)
© Издательство Символ-Плюс, 2011
Authorized translation of the English edition © 2010 Pearson Education. This translation is
published and sold by permission of Pearson Education, the owner of all rights to publish and
sell the same.
Все права на данное издание защищены Законодательством РФ, включая право на
полное или частичное воспроизведение в любой форме. Все товарные знаки или
зарегистрированные товарные знаки, упоминаемые в настоящем издании, являются
собственностью соответствующих фирм.
Издательство «Символ-Плюс». 199034, Санкт-Петербург, 16 линия, 7, тел. (812) 3805007, www.symbol.ru. Лицензия ЛП N 000054 от 25.12.98. Подписано в печать
04.11.2011. Формат 70x100 1/ 1 6 . Объем 55 печ. л.

Оглавление
Введение………………...……………………………………...……………………………..19
I. Базовые

сведения ................................................................. ………………………………..27

1.Почему именно WPF и как насчет Silverlight?........... ………………………………..29
Взгляд в прошлое………………………………………...………………………………30
Появление WPF…………………………..………………………………………………32
Эволюция WP…………………………….………………………………………………35
Усовершенствования в WPF 3.5 и WPF 3.5 SP1………………………….…………36
Усовершенствования в WPF 4………………………..………………………………38
Что такое Silverlight………………………………………………………………………40
Резюме…………………………………….………………………………………………42
2.Все тайны XAML .............................................................. …..……………………………43
Определение XAML……………………...………………………………………………45
Элементы и атрибуты…………………….………………………………………………47
Пространства имен……………………….………………………………………………48
Элементы свойств………………………...………………………………………………51
Конвертеры типов………………………...………………………………………………52
Расширения разметки…………………….………………………………………………55
Дочерние объектные элементы………….………………………………………………58
Свойство Content……………………...………………………………………………58
Элементы коллекций………………….………………………………………………59
Еще о преобразовании типов…………………………………………………………61
Сочетание XAML и процедурного кода...………………………………………………63
Загрузка и разбор XAML во время выполнения……………………………………63
Компиляция XAML…………………..………………………………………………67
Введение в XAML2009 ………………….………………………………………………72
Полная поддержка универсальных классов…………………………………………73
Словарные ключи произвольного типа...……………………………………………74
Встроенные системные типы данных..………………………………………………75
Создание объектов с помощью конструктора с аргументами ................................... 75
Создание экземпляров с помощью фабричных методов……………………………76
Гибкость присоединения обработчиков событий…...………………………………76

6

Оглавление
Определение новых свойств,,,,………………………………………………………77
Трюки с классами чтения и записи XAML,,,,………………………………………77
Обзор……………………..……………………………………………………………78
Циклы обработки узлов………………………………………………………………81
Чтение XAML…..……………………………………………………………………..82
Запись в объекты………………...……………………………………………………86
Запись в формате XML.………………………………………………………………88
XamlServices.....……………………………………………………………………….89
Ключевые слова XAML…………………………………………………………………92
Резюме..…………………………………………………………………………………..96
Возражение 1: XML слишком многословен, долго набирать………………….......97
Возражение 2: системы, основанные на XML, низкопроизводительны…………..97
3. Основные принципы WPF….……………………………………………………… 98
Обзор иерархии классов….……………………………………………………………..98
Логические и визуальные деревья…………………………………………………….100
Свойства зависимости….………………………………………………………………106
Реализация свойства зависимости………………………………………………….107
Уведомление об изменении………………………………………………………...109
Наследование значений свойств……………………………………………………111
Поддержка нескольких поставщиков………………………………………………113
Присоединенные свойства………………………………………………………….117
Резюме….……………………………………………………………………………….121
II. Создание

WPF-приложения…………………………………………………………..123

4. Задание размера, положения и преобразований элементов…………………....125
Управление размером…………………………………………………………….……126
Свойства Height и Width…………………………………………………………….126
Свойства Margin и Padding………………………………………………………….128
Свойство Visibility…...……………………………………………………………...131
Управление положением……………………..………………………………………..132
Выравнивание……………………………………………………………………….132
Выравнивание содержимого………………………………………………………..133
Свойство FlowDirection……………………………………………………………..134
Применение преобразований……………………………………………………….135
Преобразование RotateTransform……………………….…………………………..137
Преобразование ScaleTransform……………………………………………………139
Преобразование SkewTransform……………………………………………………141
Преобразование TranslateTransform………………………………………………..142
Преобразование MatrixTransform…………………………………………………..142
Комбинирование преобразований………………………………………………….143

7

Оглавление
Резюме…………………………………………………………………………………….144
5. Компоновка с помощью панелей…………………………………………………..146
Панель Canvas…………………………………………………………………………….147
Панель StackPanel………………………………………………………………………...150
Панель WrapPanel………………………………………………………………………..152
Панель DockPanel………………………………………………………………………...154
Панель Grid……………………………………………………………………………….158
Задание размеров строк и столбцов…………………………………………………..162
Интерактивное задание размера с помощью GridSplitter…………………………...165
Задание общего размера для строк и столбцов……………………………………...166
Сравнение Grid с другими панелями…………………………………………………169
Примитивные панели… … … … … … … … … … … … … … … … … … … … … … … … … . . 171
Панель TabPanel……………………………………………………………………….171
Панель ToolBarPanel…………………………………………………………………..171
Панель ToolBarOverflowPanel………………………………………………………...171
Панель ToolBarTray…………………………………………………………………...171
Панель UniformGrid…………………………………………………………………...172
Панель SelectiveScrollingGrid…………………………………………………………172
Обработка переполнения содержимого…………………………………………………173
Отсечение……………………………………………………………………………...173
Прокрутка……………………………………………………………………………...175
Масштабирование……………………………………………………………………..177
Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели……..182
Резюме…………………………………………………………………………………….192
6. События ввода: клавиатура, мышь, стилус
и мультисенсорные устройства……………………………………………………….193
Маршрутизируемые события……………………………………………………………193
Реализация маршрутизируемого события…………………………………………...194
Стратегии маршрутизации и обработчики событий………………………………...195
Маршрутизируемые события в действии……………………………………………196
Присоединенные события…………………………………………………………….200
События клавиатуры……………………………………………………………………..202
События мыши…………………………………………………………………………...205
Класс MouseEventArgs………………………………………………………………...206
Перетаскивание………………………………………………………………………..207
Захват мыши…………………………………………………………………………...208
События стилуса………………………………………………………………………….209
Класс StylusDevice………………………………………………………………….....210
События ....................................................................................... ………………………210
Мультисенсорные события………………………………………………………….….211
Простые события касания…………………………………………………………....212

8

Оглавление

События манипулирования, описывающие сдвиг,поворот и масштабирование.216
Команды………………………………………………………………………………….224
Встроенные команды………………………………………………………………….225
Выполнение команд с помощью жестов ввода……………………………………...228
Элементы управления со встроенными привязками к командам…………………..229
Резюме…………………………………………………………………………………….230
7. Структурирование и развертывание приложения………………………………231
Стандартные приложения Windows……………………………………………………..231
Класс Window…………………………………………………………………………232
Класс Application………………………………………………………………………235
Показ заставки………………………………………………………………………...242
Создание и показ диалоговых окон…………………………………………………..243
Сохранение и восстановление состояния приложения……………………………...246
Развертывание: технология ClickOnce и установщик Windows…………………….247
Приложения Windows с навигацией…………………………………………………….249
Страницы и их навигационные контейнеры…………………………………………249
Переходы между страницами………………………………………………………...252
Передача данных между страницами………………………………………………...258
Приложения-гаджеты…………………………………………………………………….261
XAML-приложения для браузера………………………………………………………..263
Ограниченный набор возможностей…………………………………………………265
Интегрированная навигация………………………………………………………….268
Развертывание…………………………………………………………………………268
Автономные XAML-страницы…………………………………………………………..271
Резюме…………………………………………………………………………………….272
8. ОсобенностиWindows7………………………………………………………………273
Списки переходов………………………………………………………………………...273
Элемент JumpTask…………………………………………………………………….275
Элемент JumpPath……………………………………………………………………..282
Настройка элементов на панели задач…………………………………………………..287
Индикатор выполнения для элемента на панели задач……………………………...287
Наложения для элементов на панели задач………………………………………….288
Настройка содержимого эскиза………………………………………………………289
Добавление кнопок управления к эскизу на панели задач………………………….290
Функция Aero Glass………………………………………………………………………292
Функция TaskDialog……………………………………………………………………...296
Резюме…………………………………………………………………………………….299

9

Оглавление

III.

Элементы управления………………………………………………………………...301

9. Однодетные элементы управления..………………………………………………303
Кнопки ......................................................................... …………………………………..305
Класс Button ............................................................ …………………………………..306
Класс RepeatButton ................................................. …………………………………..307
Класс ToggleButton ................................................. …………………………………..308
Класс CheckBox ...................................................... …………………………………..308
Класс RadioButton................................................... ………………………………..…309
Простые контейнеры ................................................. …………………………………..311
Класс Label .............................................................. …………………………………..311
Класс ToolTip .......................................................... …………………………………..312
Класс Frame ............................................................. …………………………………..314
Контейнеры с заголовками ....................................... …………………………………..316
Класс GroupBox ............................................................................................................. .316
Класс Expander ............................................................................................................... .318
Резюме ................................................................................................................................ .318
10. Многодетные элементы управления….……………………………………….....319
Общая функциональность ......................................... …………………………………..320
DisplayMemberPath .............. .................................. …………………………………..321
ItemsPanel................................................................. …………………………………..322
Управление поведением прокрутки. .................... …………………………………..325
Селекторы.................................................................... …………………………………..325
Элемент ComboBox ................................................ …………………………………..326
Элемент ListBox...................................................... …………………………………..332
Элемент ListView.................................................... …………………………………..335
Элемент TabControl ................................................ …………………………………..336
Элемент DataGrid.................................................... ……………………………..……337
Меню ............................................................................ …………………………………..345
Элемент Menu ......................................................... …………………………………..345
Элемент ContextMenu ............................................ …………………………………..347
Другие многодетные элементы управления ........... …………………………………..349
Элемент TreeView .................................................. …………………………………..349
Элемент ToolBar ..................................................... …………………………………..351
Элемент StatusBar ................................................... …………………………………..354
Резюме ......................................................................... …………………………………..355
11. Изображения, текст и другие элементы управления…………………………..356
Элемент управления Image ....................................... …………………………………..356
Элементы управления Text и Ink.............................. …………………………………..358
Элемент TextBlock.................................................. …………………………………..360
Элемент TextBox .................................................... …………………………………..362
Элемент RichTextBox ............................................. …………………………………..364

10

Оглавление
Элемент PasswordBox………………………………………………………………364
Элемент InkCanvas………………………………………………………………….365
Документы……………………………………………………………………………...367
Создание потоковых документов…………………………………………………..367
Отображение потоковых документов……………………………………………...378
Добавление комментариев………………………………………………………….380
Диапазонные элементы управления…………………………………………………..383
Элемент ProgressBar ………………………………………………………………...384
Элемент Slider ……………………………………………………………………….385
Календарные элементы управления…………………………………………………..386
Элемент Calendar ......... ……………………………………………………………..386
Элемент DatePicker .... ………………………………………………………………388
Резюме ………………………………………………………………………………….389

IV. Средства

для профессиональных разработчиков…………………………………...391

12. Ресурсы ............... ………………………………………………………………………393
Двоичные ресурсы ......... ………………………………………………………………393
Определение двоичного ресурса…………………………………………………...394
Доступ к двоичным ресурсам………………………………………………………395
Локализация двоичных ресурсов…………………………………………………..400
Логические ресурсы ...... ……………………………………………………………….402
Поиск ресурса............... ……………………………………………………………..406
Статические и динамические ресурсы…………………………………………….406
Взаимодействие с системными ресурсами………………………………………..411
Резюме………………………………………………………………………………….413
13. Привязка к данным . …………………………………………………………………414
Знакомство с объектом Binding……………………………………………………….414
Использование объекта Binding в процедурном коде…………………………….414
Использование объекта Binding в XAML…………………………………………417
Привязка к обычным свойствам .NET……………………………………………..419
Привязка ко всему объекту…………………………………………………………420
Привязка к коллекции ………………………………………………………………422
Обобществление источника с помощью DataContext…………………………….426
Управление визуализацией……………………………………………………………428
Форматирование строк…………………………………………………………...428
Шаблоны данных .... ………………………………………………………………431
Конвертеры значений……………………………………………………………..434
Настройка представления коллекции………………………………………..…….440
Сортировка .............. ………………………………………………………………440
Группировка ............................................................... ……………………………...443
Фильтрация………………………………………………………………………..446
Навигация………………………………………………………………………….447

11

Оглавление
Дополнительные представления ....................................... ………………………...449
Поставщики данных ............................ .................... .............. ………………………...451
Класс XmlDataProvider ....................................................... ………………………...452
Класс ObjectDataProvider…………………………………………………………...455
Дополнительные вопросы....................................................... ………………………...459
Настройка потока данных .................................................. ………………………...459
Добавление в привязку правил проверки………………………………………….461
Работа с несколькими источниками……………………………………………….466
А теперь все вместе: клиент Twitter на чистом XAML………………………………469
Резюме ....................................... ………………………………………………………..471

14. Стили, шаблоны, обложки и темы…………………………………………………472
Стили .....................................................………………………………………………...473
Обобществление стилей……………………………………………………………475
Триггеры…………………………………………………………………………….481
Шаблоны ................................... .......... ............................ ……………………………...488
Введение в шаблоны элементов управления……………………………………...489
Обеспечение интерактивности с помощью триггеров……………………………490
Ограничение типа целевого элемента……………………………………………..492
Учет свойств шаблона-родителя…………………………………………………...493
Учет визуальных состояний с помощью триггеров………………………………500
Учет визуальных состояний с помощью менеджера визуальных состояний…505
Комбинирование шаблонов со стилями…………………………………………...514
Обложки ...................................................... …………………………………………….517
Темы ...................................................... .................... …………………………………...524
Системные цвета, шрифты и параметры…………………………………………..524
Стили и шаблоны тем ......... ..................... …………………………………………525
Резюме………………………………………………………………………………….529
V.

Мультимедиа ........................................... ..... …………………………………………….531
15. Двумерная графика…………………………………………………………………..533
Класс Drawing........................................................................ …………………………...534
Класс Geometry...................................... .............. …………………………………537
Класс Реп.......................................................... .... ...... ……………………………...548
Пример изображения ............................ ........ ……………………………………...550
Класс Visual ................... .......................................................... ………………………...552
Наполнение DrawingVisual содержимым………………………………………….553
Отображение объекта Visual на экране……………………………………………556
Проверка попадания в Visual………………………………………………………559
Класс Shape ................................................. .............. …………………………………...566
Класс Rectangle……………………………………………………………………...568
Класс Ellipse………………………………………………………………………...569

12

Оглавление

Класс Line . ………………………………………………………………………………570
Класс Polyline……………………………………………………………………………571
Класс Polygon……………………………………………………………………………572
Класс Path ………………………………………………………………………………..572
Изображение, составленное из объектов Shape………………………………………..573
Кисти ............................................................................ ……………………………………….575
Цветные кисти ......................................................... ……………………………………...578
Мозаичные кисти………………………………………………………………………..584
Кисти как маски непрозрачности………………………………………………………592
Эффекты .... ………………………………………………………………………………….594
Повышение производительности визуализации…………………………………………597
Класс RenderTargetBitmap………………………………………………………………597
Класс BitmapCache………………………………………………………………………598
Класс BitmapCacheBrush………………………………………………………………..601
Резюме……………………………………………………………………………………….601
16. Трехмерная графика………………………………………………………………...602
Введение в трехмерную графику………………………………………………………….603
Камеры и системы координат……………………………………………………………..607
Свойство Position………………………………………………………………………..608
Свойство LookDirection…………………………………………………………………611
Свойство UpDirection……………………………………………………………………614
Классы OrthographicCamera и PerspectiveCamera……………………………………...617
Класс Transform3D…………………………………………………………………………620
Преобразование TranslateTransform3D ................ ……………………………………...623
Преобразование ScaleTransform3D ...................... ……………………………………...623
Преобразование RotateTransform3D..................... ……………………………………...627
Комбинирование преобразований Transform3D……………………………………….630
Класс Model3D……………………………………………………………………………..631
Класс Light ............................................................... ……………………………………...632
Класс GeometryModelSD ....................................... ……………………………………...639
Класс Model3DGroup.............................................. ……………………………………...654
Класс VisualSD ............................................................ ……………………………………...656
Класс ModelVisual3D ............................................. ……………………………………...656
Класс UIElement3D ................................................. ……………………………………...658
Класс Viewport2DVisual3D.................................... ……………………………………...660
Проверка попадания в трехмерном пространстве……………………………………662
Класс Viewport3D ....................................................... ……………………………………...663
Преобразование двумерных и трехмерных систем координат . . . . ……………………666
Метод Visual.TransformToAncestor ...................... ……………………………………...666
Методы Visual3D.TransformToAncestor и Visual 3D.
TransformToDescendant .......................................... ……………………………………...670
Резюме .......................................................................... ……………………………………...674

13

Оглавление

17. Анимация...................................................................... .......... ………………………..675
Анимация в процедурном коде ................................................. ………………………..676
Выполнение анимации «вручную» ...................................... ………………………..676
Введение в классы анимации................................................ ………………………..677
Простые приемы работы с анимацией ................................ ………………………..685
Анимация в XAML-коде ............................................................ ………………………..690
Триггеры событий и раскадровки ........................................ ………………………..690
Использование раскадровки как временной шкалы .......... ………………………..698
Анимация с опорными кадрами ................................................ ………………………..699
Линейные опорные кадры..................................................... ………………………..700
Сплайновые опорные кадры………………………………………………………...702
Дискретные опорные кадры ................................................. ………………………..703
Переходные опорные кадры ................................................. ………………………..706
Переходные функции…………………………………………………………………..706
Встроенные переходные функции………………………………………………….707
Другие встроенные переходные функции………………………………………….708
Написание своей переходной функции…………………………………………….710
Анимация и менеджер визуальных состояний………………………………………..712
Переходы…………………………………………………………………………….716
Резюме ............... ………………………………………………………………………...720
18.Аудио, видео и речь ................................................................. ………………………..722
Аудио................................................................................ .......... ………………………..722
Класс SoundPlayer .................................................................. ………………………..723
Класс SoundPlayerAction ....................................................... ………………………..724
Класс MediaPlayer .................................................................. ………………………..724
Классы MediaElement и MediaTimeline ............................... ………………………..725
Видео ............................................................................................ ……………………….727
Управление визуальными аспектами класса MediaElement. . . ………………….728
Управление мультимедийным содержимым ...................... ………………………..730
Речь ............................................................................................... ………………………..734
Синтез речи ............................................................................. ………………………..734
Распознавание речи......................... ..................................... ………………………..737
Резюме .................................................................... ..................... ………………………..743
VI. Дополнительные вопросы…………………………………………………………….745

19.Интероперабельность с другими технологиями………………………………….747
Встраивание элементов управления Win32 в WPF-приложения……………………750
Элемент управления Win32 Webcam................................... ………………………..750
Использование элемента управления Webcam в WPF ...... ………………………..753
Поддержка навигации с помощью клавиатуры.................. ………………………..760

14

Оглавление

Встраивание элементов управления WPF в Win32-приложения……………………..764
Введение в HwndSource………………………………………………………………765
Обеспечение правильной компоновки………………………………………………768
Встраивание элементов управления Windows Forms в WPF-приложения…………772
Встраивание PropertyGrid с помощью процедурного кода . . . . ................................773
Встраивание элемента PropertyGrid с помощью XAML……………………………775
Встраивание элементов управления WPF в приложения Windows Forms ………...777
Сочетание содержимого DirectX с содержимым WPF………………………………..781
Встраивание элементов управления ActiveX в WPF-приложения ………………...788
Резюме…………………………………………………………………………………...792
20. Пользовательские и нестандартные элементы управления ………………...794
Создание пользовательского элемента управления…………………………………...796
Создание пользовательского интерфейса элемента управления…………………..796
Наделение пользовательского элемента управления поведением ………………...799
Включение в пользовательский элемент управления свойств зависимости………802
Включение в пользовательский элемент управления
маршрутизируемых событий………………………………………………………...804
Создание нестандартного элемента управления………………………………………806
Программирование поведения нестандартного элемента ………………………...806
Создание пользовательского интерфейса нестандартного
элемента управления ........................... ……………………………………………….813
Некоторые соображения о более сложных элементах
управления............................................ ……………………………………………….817
Резюме ............................................... ……………………………………………………824
21 .Компоновка с помощью нестандартных панелей…………………………...……825
Взаимодействие между родителями и потомками……………………………………826
Этап измерения ............... …………………………………………………………….826
Этап размещения .................. .............. ....................... ………………………………..828
Создание панели SimpleCanvas………………………………………………………...830
Создание панели SimpleStackPanel…………………………………………………….834
Создание панели OverlapPanel………………………………………………………….837
Создание панели FanCanvas ............ ……………………………………………………842
Резюме ................................ ................ ........... …………………………………………847
Алфавитный указатель ........................................................ ………………………………..848

Об авторе

Адам Натан - ведущий разработчик системы Microsoft Visual Studio, последняя версия
которой представляет собой полноценное WPF-приложение. Ранее Адам был
основателем, архитектором и разработчиком сайта Popflу, первого продукта корпорации
Microsoft, построенного на базе технологии Silverlight, которая вошла в число 25 самых
инновационных продуктов 2007 года по версии журнала PCWorld Magazine. Начав
карьеру в составе коллектива разработчиков общеязыковой среды выполнения Microsoft
(Common Language Runtime), Адам постоянно находился в гуще событий, связанных с
созданием технологий .NET и WPF.
Многие сотрудники Microsoft и других компаний, занимающихся разработкой ПО,
считают книги Адама обязательными для прочтения. Он автор бестселлера «WPF
Unleashed» (Sams, 2006), который номинировался на премию Jolt Award в 2008 году, а
также книг «Silverlight 1.0 Unleashed» (Sams, 2008) и «.NET and COM: The Complete
Interoperability Guide» (Sams, 2002). Кроме того, Адам является одним из соавторов книг
«ASP.NET: Tips, Tutorials, and Code (Sams, 2001), «.NET Framework Standard Library
Annotated Reference, Volume 2» (Addison-Wesley, 2005) и «Windows Developer Power
Tools» (O‘Reilly, 2006). Натан также создал сайт PINVOKE.NET и связанную с ним
надстройку над Visual Studio. Связаться с Адамом можно через сайт www.adamnathan.net
или по адресу @adamnathan в Twitter.

Посвящается
Л и ндсей, Тайлеру и Райану

Благодарности
Как всегда, я благодарю свою чудесную супругу Линдсей за невероятную поддержку и
понимание. Нескончаемый процесс написания книг здорово сказывается на нашей жизни,
и никто бы не удивился, если бы ее терпение наконец иссякло. Однако же никогда
раньше ее поддержка не была столь ощутимой, как во время работы над этой книгой.
Линдсей, что бы я без тебя делал!
Хотя создание любой книги, и этой в том числе, - по большей части глубоко личное
занятие, она все же является плодом совместного труда многих талантливых и
трудолюбивых людей. Не откажу себе в удовольствии назвать их поименно.
Я искренне благодарен Дуэйну Ниду (Dwayne Need), старшему менеджеру команды
разработчиков WPF, - он потрясающий технический редактор. Его глубокие и
проницательные рецензии на черновые варианты позволили значительно улучшить
книгу. Выражаю признательность Роберту Хогу (Robert Hogue), Джо Кастро (Joe Castro)
и Джордану Паркеру (Jordan Parker) за полезные отзывы. Дэвид Тейтельбаум (David
Teitlebaum), специалист по трехмерной графике из команды разработчиков WPF,
заслуживает самой горячей благодарности за согласие подкорректировать замечательную
главу о 3D-графике, первоначально написанную Дэниелом Лехенбауэром (Daniel
Lehenba- uer). Ознакомиться с методологией и советами Дэниела и Дэвида - большая
удача для любого читателя, подумывающего о том, чтобы заняться трехмерной графикой.
Хочется также поблагодарить следующих людей (в алфавитном порядке): Брайана
Чэпмена (Brian Chapman), Беатрис де Оливейра Коста (Beatrix de Oliveira Costa),
Эфианию Эчеруо (Ifeanyi Echeruo), Дэна Глика (Dan Glick), Нила Кронлейга (Neil
Kronlage), Рико Мариани (Rico Mariani), Майка Мюллера (Mike Mueller), Олега
Овечкина, Лори Пирс (Lori Pearce), С. Рамини (S. Ramini), Роба Рилайи (Rob Relyea),
Тима Райса (Tim Rice), Бена Ронко (Ben Ronco), Адама Смита (Adam Smith), Тима Снита
(Tim Sneath), Дэвида Тредуэлла (David Treadwell) и Парамеша Вайдиянатана (Paramesh
Vaidyanathan).

17

Я также выражаю признательность коллективу издательства Sams, а особенно Нилу Роуи
(Neil Rowe) и Бетси Харрис (Betsy Harris), с которыми мне всегда приятно работать.
Лучшей команды для подготовки книги не найти. Никто ни разу не сказал мне, что текст
слишком длинный, или слишком короткий, или слишком отличается по стилю от
типичной книги из серии «Подробное руководство». Мне предоставили свободу писать
такую книгу, какую я хотел написать.
Спасибо также маме, папе и брату, которые раскрыли передо мной мир программирования, когда я еще учился в начальной школе. Если у вас есть дети, то
посвятите их в магию создания программ, когда они еще прислушиваются к вашим
словам! (A WPF и Silverlight помогут превратить этот опыт в незабываемое
удовольствие!)
И наконец, спасибо вам за то, что вы взяли в руки эту книгу и прочитали ее хотя бы до
этого места! Надеюсь, что вы на этом не остановитесь и для вас погружение в мир WPF 4
будет таким же завораживающим, как и для меня!

Нам важно ваше мнение!

Вы, читатель этой книги, - наш самый важный критик и комментатор. Мы ценим ваше
мнение и хотим знать, что мы сделали правильно, что могли бы улучшить, на какие темы
нам стоило бы выпускать книги. В общем, нам интересны любые мысли, которыми вы
хотели бы с нами поделиться.
Вы можете писать мне по обычной или электронной почте о том, что понравилось или не
понравилось в этой книге. А также о том, что мы могли бы еще сделать, чтобы наши
книги стали лучше.
Пожалуйста, имейте в виду, что я не в состоянии ответить на технические
вопросы по теме данной книги и что из-за большого количества получаемой почты
я не всегда имею возможность ответить на каждое сообщение.
Если будете писать, не забудьте указать название и автора книги, а также свое имя и
телефон или адрес электронной почты. Я внимательно изучу ваши замечания и направлю
их автору и редакторам, работавшим над книгой.
Электронная почта: feedback@samspublishing.com
Почтовый адрес: Neil Rowe
Executive Editor
Sams Publishing
800 East 96th Street
Indianapolis, IN 46240 USA

В помощь читателям
Посетите наш сайт и зарегистрируйте свой экземпляр книги по адресу
informit.com/register, чтобы получить доступ к обновлениям, загружаемым материалам
и перечню замеченных опечаток.

Введение
Благодарим за выбор книги ―WPF 4 Подробное руководство‖. Windows Presentation
Foundation (WPF) - самая современная из предлагаемых корпорацией Microsoft
технологий создания графических интерфейсов пользователя в ОС Windows, будь то
простые формы, документо-ориентированные окна, анимированные изображения, видео,
ЗD-среды с эффектом погружения или все вышеперечисленное. Технология WPF
позволяет разрабатывать самые разнообразные приложения проще, чем когда бы то ни
было ранее. Кроме того, она лежит в основе технологии Silverlight, которая
распространяет WPF на Сеть и мобильные устройства, например телефоны на базе ОС
Windows.
С момента анонсирования WPF в 2003 году (под кодовым названием Avalon) эта
технология привлекла к себе пристальное внимание благодаря революционному
изменению процесса разработки ПО - особенно со стороны программистов Windows,
привыкших к Windows Forms и GDI. WPF сравнительно легко позволяет создавать
интересные и полезные приложения, демонстрирующие разнообразные возможности,
которые трудно реализовать с помощью других технологий. В версии WPF 4,
выпущенной в апреле 2010 года, существенно улучшены практически все аспекты этой
технологии.
WPF знаменует собой отход от предшествующих технологий в плане модели
программирования, основополагающих идей и базовой терминологии. Даже просмотр
исходного кода WPF-приложения (например, путем декомпиляции его компонентов с
помощью программы .NET Reflector или ей подобной) может стать источником
сюрпризов, потому что интересующий вас код часто находится не там, где вы ожидаете.
А если добавить сюда еще и тот факт, что любую задачу в WPF можно решить
несколькими способами, то легко прийти к разделяемому многими выводу: изучить
WPF очень трудно.
Вот тут-то и приходит на помощь эта книга. Когда WPF еще только разрабатывалась,
было понятно, что не будет недостатка в книгах, посвященных этой технологии. Но
лично меня беспокоило другое: смогут ли авторы соблюсти баланс между изложением
самой технологии со всеми ее своеобразными идеями и демонстрацией использования
ее на практике. Поэтому, работая над первым изданием этой книги, «Windows
Presentation Foundation Unleashed», я ставил перед собой следующие цели:
 Познакомить читателя с базовыми концепциями в доступной форме, не покидая
практическую почву

20

Введение


Ответить на вопросы, возникающие у большинства изучающих технологию, и
показать, как решаются типичные задачи
 Предложить авторитетный источник информации благодаря участию членов
команды разработчиков WPF, которые проектировали, реализовывали и
тестировали эту технологию
 Четко очертить границы применимости технологии, не делая вид, что она
представляет собой решение всех проблем
 Предложить удобное справочное руководство, к которому можно возвращаться
снова и снова
Успех первого издания превзошел самые смелые мои ожидания. Теперь, по прошествии
четырех лет, я полагаю, что и во втором издании мне удалось достичь тех же целей,
только с большей глубиной. Помимо освещения новых возможностей, появившихся в
WPF 3.5, WPF 3.5 SP1 и WPF 4, я более подробно рассказываю о средствах, имевшихся
еще в первой версии WPF. Надеюсь, что любой читатель - неважно, приступает он к
изучению WPF впервые или имеет солидный опыт работы с этой технологией, согласится, что книга отвечает всем заявленным критериям.

Предполагаемая аудитория
Эта книга адресована разработчикам, заинтересованным в создании пользовательских
интерфейсов для Windows. Неважно, что именно вы разрабатываете: программы для
бизнеса или для массового потребителя, повторно используемые элементы управления, здесь вы найдете сведения, позволяющие извлечь максимум пользы из платформы. Книга
написана так, что ее смогут понять даже читатели, совсем не знакомые с каркасом .NET
Framework. Но и те, кто уверенно владеет WPF, тоже найдут интересную для себя информацию. Для них эта книга станет как минимум ценным справочным руководством.
Поскольку в основе WPF и Silverlight лежат одни и те же технология и концепции, то,
прочитав эту книгу, вы заодно повысите свою квалификацию как разработчика
приложений на платформе Windows Phone 7 и веб-приложений*
Хотя книга и не предназначена специально для графических дизайнеров, знакомство с
ней поможет лучше понять, что на самом деле представляют собой такие продукты, как
Microsoft Expression Blend.
Подведем итоги. В этой книге:
 Содержится все, что необходимо знать об основанном на XML языке extensible
Application Markup Language (XAML) для декларативного создания
пользовательских интерфейсов, допускающих применение стилей.
 Весьма детально рассматриваются различные функциональные возможности
WPF: элементы управления, компоновка, ресурсы, привязка к данным, стили,
графика, анимация и многое другое.

Введение

21

 Особое внимание уделено новейшим средствам, в том числе мультисенсорному
вводу, усовершенствованной визуализации текста, новым элементам управления,
дополнениям языка XAML, программе Visual State Manager, переходным кривым в
анимации и т. д.
 Освещаются вопросы, не затрагиваемые в большинстве других книг: трехмерная
графика, синтез и распознавание речи, документы, эффекты и пр.
 Демонстрируется создание популярных элементов пользовательского интерфейса,
например галерей, экранных подсказок, нестандартных способов компоновки
элементов.
 Демонстрируется создание более сложных механизмов организации пользовательского интерфейса, например выдвигающихся и стыкуемых панелей, как в
Visual Studio.
 Объясняется, как писать и развертывать приложения любых типов, в том числе со
встроенной навигацией, исполняемых в браузере и содержащих эффектные
непрямоугольные окна.
 Описывается, как создавать полноценные элементы управления WPF.
 Демонстрируется создание гибридных приложений, в которых WPF сочетается с
Windows Forms, DirectX, ActiveX и другими технологиями.
 Объясняется, как задействовать в WPF-приложении новые средства Windows 7,
например списки переходов, и как обойти некоторые присущие WPF ограничения.
Нельзя сказать, что в этой книге описаны абсолютно все возможности WPF (в частности,
вопросы спецификации XML Paper Specification (XPS) лишь слегка затронуты). Их так
много, что в одной книге рассмотреть все, на мой взгляд, невозможно. Но думаю, что вам
понравятся широта и глубина охвата материала.
Примеры, приведенные в книге, написаны на XAML и С#; при обсуждении вопросов
интероперабельности встречается также код на С++/CLI. Повсеместное использование
языка XAML объясняется рядом причин: зачастую это самый быстрый способ записать
исходный код; фрагменты, написанные на XAML, можно копировать в
инструментальные средства и видеть результат, не прибегая к компиляции; основанные
на WPF инструменты генерируют код на XAML, а не на процедурных языках; наконец,
XAML не зависит от того, на каком .NET-совместимом языке вы пишете: Visual Basic, C#
или еще каком-то. В тех случаях, когда соответствие между XAML и C# неочевидно,
приводятся эквивалентные представления кода на обоих языках.

Требования к программному обеспечению
В этой книге рассматриваются окончательная версия Windows Presentation Foundation 4.0,
соответствующий пакет Windows SDK и Visual Studio 2010.
Должно быть установлено следующее программное обеспечение:

22

Введение


Версия ОС Windows, поддерживающая .NET Framework 4.0, например: Windows
ХР с пакетом обновлений Service Pack 2 (включая Media Center, Tablet PC и
версию x64), Windows Server 2003 с пакетом обновлений Service Pack 1 (включая
версию R2), Windows Vista и более поздние версии ОС.
 Каркас .NET Framework 4.0, который устанавливается по умолчанию начиная с
Windows Vista. Для предыдущих версий Windows его можно бесплатно загрузить
с сайта http://msdn.com.
Кроме того, рекомендуется иметь следующее программное обеспечение:
 Пакет средств разработки Windows Software Development Kit (SDK) и прежде
всего включенные в него средства для .NET. Его также можно бесплатно
загрузить с сайта http://msdn.com.
 Visual Studio 2010 или более позднюю версию; подойдет и бесплатная версия
Express, имеющаяся на сайте http://msdn.com.
Для поддержки графического дизайна в среде WPF очень полезно иметь комплект
программ Microsoft Expression (конкретно Expression Blend).
Некоторые из включенных в книгу примеров ориентированы на системы Windows Vista,
Windows 7 или компьютер с поддержкой мультисенсорного ввода, но в большинстве
своем примеры будут работать во всех перечисленных выше версиях Windows.

Примеры кода
Исходный код всех примеров, встречающихся в этой книге, можно загрузить со
страницы http://informit.com/title/9780672331190 или http://adamnathan.net/wpf.

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

Часть I «Базовые сведения»
Эта часть состоит из следующих глав:
• Глава 1 «Почему именно WPF и как насчет Silverlight?»
• Глава 2 «Все тайны XAML»
• Глава 3 «Основные принципы WPF»
В главе 1 WPF сопоставляется с альтернативными технологиями, чтобы вам было
проще решить, отвечает ли она вашим нуждам. В главе 2 подробно рассматривается
язык XAML с целью заложить фундамент для понимания

Введение

23

XAML-кода, который встретится вам в этой книге и в реальной практике. В главе 3
освещаются уникальные особенности модели программирования WPF, выходящие за
пределы того, что уже известно программистам, работающим с .NET.

Часть II «Создание WPF-приложения»
Эта часть состоит из следующих глав:
 Глава 4 «Задание размера, положения и преобразований элементов»
 Глава 5 «Компоновка с помощью панелей»
 Глава 6 «События ввода: клавиатура, мышь, стилус и мультисенсорные
устройства»
 Глава 7 «Структурирование и развертывание приложения»


Глава 8 «Особенности Windows 7»

В части II вы узнаете, как собрать и развернуть традиционное приложение (хотя
затрагиваются и некоторые дополнительные механизмы, например преобразования,
непрямоугольные окна и технология Aero Glass). В главах 4 и 5 обсуждается компоновка
элементов управления (и других элементов) в пользовательском интерфейсе программы.
Глава 6 посвящена событиям ввода, в том числе поддержке новых устройств с
мультисенсорным вводом. В главе 7 рассматриваются различные способы пакетирования
и развертывания пользовательских интерфейсов на базе WPF для получения законченного приложения. В последней главе этой части речь пойдет об использовании некоторых
возможностей Windows 7, позволяющих создавать приложения с современным внешним
видом.

Часть III «Элементы управления»
Эта часть состоит из следующих глав:
 Глава 9 «Однодетные элементы управления»
 Глава 10 «Многодетные элементы управления»
 Глава 11 «Изображения, текст и другие элементы управления*
Часть III представляет собой обзор элементов управления, встроенных в WPF. Среди них
много хорошо знакомых, но есть и несколько неожиданных. Две категории элементов
управления — однодетные и многодетные1 - настолько важные и глубокие темы, что
заслуживают отдельных глав. Прочие элементы управления рассматриваются в главе 11.

1

Термины «однодетный» и «многодетный элемент управления» (content control и items
control) могут показаться непривычными, однако же какой-то эквивалент предложить
необходимо. Термин ―content control‖ буквально означает ―элемент управления со
свойством Content‖, a ―items control‖ – ―элемент управления со свойством Items‖.
Вариант «элемент управления содержимым», встречающийся в локализованных
продуктах Microsoft, совершенно не отражает сути дела. - Прим. перев.

24

Введение

Часть IV «Средства для профессиональных
разработчиков»
Эта часть состоит из следующих глав:
 Глава 12 «Ресурсы»
 Глава 13 «Привязка к данным»
 Глава 14 «Стили, шаблоны, обложки и темы»
Средства, рассматриваемые в части IV, не относятся к активно используемым в WPFприложениях, но их применение может существенно повысить качество процесса
разработки. Они незаменимы для профессиональных разработчиков, серьезно
относящихся к созданию надежных и удобных для сопроводи ждения приложений или
компонентов. Речь идет не столько о результатах, видимых конечному пользователю,
сколько о рекомендуемых способах достижения желаемого результата.

Часть V «Мультимедиа»
Эта часть состоит из следующих глав:
• Глава 15 «Двумерная графика»
• Глава 16 «Трехмерная графика»
• Глава 17 «Анимация»
• Глава 18 «Аудио, видео и речь»
В этой части рассматриваются те возможности WPF, которые обычно вызывают
наибольший интерес. Поддержка двумерной и трехмерной графики, анимации, видео и
пр. позволяет создавать приложения, поражающие воображение пользователя. Именно
эти средства наряду со способами их использования и отличают WPF от
предшествующих технологий. WPF снижает барьеры, стоящие на пути включения такого
содержимого в приложения, позволяя браться за задачи, о которых раньше вы и
помыслить не могли!

Часть VI «Дополнительные вопросы»
Эта часть состоит из следующих глав:
• Глава 19 «Интероперабельность с другими технологиями*
1
• Глава 20 «Пользовательские и нестандартные элементы управления»
• Глава 21 «Компоновка с помощью нестандартных панелей»
В части VI рассматриваются вопросы, интересные для разработчиков более - сложных
WPF-приложений и элементов управления. Уже имеющиеся элементы управления WPF
допускают применение стилей в очень широких пределах, поэтому потребность в
создании дополнительных элементов не так неасущна.

1

В терминологии Microsoft - «настраиваемые». - Прим. перев.

Введение

25

Типографские соглашения
В этой книге новые термины и иные специальные элементы выделяются с помощью
шрифтов, а именно:
Шрифт
Курсив

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

FAQ
Что такое врезка FAQ?
В такой врезке формулируется вопрос, который может возникнуть у читателя в
данном месте, и дается краткий ответ на него.

КОПНЕМ ГЛУБЖЕ
Врезки «Копнем глубже»
В такой врезке представлена более подробная информация по теме в дополнение к
содержащейся в основном тексте. Можно сказать, что это добавочные сведения для
особо любознательных.

СОВЕТ
Это описание приемов, которые могут пригодиться на практике, например самый
быстрый способ достижения цели или альтернативный подход, дающий более
качественный результат либо позволяющий решить задачу скорее и проще.

ПРЕДУПРЕЖДЕНИЕ
Такие врезки привлекают внимание к действию или условию, способному привести к
неожиданному либо непредсказуемому результату, - с объяснением того, как
избежать подобных последствий.

I
Базовые сведения

Глава 1 «Почему именно WPF и как насчет Silverlight?»
Глава 2 «Все тайны XAML»
Глава 3 «Основные принципы WPF»

1
Почему именно WPF и как насчет Silverlight?





Взгляд в прошлое
Появление WPF
Эволюция WPF
Что такое Silverlight

В кино и на телевидении главные герои обычно не похожи на обычных людей, которые
встречаются нам в повседневной жизни. Они внешне более привлекательные, обладают
мгновенной реакцией и почему-то всегда точно знают, что делать дальше. То же самое
можно сказать и про компьютерные программы, которые показывают в фильмах.
Впервые меня это поразило в 1994 году при просмотре фильма ―Disclosure‖
(Разоблачение) с Майклом Дугласом и Деми Мур. Почтовая программа, которой они
пользовались, выглядела совершенно не так, как Microsoft Outlook! По ходу фильма мы
дивились различным визуальным эффектам: вращающееся трехмерное «е»; сообщения,
которые разворачиваются при открытии и комкаются при удалении; намеки на
поддержку рукописного ввода и симпатичная анимация при распечатке сообщения. (Эта
почтовая программа еще не самая нереалистичная из встречающихся в фильме.
Достаточно лишь вспомнить «базу данных виртуальной реальности».)
Голливуд уже давно говорит нам, что реальные программы вовсе не такие впечатляющие,
какими должны быть, и речь здесь идет отнюдь не о функциональности. Вы, наверное, и
сами сможете вспомнить несколько примеров забавных и фантастичных программ из
известных фильмов и сериалов. Однако в последние годы реальные программы стали
подтягиваться к голливудским стандартам! Это наблюдается и в традиционных
операционных системах (да, и в Windows тоже), и в веб-приложениях, и в ПО для таких
устройств, как iPhone, iPad, Zune, TiVo, Wii, Xbox, Windows Phone и многих-многих других. Пользователи ожидают от программ большего, а компании-производители тратят
массу времени и денег, чтобы превзойти конкурентов в области разработки
пользовательского интерфейса. И это касается не только программ, рассчитанных на
массового потребителя. Даже бизнес-приложения

30

Почему именно WPF и как насчет Silverlight?

и инструменты для внутреннего использования могут здорово выиграть улучшения
интерфейса.
Однако при возрастании требований к пользовательскому интерфейсу традиционного
подхода и старых технологий разработки приложений часто оказывается недостаточно.
Современные программы обычно нуждаются в быстрой и кардинальном изменении
интерфейса по инициативе различных сторон! профессиональных дизайнеров,
проектировщиков пользовательских интерфейсов или начальства, которое хочет, чтобы
приложение выглядело более эффектно и включало анимацию. Но для этого необходима
технология, позволяющая естественным образом отделить пользовательский интерфейс
от реализации приложения, а визуальное поведение - от внутренней программной
логики. У разработчиков должна быть возможность создавать внешне аскетичные, но
вместе с тем полнофункциональные приложения, которые впоследствии могут быть
красиво оформлены дизайнерами без привлечения программистов. Однако присущий
Win32 стиль программирования, при котором элементы управления содержат код
собственной визуализации, как правило, сильно затрудняет быструю смену интерфейса.
В 2006 году корпорация Microsoft выпустила в свет технологию, которая позволила
разработчикам создавать приложения XXI века, отвечающие возросшим требованиям.
Она называется Windows Presentation Foundation (WPF), С выходом версии WPF 4 в 2010
году эта технология позволила добиваться еще более впечатляющих результатов при
разработке практически любых программ. Всего через десять лет после того, как Том
Круз поспособствовал популяризации идеи компьютера с мультисенсорным
интерфейсом ввода в фильме «Особое мнение» (Minority Report), и после реализации
такого интерфейса в самых разных устройствах (из которых наиболее известен iPhone),
WPF 4 и Windows 7 его применение стало массовым. Голливуду пора придумывать чтонибудь новенькое!

Взгляд в прошлое
Базовые технологии большинства интерфейсов в Windows — интерфейс графического
устройства (Graphics Device Interface, GDI) и подсистема USER - появились в Windows
1.0 еще в 1985 году. В мире технологий это смело можно назвать доисторическим
периодом! В начале 1990-х годов компания Silicon Graphics разработала ставшую
популярной графическую библиотеку OpenGL для двумерной и трехмерной графики как
в Windows, так и в других системах. Она была с восторгом принята компаниями,
работающими в сфере создания систем автоматизированного проектирования, программ
визуализации научных данных и игр. Технология Microsoft DirectX, представленная в
1995 году обеспечила высокоскоростную альтернативу для 2D-графики, ввода, сетевого
взаимодействия, работы со звуком, а со временем и 3D-графики (которая стала
возможной с версией DirectX 2, вышедшей в 1996 году).
Впоследствии и в GDI, и в DirectX было внесено много существенных улучшений.
Например, технология GDI+, представленная в Windows ХР, добавила

Взгляд в прошлое

31

поддержку прозрачности и градиентные кисти. Однако ввиду большой сложности и в
отсутствие аппаратного ускорения она работает медленнее, чем GDI. Что касается
технологии DirectX (кстати, используемой в ХBох), то постоянно выходят новые версии,
раздвигающие пределы возможностей компьютерной графики. После появления каркаса
.NET и управляемого кода (в 2002 году) разработчики получили очень продуктивную
модель для создания Windows и веб-приложений. Включѐнная в неѐ технология Windows
Forms (основанная на GDI+) стала основным способом создания пользовательских
интерфейсов в Windows для разработчиков на С#, Visual Basic и (в меньшей степени)
С++. Она пользовалась успехом и оказалась весьма продуктивной, но имела
фундаментальные ограничения, уходящие корнями в GDI+ и подсистему USER.
Начиная с версии DirectX 9 Microsoft стала поставлять эту систему для управляемого
кода (подобно тому, как в прошлом поставлялись библиотеки специально для Visual
Basic), которая впоследствии была заменена каркасом XNA Framework. Хотя это и
позволило разработчикам на C# использовать DirectX без многих проблем, связанных с
интероперабельностью .NET и СОМ, однако работать с управляемыми каркасами
оказалось не намного проще по сравнению с неуправляемыми альтернативами.
Исключение составляет только разработка игр в среде XNA Framework, поскольку она
включает в себя специализированные для этой цели библиотеки и работаете такими
мощными инструментами, как XNA Framework Content Pipeline и XNA Game Studio
Express.
Поэтому, хотя разработать в Windows почтовую программу с 3D-эффектами (как в
фильме «Разоблачение») можно было уже в середине 90-х годов с помощью
альтернативных GDI технологий (фактически комбинируя DirectX или OpenGL с GDI), на
практике этот способ очень редко применялся даже и десять лет спустя. На то было
несколько причин: аппаратное обеспечение, позволяющее достичь нужных результатов,
было не так распространено вплоть до последнего времени; работать с альтернативными
технологиями на порядок сложнее; и к тому же использование GDI считалось «вполне
приемлемым».
Графические подсистемы компьютеров продолжали совершенствоваться и дешеветь,
ожидания потребителей росли, но до появления WPF проблеме сложности построения
выразительных пользовательских интерфейсов не уделяли должного внимания.
Некоторые разработчики самостоятельно брались за ее решение, стремясь сделать свои
приложения и элементы управления более привлекательными. Простым примером тут
является использование растровых изображений вместо стандартных кнопок. Однако
мало того что подобные нестандартные решения было трудно реализовывать, они еще
зачастую оказывались ненадѐжными. Основанные на них приложения не всегда доступны людям с ограниченными возможностями, плохо адаптируются к высокому
разрешению и имеют другие визуальные огрехи.

32

Почему именно WPF и как насчет Silverlight?

Появление WPF
Корпорация Microsoft понимала, что необходимо нечто совершенно новое свободное от
ограничений GDI+ и подсистемы USER, но не менее продуктивное и удобное в
использовании, чем Windows Forms. И с учѐтом роста популярности
кроссплатформенных приложений, основанных на HTML и JavaScript, Windows отчаянно
нуждалась в столь же простой технологии, которая при этом ещѐ и позволяла бы
задействовать все возможности локального компьютера. И Windows Presentation
Foundation (WPF) дала в руки разработчиков ПО и графических дизайнеров тот
инструмент, который был необходим для создания современных решений и не требовал
освоения сразу нескольких сложных технологий. И хотя слово Presentation
(представление) - всего лишь высокопарный синоним привычного «пользовательского
интерфейса» возможно, оно лучше отражает более высокий уровень визуального
совершенства, которого ждут от современных приложений, равно как и обширную новую
функциональность, включенную в WPF!
Перечислим основные возможности, которые предоставляет WPF.


Широкая интеграция. До WPF разработчикам в Windows, которые хотели
использовать одновременно ЗD-графику, видео, речь и форматированные
документы в дополнение к обычной двумерной графике и элемент управления,
приходилось изучать несколько независимых технологий, плохо совместимых
между собой и не имеющих встроенных средств сопряжения. А в WPF все это
входит в состав внутренне согласованной модели программирования,
поддерживающей композицию и визуализацию разнородных элементов. Одни и те
же эффекты применимы к различным видам мультимедийной информации, а один
раз освоенная техника может использоваться и для других целей.
 Независимость от разрешающей способности. Только представьте себе мир, в
котором переход к более высокому разрешению экрана или принтера не означает,
что все уменьшается. Вместо этого графические элементе и текст только
становятся более четкими! Представьте себе пользовательский интерфейс,
который прекрасно выглядит и на маленьком нетбуке и на полутораметровом
экране телевизора! WPF все это обеспечивает и дает возможность уменьшать или
увеличивать элементы на экране независимо от его разрешения. Это стало
возможным благодаря тому, что WPF основана на использовании векторной
графики.
 Аппаратное ускорение. Поскольку WPF основана на технологии DirectX®, то все
содержимое в WPF-приложении, будь то двумерная или трехмерная графика,
изображения или текст, преобразуется в трехмерные треугольники, текстуры и
другие объекты Direct3D, а потом отрисовываются аппартной графической
подсистемой компьютера. Таким образом, WPF-приложения задействуют все
возможности аппаратного ускорения графики, что позволяет добиться более
качественного изображения и одновременно повысить производительность
(поскольку часть работы перекладывается с центральных процессоров на
графические). При этом от применения

Появление WPF

33

новых графических ускорителей и их драйверов выигрывают все WPF- приложения (а не
только высококлассные игры). Но WPF не требует обязательного наличия
высокопроизводительной графической аппаратуры.
В ней имеется и собственный программный конвейер визуализации. Это позволяет
использовать возможности, которые пока еще не поддерживаются аппаратно (например,
осуществлять высокоточное отображение любого содержимого на экране). Программная
реализация используется и как запасной вариант в случае отсутствия аппаратных
ресурсов (например, если в системе стоит устаревшая графическая карта, или карта
современная, но GPU не хватает ресурсов, скажем, видеопамяти).
 Декларативное программирование. Декларативное программирование не
является уникальной особенностью WPF, поскольку в программах на платформе
Win16/Win32 сценарии описания ресурсов для определения компоновки
диалоговых окон и меню применяются вот уже 25 лет. И в .NET-приложениях
часто используются декларативные атрибуты наряду с конфигурационными и
ресурсными XML-файлами. Однако в WPF применение декларативного
программирования вышло на новый уровень благодаря языку XAML (extensible
Application Markup Language - расширяемый язык разметки приложений)
(произносится «заммел»). Сочетание WPF и XAML аналогично использованию
HTML для описания пользовательского интерфейса, но с гораздо более широкими
выразительными возможностями. И эта выразительность выходит далеко за рамки
описания интерфейса. В WPF язык XAML применяется в качестве формата
документов, для представления 3D-моделей и многого другого. В результате
дизайнер может непосредственно влиять на внешний вид приложения и
некоторые особенности его поведения; раньше для этого, как правило,
приходилось писать код. В следующей главе мы будем рассматривать XAML
подробно.
 Богатые возможности композиции и настройки. В WPF элементы управления
могут сочетаться немыслимыми ранее способами. Можно создать
комбинированный список, содержащий анимированные кнопки, или меню,
состоящее из видеоклипов! Конечно, сама мысль о таком интерфейсе: может
привести в ужас, но важно то, что для оформления элемента способом, о котором
его автор и не помышлял, не понадобится писать никакой(!) код (и в этом
коренное отличие от предшествующих технологий, где отрисовка элементов
жѐстко задавалась создателем кода). Кроме того, отметим, что WPF позволяет
безо всякого труда радикально изменять обложку (скин) приложения (мы
рассмотрим этот вопрос в главе 14 «Стили, шаблоны, обложки и темы»).
Короче говоря, цель WPF — соединить в себе все лучшее, имеющееся в DirectX
(трехмерная графика и аппаратное ускорение), Windows Forms (продуктивность
разработки), Adobe Flash (развитая поддержка анимации) и HTML (декларативная
разметка). Надеюсь, эта книга убедит вас в том, что WPF повышают производительность
труда, дает больше возможностей и более увлекательна, чем любая другая технология, с
которой вам доводилось работать прежде!

34

Почему именно WPF и как насчет Silverlight?

КОПНЕМ ГЛУБЖЕ
GDI и аппаратное ускорение графики
На самом деле в технологии GDI в Windows ХР тоже использовалось аппаратное
ускорение графики. Модель драйвера видеокарты явно поддерживает ускорение
наиболее распространѐнных операций GDI. В Windows Vista реализована новая модель
драйвера видеокарты без аппаратного ускорения примитивов GDI. Вместо этого
применяется программная реализация устройства канонического отображения для
поддержки операций GDI в унаследованных драйверах. Однако в Windows 7
восстановлено частичное аппаратное ускорение для примитивов GDI.

FAQ
Позволяет ли WPF сделать что-то, чего нельзя было сделать ранее?
Если быть совсем точным, то нет. Точно так же ни С#, ни .NET не позволяют сделать
ничего, что нельзя было бы реализовать на языке ассемблера. Вопрос лишь в том,
сколько времени и сил потребуется для достижения желаемого результата!
Если вы попытаетесь создать эквивалент WPF-приложения с нуля без использования
WPF, то придется не только заниматься отрисовкой пикселов на экране и
взаимодействием с устройствами ввода, но также проделать массу дополнительной
работы для поддержки доступности и локализации, тогда как в WPF все это уже
встроено. Кроме того, WPF обеспечивает простой способ задействовать все
возможности Windows 7, например определить списки перехода с помощью
коротенького кода на XAML (см. главу 8 «Особенности Windows 7»).
Поэтому я полагаю, что с учетом времени и финансовых затрат большинство людей
утвердительно ответят на поставленный вопрос.

FAQ
Когда следует использовать DirectX вместо WPF?
DirectX больше подходит для разработчиков зрелищных игр или приложений со
сложными 3D-моделями, в которых требуется максимальная производительность.
Отметим, однако, что очень легко написать такое приложение DirectX, которое будет
работать гораздо медленнее аналогичного WPF-приложения.
DirectX - это низкоуровневый интерфейс к графическому оборудованию, который
делает явными все особенности конкретного графического процессора. DirectX можно
назвать «языком ассемблера в мире графики»: вы можете делать все, что поддерживает
данный GPU, но учитывать капризы разнообразной аппаратуры придется
самостоятельно. Это трудно, зато такой низкоуровневый интерфейс позволяет
опытным разработчикам достичь желаемого компромисса между высоким качеством
графики и скоростью работы. Кроме того, DirectX позволяет работать с последними
достижениями в области GPU, а они появляются гораздо быстрее, чем новые версии
WPF.

Эволюция WPF

35

С другой стороны, WPF обеспечивает более высокий уровень абстракции. Вы
передаете системе описание сцены, а она уже сама решает, как оптимально визуализировать ее с учетом имеющихся аппаратных ресурсов. (Это система, работающая
в режиме запоминания, а не в режиме непосредственной визуализации.) В WPF
основное внимание уделено двумерной графике, а трехмерная графика ограничена
сценариями визуализации данных и интеграцией с 2D без претензии на полную
поддержку всех возможностей DirectX.
Однако, отдавая предпочтение DirectX, следует иметь в виду потенциально астрономическое увеличение стоимости разработки. По большей части затраты связаны с
необходимостью тестировать приложение для всех возможных комбинаций драйверов
и GPU, которые вы намереваетесь поддерживать. Одно из основных преимуществ
построения приложения на базе WPF состоит в том, что Microsoft уже провела такое
тестирование за вас! Вы же можете сосредоточиться на измерении
производительности, для чего достаточно относительно дешевой системы. А тот факт,
что WPF-приложение способно использовать установленный на компьютере
пользователя GPU даже в условиях частичного доверия, - еще один аргумент в пользу
выбора этой технологии.
Отметим также, что WPF и DirectX можно использовать совместно в одном приложении. В главе 19 «Интероперабельность с другими технологиями» описано, как это
делается.

Эволюция WPF
Как ни странно, WPF 4 действительно является четвертой основной версией WPF.
Странность в том, что первый выпуск имел номер 3.0! Он увидел свет в ноябре 2006 года
и получил название WPF 3.0, потому что вошел в состав каркаса .NET Framework 3.0.
Второй выпуск - WPF 3.5 - состоялся почти год спустя (если быть точным, то на день
раньше). Третья основная версия вышла еще через год (в августе 2008 года). Она была
включена в пакет обновлений Service Pack 1 (SP1) для .NET 3.5, но в части WPF это был
не обычный пакет обновлений, поскольку появилось много новых возможностей и
улучшений.
Кроме основных версий в августе 2008 года на сайте http://wpf.codeplex.com Microsoft
представила набор инструментов WPF Toolkit, который содержит многочисленные
инструментальные средства и примеры использования и обновляется несколько раз в год.
WPF Toolkit предназначен для ускоренного внедрения новых возможностей, правда, в
экспериментальной форме (и зачастую вместе с исходным кодом). Нередко средства,
впервые появившиеся в WPF Toolkit, со временем включаются в новые версии WPF — в
зависимости от мнения пользователей об их желательности и степени готовности.
На момент выпуска первой версии для WPF не существовало практически никакой
инструментальной поддержки. В последующие месяцы были написаны примитивные
WPF-расширения для Visual Studio 2005, а затем состоялся первый выпуск Expression
Blend для публичного ознакомления. Теперь же среда Visual Studio 2010 включает не
только полноценную поддержку WPF, но и сама была существенно переписана и ныне
является WPF-приложением!

36

Почему именно WPF и как насчет Silverlight?

Программа Expression Blend, полностью написанная средствами WPF, также получила
множество новых функций для проектирования и создания проток) типов замечательных
пользовательских интерфейсов. Кроме того, за последние несколько лет немало WPFприложений было выпущено такими компаниями, как Autodesk, SAP, Disney, Blockbuster,
Roxio, AMD, Hewlett Packard, Lenovo и многими другими. Сама корпорация Microsoft,
конечно же, тоже разработала многочисленные приложения, основанные на WPF (Visual
Studiо Expression, Test and Lab Manager, Deep Zoom Composer, Songsmith Surface, Semblio,
Robotics Studio, LifeCam, Amalga, Games for Windows LIVE Marketplace. Office
Communicator Attendant, Active Directory Administrative Centera Dynamics NAV, Pivot,
PowerShell ISE и многие другие).

СОВЕТ
Чтобы узнать, какие WPF-элементы применяются в конкретном WPF-приложении,
можно
воспользоваться
программой
Snoop,
представленной
на
сайте
http://snoopwpf.codeplex.com.

Давайте более подробно рассмотрим, как WPF менялась со временем.

Усовершенствования в WPF 3.5 и WPF 3.5 SP1
В версиях WPF 3.5 и 3.5 SP1 произошли следующие существенные изменения:
 Интерактивная 3D-графика - интеграция двумерной и трехмерной графики
улучшилась благодаря базовому классу UIElement3d, который позволил 3Dэлементам принимать данные, получать фокус клавиатуры и события классу со
странным названием Viewport2DVisual3D, который позволил помещать любой
интерактивный 2D-элемент управления на 3D-сцену; и другим нововведениям.
Подробнее см. в главе 16 «Трехмерная графика*.
 Полноценная интероперабельность с DirectX - ранее WPF-приложение могло
взаимодействовать с DirectX только на уровне общей для обеих технологий
платформы Win32. Теперь с помощью класса D3DImage можно работать
непосредственно с поверхностью Direct3D, а не с ее описателями HWND. Среди
прочего это позволяет размещать WPF-содержимое поверх DirectX-содержимого и
наоборот. См. главу 19.
 Улучшенная привязка к данным - в WPF появилась поддержка технологии
привязки XLINQ, улучшены способы контроля данных и отладки, а также
форматирование выводимых строк средствами XAML, что позволяет в ряде
случаев обойтись без написания процедурного кода. См. главу 13 «Привязка к
данным».
 Улучшенные спецэффекты - уже самая первая версия WPF поставлялась, с
полезными спецэффектами для рисунков (размывание, тени, внешнее свечение,
выдавливание и выпуклость), но пользоваться ими не рекомендовалось

Эволюция WPF

37

из-за крайне низкой производительности. Теперь все изменилось - добавлен новый набор
эффектов с поддержкой аппаратного ускорения, а также реализована совершенно иная
архитектура, позволяющая подключать собственные эффекты с аппаратным ускорением
с помощью пиксельных построителей текстур (pixel shaders). См. главу 15 «Двумерная
графика».










Высокопроизводительное произвольное рисование - раньше в WPF не имелось
хорошего механизма произвольного рисования, масштабируемого на тысячи точек
или фигур, поскольку даже примитивы самого низкого уровня были для этого
слишком медленны. Теперь класс WriteableBitmap модернизирован таким
образом, что при рисовании можно указывать изменившиеся области, а не
обновлять весь растр в каждом кадре! Впрочем, поскольку этот класс позволяет
только задавать отдельные пикселы, то рисованием такой процесс можно назвать с
большой натяжкой.
Улучшения в области обработки текста — повышена производительность,
улучшена поддержка интернационализации (усовершенствован редактор методов
ввода (IME) и улучшена поддержка индийских языков). Модернизации
подверглись также классы TextBox и RichTextBox. См. главу 11 «Изображения,
текст и другие элементы управления».
Модернизация приложений с частичным доверием - .NET-приложениям с
частичным доверием стало доступно гораздо больше функциональности,
например, возможность обращаться к WCF (Windows Communication Foundation)
для вызова веб-служб (с привязкой basicHttpBinding) и возможность читать и
записывать файлы cookie. Кроме того, технология XAML Browser Applications
(XBAPs) - основной механизм запуска WPF-приложений с частичным доверием, ранее доступная только для Internet Explorer, теперь распространена и на браузер
Firefox (но необходимая для этого надстройка больше не устанавливается по
умолчанию).
Улучшенное развертывание приложений и каркаса .NET - проявляется в
различных формах: ускорение процедуры установки и уменьшение объема .NET
за счет внедрения технологии «клиентского профиля», которая позволяет
исключить части .NET, нужные только для серверов (например, ASP.NET); новый
компонент-«загрузчик», который обрабатывает зависимости .NET, ранее
установленные компоненты и появившиеся обновления и позволяет осуществлять
установку нестандартных дистрибутивов; а также разнообразные новые
возможности в технологии ClickOnce.
Улучшенная производительность - в WPF и в общеязыковой среде исполнения
реализован ряд изменений, которые существенно повысили скорость исполнения
WPF-приложений без каких либо изменений в коде. Например, заметно
уменьшилось время загрузки приложения (особенно первой), анимация (особенно
медленная) стала гораздо более плавной, привязка данных в некоторых ситуациях
начала работать быстрее, а полупрозрачные окна (описанные в главе 8) теперь
поддерживают аппаратное ускорение. Имеются и другие усовершенствования в
области производительности, которые пользователь должен включать
самостоятельно из-за ограничений

38

Почему именно WPF и как насчет Silverlight?

совместимости, например улучшенная виртуализация и отложенная прокрутка в
многодетных элементах управления (см. главу 10 «Многодетные I элементы
управления»).

Усовершенствования в WPF 4
В версию WPF 4 дополнительно внесены следующие изменения:
 Поддержка мультисенсорного ввода - на компьютерах с поддержкой
мультисенсорного ввода, работающих под управлением ОС Windows 7 или более
поздней, элементы WPF могут получать различные события ввода:
низкоуровневые данные, простые преобразования (например, поворот и
масштабирование) или высокоуровневые (в том числе нестандартные) жесты. Все
встроенные в WPF элементы управления модернизированы для работы с
мультисенсорными устройствами ввода. Разработчики WPF воспользовались
результатами работы над проектом Microsoft Surface (который в свою очередь,
основан на WPF). В итоге поддержка мультисенсорного ввода в WPF 4
совместима с версией 2 Surface SDK, что стало отличной новостью для всех, кто
собирался разрабатывать приложения для Windows и Surface. См. главу 6
«События ввода: клавиатура, мышь, стилус и мультисенсорные устройства».
 Полноценная поддержка прочих возможностей Windows 7 - мультисенсорный
ввод - конечно, ценное добавление в Windows 7, но есть и много других, не
требующих наличия специального оборудования и, следовательно, доступных
широкому кругу пользователей. WPF обеспечивая оптимальный способ
интеграции приложений с такими новыми возможностями панели задач, как
списки переходов (Jump List) и многослойные значки (icon overlays), а также с
обновленными стандартными диалогов» ми окнами и многими другими
нововведениями. См. главу 8.
 Новые элементы управления - WPF 4 включает такие элементы управления, как
DataGrid, Calendar и DatePicker, которые первоначально появились в WPF Toolkit.
См. главу 11.
 Новые функции для анимации переходов - появилось одиннадцать новых
классов анимации, в том числе BounceEase, ElasticEase и SineEase, которые
позволяют декларативно задавать замысловатые траектории анимации с
настраиваемым ускорением и замедлением. Эти «переходные функции» и
поддерживающая их инфраструктура впервые были представлены в Silverlight 3 и
только потом вошли в WPF 4.
 Улучшенная стилизация с помощью Visual State Manager - менеджер
визуальных состояний впервые появился в Silverlight 2; он предлагает новый
способ организации визуальных элементов и их интерактивного поведения: в виде
«визуальных состояний» и «переходов между состояниями». Это упрощает
дизайнерам работу с элементами управления в таких инструментальных средах,
как Expression Blend, а также позволяет создавать общие шаблоны для WPF и
Silverlight.

Эволюция WPF








39

Улучшенное поведение на границе пикселов - WPF всегда стремилась соблюдать баланс между независимостью от разрешающей способности устройства
(для чего требуется игнорировать границы физических пикселов) и обеспечением
четкости визуальных элементов (что предполагает привязку к границам пикселов,
особенно для мелких элементов). С самого начала WPF поддерживала свойство
SnapsToDevicePixels, позволяющее принудительно осуществлять «привязку
элементов к пикселам». Но использовать его было довольно сложно, а в
некоторых случаях оно не давало никакого эффекта. Тогда в Silverlight был сделан
шаг назад, в направлении обычной чертежной доски, и реализовано свойство
UseLayoutRounding, которое работало более естественным образом. Теперь это
свойство появилось и в WPF 4. Если задать его равным true для корневого
элемента, то координаты этого элемента и всех его потомков будут округляться (с
недостатком или с избытком), так чтобы они совпали с границами ближайших
пикселов. В результате пользовательский интерфейс сохраняет способность к
масштабированию, оставаясь при этом четким!
Более четкий текст - стремление WPF обеспечить независимость от разрешающей способности устройства и масштабируемость интерфейса всегда
терпело неудачу при отображении небольших фрагментов текста, которые
превалируют в традиционных интерфейсах на экранах с разрешением 96 точек на
дюйм (DPI). Это очень огорчало многих пользователей и разработчиков. Я даже
говорил, что всегда смогу отличить интерфейс, созданный с помощью WPF,
просто по размытости текста. В WPF 4 разработчики наконец-то решили эту
проблему, предложив альтернативный способ визуализации текста, при котором
текст выглядит так же четко, как выведенный с помощью GDI, но с сохранением
почти всех преимуществ WPF. Например, этот режим используется в Visual Studio
2010 для отображения текстовых документов. Но поскольку новый способ
визуализации имеет ряд ограничений, то включать этот режим следует явно. См.
главу 11.
Усовершенствования в области развертывания приложений - теперь
клиентский профиль .NET может устанавливаться на одной машине с полным
каркасом .NET и использоваться почти во всех сценариях, характерных для WPFприложений. На самом деле проекты для .NET 4.0 в Visual Studio 2010 по
умолчанию ориентированы на более компактный клиентский профиль.
Дальнейшее повышение производительности - для максимального ускорения
векторной графики WPF может кэшировать результаты рендеринга в виде
растровых изображений и затем использовать их повторно. Этим поведением
можно управлять с помощью свойства CacheMode; см. главу 15. Стимулом для
многочисленных улучшений в области производительности послужило то, что
WPF активно используется в Visual Studio 2010, но ощутить эффект теперь могут
все WPF-приложения.

40

Почему именно WPF и как насчет Silverlight?

FAQ
Что будет добавлено в WPF после версии 4?
Во время написания этой книги никаких анонсов еще не было, но без особого риска
можно предположить, что повышение производительности и дальнейшее сближение с
Silverlight по-прежнему будут оставаться в центре внимания разработчиков WPF.
Дополнительным источником информации о том, что может быть включено в ядро,
служит WPF Toolkit. Речь может идти об элементах управления для построения
графиков, а также об элементах BreadcrumbBar, NumericUpDown и др.

FAQ
Зависит ли поведение WPF от версии Windows?
Среди прочего в WPF реализованы API, которые относятся только к Windows 7 (или
более поздним версиям), в частности мультисенсорный ввод и другие функции,
описанные в главе 8. Кроме того, WPF ведет себя несколько иначе при запуске в
Windows ХР (самой старой версии Windows, которую поддерживает WPF). Например,
не производится сглаживание для ЗБ-объектов.
И, конечно же, для элементов управления WPF по умолчанию применяются разные
темы - в зависимости от операционной системы (Aero в Windows Vista и Windows 7,
Luna в Windows ХР).
Кроме того, в Windows ХР используется старая модель драйверов, что может негативно сказаться на работе WPF-приложений. Модель драйверов в последних версиях
Windows подразумевает виртуализацию и распределение ресурсов графических
процессоров, что повышает общую производительность системы при исполнении
нескольких программ, активно работающих с графикой. Запуск нескольких
приложений WPF или DirectX в Windows ХР может затормозить систему, но в более
поздних версиях Windows таких проблем быть не должно.

Что такое Silverlight
Silverlight - это компактная, облегченная версия каркаса .NET, рассчитанная на
насыщенные веб-приложения (в качестве альтернативы Adobe Flash и Flex). Подход
Silverlight к созданию пользовательских интерфейсов такой же, как WPF, что дает массу
преимуществ. Первая версия Silverlight вышла в 2007 году, а теперь, как и WPF, она
находится на уровне четвертой версии. Silverlight 4 была выпущена в апреле 2010 года,
через несколько дней после выхода WPF
Взаимоотношения между WPF и Silverlight выглядят несколько «запутанно» как и
вопрос о том, когда применять одну технологию, а когда другую. Еще больше осложняет
ситуацию тот факт, что WPF-приложения можно запускать в браузере (с помощью
технологии XBAPs), то есть они практически «готовы для Сети». И наоборот,
приложения Silverlight можно запускать вне браузера, даже в автономном режиме.

Что такое Silverlight

41

По существу, Silverlight является подмножеством WPF и дополнительно включает
несколько фундаментальных классов из каркаса .NET Framework (встроенные типы
данных, классы коллекций и т. д.). Каждая новая версия Silverlight содержит все больше
функциональности WPF. И хотя совместимость WPF и полным каркасом .NET попрежнему является целью разработчиков Silverlight, они не упустили возможность учесть
ошибки, допущенные при создании WPF и .NET. Внесены некоторые изменения и
поддерживаются возможности, которые еще не вошли в полный каркас .NET. Кое-какие
изменения и дополнения впоследствии были включены в состав WPF и .NET Framework
(например, Visual State Manager и привязка элементов к границам пикселов), другие - нет
(видеокисти и построение перспективы). И наоборот, в WPF и .NET есть такие функции,
которые Silverlight, вероятно, никогда не будет поддерживать.
Короче говоря, следует задавать не вопрос «Что использовать: WPF или Silverlight?», а
вопрос «Использовать полный каркас .NET или облегченный?». Если требуется
функциональность, которая существует только в полной версии .NET, то выбор
очевиден. В таком случае рекомендуется использовать WPF. Напротив, если необходима
возможность выполнять приложение на компьютерах Мас или устройствах, отличных от
стандартного ПК, то ответ тоже ясен - выбирайте Silverlight. На любой платформе
Silverlight обеспечивает единую технологию построения интерфейса (хотя прекрасно
взаимодействует с HTML). Во всех остальных случаях выбор зависит от природы
приложения и целевой аудитории.
В идеале было бы желательно отложить решение о выборе конкретной технологии.
Хотелось бы использовать один и тот же исходный код (и даже скомпилированные
двоичные файлы) и иметь возможность легко адаптировать приложение к параметрам
устройства, на котором оно выполняется, - будь то мобильное устройство либо обычный
ПК под управлением Windows или Мас. Возможно, когда-нибудь так и будет, но пока
создание единого кода, который работал бы и в WPF, и в Silverlight, требует
дополнительной работы. Обычно создают совместимый с Silverlight код, в котором
части, специфичные для .WPF, обрамлены директивами #ifdef. Это позволяет
компилировать разные версии приложения для Silverlight и для WPF, сводя расхождения
в исходном коде к минимуму.
Лично я ожидаю (и надеюсь), что различия между WPF и Silverlight со временем будут
стираться. Конечно, Silverlight звучит гораздо выразительнее, чем Windows Presentation
Foundation, но само наличие двух названий порождает определенные проблемы и
искусственные различия. Гораздо продуктивнее рассматривать Silverlight и WPF как две
реализации одной и той же базовой технологии. На самом деле в корпорации Microsoft
эти продукты разрабатываются практически одни и теми же людьми. И Microsoft
постоянно говорит о построении «клиентского континуума», который позволил бы
создавать приложения для любой платформы и устройства программистам, обладаю-

42

Почему именно WPF и как насчет Silverlight?

щим определенными навыками (о которых вы узнаете в этой книге), с помощью одних и
тех же инструментов (Visual Studio, Expression Blend и т. д.) При этом желательно, чтобы
если не двоичный, то хотя бы исходный код (на .NET совместимом языке типа C# или
VB в сочетании с XAML) был один и тот же. Назвать эту книгу «WPF и Silverlight.
Полное руководство» было бы, пожалуй, чересчур смело, но рад сообщить, что знания,
которые вы получите, прочитав ее, сделают вас специалистом и по WPF, и по Silverlight.

Резюме
В последнее время растет число программ, имеющих высококачественный интерфейс иногда даже как в кино, - а те, что не следуют этой тенденции, рискуют показаться
старомодными. Однако еще не так давно создание подобного интерфейса (особенно в
Windows) требовало значительных усилий.
Технология WPF существенно упрощает создание любых пользовательских
интерфейсов, будь то традиционное Windows-приложение или иммерсивная (создающая
эффект присутствия) трехмерная программа, сравнимая с летним блокбастером. При этом
впечатляющий интерфейс может создаваться относительно независимо от остального
приложения, что позволяет дизайнерам гораздо эффективнее участвовать в процессе
разработки приложения. Но не ограничивайтесь мечтами. Читайте дальше, и вы
научитесь все это делать!

2













Все тайны XAML
Определение XAML
Элементы и атрибуты
Пространства имен
Элементы свойств
Конвертеры типов
Расширения разметки
Дочерние объектные элементы
Сочетание XAML и процедурного кода
Введение в XAML2009
Трюки с классами чтения и записи XAML
Ключевые слова XAML

Язык XML активно используется в технологиях .NET для выражения различной
функциональности в ясном, декларативном стиле. В этом смысле языку XAML
(диалекту XML), появившемуся уже в первой версии WPF в 2006 году, отведена особая
роль. Часто его неправильно считают всего лишь средством описания пользовательского
интерфейса, чем-то вроде HTML. Однако, прочитав эту главу, вы поймете, что XAML
может гораздо больше, чем просто расставлять элементы управления на экране.
В технологиях WPF и Silverlight XAML действительно применяется главным образом
для описания пользовательского интерфейса (но не только). Однако в Windows Workflow
Foundation (WF) и Windows Communication Foundation (WCF) язык XAML используется
для описания операций и конфигураций, которые не имеют ничего общего с
пользовательскими интерфейсами.
Основное назначение XAML заключается в том, чтобы предоставить программистам и
специалистам в других областях возможность совместной работы. То есть XAML
становится единым языком общения, как правило, опосредованным инструментами
разработки и предметно-ориентированными средствами проектирования. Но поскольку
язык XAML (и вообще XML) может восприниматься человеком, то с ним можно
работать, даже не имея ничего, кроме редактора Блокнот.
В контексте WPF и Silverlight «специалистами в предметной области» являются
графические дизайнеры, которые могут с помощью таких средств разработки, как
Expression Blend, создавать элегантные пользовательские интерфейсы независимо от
программистов, пишущих код приложения. Такая кооперация дизайнеров и
программистов стала возможной не только благодаря общему языку XAML, но и
потому, что значительная часть функциональности, обеспечиваемой различными API,
сделана доступной для декларативного объявления. В результате для использования всей

44

Все тайны XAML

выразительной мощи средств проектирования (например, для описания сложных
анимаций переходов состояний) не требуется писать процедурный код.
Но, даже если вы не планируете работать совместно с дизайнерами, все равно стоит
ознакомиться с XAML по следующим причинам:
 Язык XAML может стать очень компактным средством описания
пользовательского интерфейса и других иерархий объектов.
 XAML позволяет легко отделить внешний вид приложения от его внутренней
логики, что сильно упрощает последующее сопровождение, даже если команда
разработчиков состоит всего из одного человека.
 Код на XAML легко скопировать в различные средства разработки, например
Visual Studio, Expression Blend или какую-нибудь небольщую автономную
программу, и сразу увидеть результат без какой либо компиляции.
 Именно код XAML генерируют практически все средства разработки, связанные с
WPF.
В этой главе мы подробно рассмотрим структуру и синтаксис языка ХАМL и покажем,
как он соотносится с процедурным кодом. В отличие от предыдущей главы, погружение в
тему будет по-настоящему глубоким. Располагая этими основополагающими знаниями,
вы сможете не только читать примеры кода, но и понять, почему API различных классов
спроектирован именно так, а не иначе. Это понимание будет полезно также при
разработке
WPF-приложений,
элементов
управления,
библиотек
классов,
поддерживающих ХАМL или инструментальных средств, получающих на входе или
порождающих на выходе XAML-код (например, программы проверки и локализации,
конвертеры форматов, конструкторы и т. д.).

СОВЕТ
Существует весколько способов выполнить написанные на XAML примеры из данной
главы (их код можно скачать в электронном виде вместе с прочим исходным кодом,
прилагаемым к книге), например:
 Сохраните текст примера в файле с расширением .xaml и откройте его в Internet
Explorer (для Windows Vista или более поздней либо для Windows ХР с установленным каркасом .NET 3.0 или более поздней версией). Можно также
использовать Firefox, если установлено соответствующее дополнение. Но по
умолчанию браузер будет использовать версию WPF, установленную вместе с
операционной системой, а не WPF 4.
 Скопируйте текст примера в какую-нибудь простую инструментальную
программу, например XAMLPAD2009, предлагаемую вместе с исходным
кодом к этой главе, или Kaxaml (http://kaxaml.com), хотя на момент написания
книги последняя еще не была модернизирована для работы с WPF 4.
 Создайте WPF-проект в Visual Studio и замените содержимое главного окна
Window или элемента Раgе текстом примера; иногда может понадобиться внести
в код некоторые изменения.
Первые два варианта открывают замечательную возможность ознакомиться с XAML и
немного поэкспериментировать. Сочетание XAML с другим содержимым в проекте
Visual Studio рассматривается в конце этой главы.

ОпределениеXAML

45

FAQ
Что случилось с XamlPad?
В состав прежних версий Windows SDK входила простая программа XamlPad, которая
позволяла вводить (или копировать) WPF-совместимый код на XAML и смотреть, как
он визуализируется в виде пользовательского интерфейса. К сожалению, это средство
больше не поставляется ввиду недостатка ресурсов. (Да, несмотря на распространенное
убеждение, ресурсы Microsoft не безграничны!) Однако есть несколько альтернативных
инструментов для экспериментов с XAML, в том числе:
 XAMLPAD2009 - включен в состав исходного кода к данной книге. В нем нет
изысков, имеющихся в других инструментах, зато прилагается исходный код. К
тому же, это единственная программа, поддерживающая версию XAML2009
(описанную далее в этой главе).
 Kaxaml - удобный инструмент, написанный Робби Ингебретсеном (Robby Ingebretsen), бывшим членом команды разработчиков WPF; доступен по адресу
http://kaxaml.com.
 XamlPadX - довольно развитая программа, доступная по адресу
http://blogs.msdn.com/llobo/archive/2008/08/25/xamlpadx-4-0.aspx.
Автором
является Лестер Лобо (Lester Lobo), один из разработчиков WPF.
XAML Cruncher - ClickOnce-приложение, которое можно найти на странице
http://charlespetzold.com/wpf/XamlCruncher/ XamlCruncher.application. Разработано
Чарльзом Петцольдом (Charles Petzold), очень плодовитым автором книг и блогером.

Определение XAML
XAML - сравнительно простой декларативный язык программирования общего
назначения, предназначенный для конструирования и инициализации объектов. На самом
деле XAML представляет собой диалект XML с добавлением ряда правил, относящихся к
элементам, атрибутам и их отображению на объекты, их свойства и значения свойств
(помимо всего прочего).
Поскольку XAML — это просто механизм для использования различных API каркаса
.NET, все попытки сравнения его с HTML, SVG (Scalable Vector Graphics) и другими
предметно-ориентированными языками и форматами некорректны. XAML содержит
правила интерпретации XML синтаксическими анализаторами и компиляторами, а также
ряд ключевых слов, но сам по себе он не определяет никаких существенных элементов.
Поэтому говорить о XAML вне связи с конкретной средой типа WPF - все равно что
обсуждать C# вне каркаса .NET Framework. Вместе с тем Microsoft формализовала
понятие «словарей XAML», то есть наборов допустимых элементов для различных
областей. Таким образом, можно говорить о WPF XAML, о Silverlight XAML и о других
типах XAML-файлов.

46

Все тайны XAML

КОПНЕМ ГЛУБЖЕ
Спецификации XAML и словарей XAML
Подробные спецификации XAML и двух словарей XAML можно найти по следующим
адресам:
• XAML
Object
Mapping
Specification
2006
(MS-XAML):
http://go.microsoft.com/fwlink/?LinkId=130721
• WPF
XAML
Vocabulary
Specification
2006
(MS-WPFXV):
http://go.microsoft.com/fwlink/?LinkId=130722
• Silverlight
XAML
Vocabulary
Specification
2008
(MS-SLXV):
http://go.microsoft.com/fwlink/?LinkId—130707

Соотношение между XAML и WPF часто понимают неправильно, поэтому важно
подчеркнуть, что WPF и XAML могут использоваться независимо друг от друга. И хотя
XAML изначально был разработан именно для WPF, сейчас он используется и в других
технологиях. Ведь благодаря своей универсальности XAML может применяться
практически в любой объектно-ориентированной технологии. Более того, использовать
XAML в WPF-проектах необязательно. Практически все, что вы способны сделать с
помощью XAML, можно реализовать на любом процедурном .NET-совместимом языке
(обратное не верно). Однако из-за тех преимуществ XAML, которые были упомянуты в
начале этой главы, трудно встретить реальный WPF-проект, где этот язык не
используется.

КОПНЕМ ГЛУБЖЕ
Функциональность XAML, недоступная из процедурного кода
Существует ряд приемов, которые можно выполнить на XAML, но не посредством
процедурного кода. Они малоизвестны и более подробно рассматриваются в главах 12
и 14.
•
Создание полного набора шаблонов. В процедурном коде можно создавать
шаблоны с помощью класса FrameworkElementFactory, но выразительные возможности этого подхода ограничены.
•
Использование конструкции x:Shared=‖False‖, заставляющей WPF
возвращать новый экземпляр при каждом обращении к элементу из словаря
ресурсов.
•
Отложенное создание объектов внутри словаря ресурсов. Это важно для
оптимизации производительности и доступно только с помощью
скомпилированного XAML-кода.

ОпределениеXAML

47

Элементы и атрибуты
В спецификации XAML определены правила отображения пространств имен, типов,
свойств и событий .NET на пространства имен, элементы и атрибуты XML. Они
иллюстрируются ниже на примере простого (но полного) XAML- файла, где
определяется WPF-объект Button, и эквивалентного ему кода на С#:
XAML:


52

Все тайны XAML

Рис. 2.2. Кнопка Button со сложным содержимым
Теперь свойство Content устанавливается с помощью XML-элемента, а не XMLатрибута, а результат эквивалентен коду на С#. Точка в выражении Button.Content
позволяет отличить элемент свойства от объектного элемента. Элементы свойств всегда
имеют вид ИмяТипа.ИмяСвойства и обязательно вложены в объектный элемент вида
ИмяТипа. У элементов свойств не может быть собственных атрибутов (за одним
исключением - атрибут х:Uid используется для локализации).
Синтаксис элементов свойств можно использовать и для простых значений свойств. В
следующем примере для Button с помощью атрибутов устанавливаются два свойства
(Content и Background).


Конечно, при вводе XAML-кода вручную лучше использовать форму с атрибутами - это
короче.

Конвертеры типов
Рассмотрим С#-код, эквивалентный предыдущему объявлению Button: установка
свойств Content и Background:

Конвертеры типов

53

System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = "OK";
b.Background = System.Windows.Media.Brushes.White;

Минуточку! Как это строка White в показанном выше XAML-файле может быть
эквивалентна статическому полю System. Windows. Media. Brushes. White (типа
System.Windows.Media.SolidColorBrush)? Действительно, здесь мы воспользовались
одной хитростью, позволяющей с помощью строк в XAML-коде задавать значения
свойств, тип которых отличается от System.String или System.Object. В таких случаях
компилятор или анализатор XAML должен найти конвертер типа, который умеет
преобразовывать строковое значение в нужный тип.
WPF предоставляет конвертеры типов для многих часто используемых типов данных:
Brush, Color, FontWeight, Point и т.д. Все это - классы, производные от TypeConverter
(BrushConverter, ColorConverter и т.д.) Вы можете написать собственный конвертер для
произвольного типа данных. В отличие от самого языка XAML, конвертеры типов
обычно не чувствительны к регистру символов.
Если бы не существовало конвертера типа Brush, то в XAML-коде для пришивания
свойству Background значения пришлось бы применить синтаксис элементов свойств:


Но даже это возможно только потому, что конвертер типа для Color умеет интерпретировать строку "White". Если бы такого конвертера не было, пришлось бы писать
следующий код:


Ho и это возможно лишь благодаря наличию конвертера типа, который умеет
преобразовывать строку "255" в значение типа Byte, ожидаемое свойствами A, R, G и В
типа Color. Не будь его, мы оказались бы в тупике. Конвертеры типов не только делают
XAML-код понятнее, но и позволяют записывать значения, которые иначе записать было
бы невозможно.

54

Глава2. Все тайны XAML

КОПНЕМ ГЛУБЖЕ
Использование конвертеров типов в процедурном коде
Хотя код на С#, в котором свойству Background присваивается значение System.
Windows. Media. Brushes.White, дает тот же результат, что XAML-код, где этому свойству присваивается строка "White", механизм преобразования типов в нем не
применяется. Ниже показан код, который более точно отражает действия среды
выполнения, когда она ищет и выполняет подходящий конвертер типа:
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = "OK";
b.Background = (Brush)System.ComponentModel.TypeDescriptor.GetConverter(
typeof(Brush)).ConvertFromInvariantString("White");

В отличие от предыдущего кода на С#, опечатка в слове "White" не приведет к ошибке
компиляции, но вызовет исключение во время выполнения, как и в случае XAML.
(Хотя Visual Studio на этапе компиляции XAML-кода предупреждает об ошибках
такого рода.)

КОПНЕМ ГЛУБЖЕ
Поиск конвертеров типов
А как все-таки компилятор или анализатор XAML находит подходящий конвертер типа
для значения свойства? Он смотрит, снабжено ли определение данного свойства или
определение
типа
данных
этого
свойства
атрибутом
System.
ComponentModel.TypeConverterAttribute.
Например, при установке свойства Background кнопки Button в XAML-коде применяется конвертер типа BrushConverter, поскольку свойство Background имеет тип
System.Windows.Media.Brush, в определении которого задан следующий атрибут:
[TypeConverter(typeof(BrushConverter)), …]
public abstract class Brush : …
{
...
}

С другой стороны, для установки свойства FontSize кнопки используется конвертер
типа FontSizeConverter, потому что это свойство (определенное в базовом классе
Control) снабжено следующим атрибутом:
[TypeConverter(typeof(FontSizeConverter)), …]
public double FontSize
{
get { … }
set { … }
}

В этом случае конвертер типа необходимо соотносить именно со свойством, поскольку
его тип данных (double) слишком общий и всегда ассоциировать его с FontSizeConverter
неразумно. На самом деле в WPF тип double часто ассоциируется с другим конвертером
типа - LengthConverter.

Расширение разметки

55

Расширения разметки
Расширения разметки, как и конвертеры типов, позволяют улучшить выразительность
языка XAML. Оба механизма могут интерпретировать строковые атрибуты во время
выполнения (за исключением нескольких встроенных расширений разметки, которые в
настоящее время вычисляются во время компиляции из соображений
производительности) и создавать объекты, соответствующие строкам. Помимо
стандартных конвертеров типов в дистрибутиве WPF имеется несколько встроенных
расширений разметки.
Но, в отличие от конвертеров типов, для расширений разметки в XAML предусмотрен
явный логичный синтаксис. Поэтому именно последним следует отдать предпочтение.
Кроме того, расширения разметки позволяют обойти потенциальные ограничения,
присущие существующим конвертерам типов, изменить которые вы не в силах.
Например, с помощью расширения разметки можно установить в качестве фона элемента
управления градиентную кисть, заданную в виде строки, хотя встроенный конвертер
BrushConverter не умеет этого делать.
Если значение атрибута заключено в фигурные скобки {}, то компилятор или анализатор
XAML считает его значение расширением разметки, а не обычной строкой (или чем-то,
нуждающимся в конвертере типов). В показанном ниже элементе Button используется
три разных расширения разметки в трех различных свойствах:

Класс расширения разметки

Первый идентификатор в каждом заключенном в фигурные скобки значении - имя класса
расширения разметки, который должен наследовать классу MarkupExtension. По
принятому соглашению имена таких классов оканчиваются словом Extension, но в XAML
его можно опускать. В данном примере Null- Extension (записано в виде x:Null) и
StaticExtension (записано в виде x:Static) - классы из пространства имен
System.Windows.Markup, поэтому для их поиска необходимо указывать префикс х. Но
класс Binding (имя которого не оканчивается словом Extension) находится в пространстве
имен System.Windows.Data, поэтому его следует искать в пространстве имен XML,
подразумеваемом по умолчанию.
Если расширение разметки поддерживает такой синтаксис, ему можно передавать
параметры,
разделенные
запятой.
Позиционные
параметры
(например,
SystemParameters.IconHeight) рассматриваются как строковые аргументы для
соответствующего конструктора класса расширения. Именованные параметры

56

Глава2. Все тайны XAML

(в данном примере Path и RelativeSource) позволяют устанавливать в конструируемом
объекте расширения разметки свойства с соответствующими именами. Значением такого
свойства может быть еще одно расширение разметки (задается с помощью вложенных
фигурных скобок, как в случае RelativeSource) или литерал, который можно подвергнуть
обычной процедуре конвертации типов. Если вы знакомы с атрибутами .NET (это
популярный механизм расширения каркаса), то, возможно, заметили что синтаксис и использование расширений разметки в XAML очень напоминает способ определения
атрибутов. Это неслучайно.
В предыдущем объявлении элемента Button расширение NullExtension позволяет в
качестве кисти Background (свойство Background) задавать null, хотя конвертер типа
BrushConverter (и, кстати, многие другие конвертеры) такую возможность не
поддерживает. Это сделано только для примера, поскольку фон null на практике
бесполезен. Расширение StaticExtension позволяет использовать в XAML статические
свойства, поля, константы и элементы перечисления вместо явного прописывания
литералов. В данном случае высота Height кнопки Button устанавливается равной
текущей высоте значков в операционной системе, которую можно получить с помощью
статического свойства IconHeight класса System.Windows.SystemParameters. Расширение
Binding подробно рассматривается в главе 13 «Привязка к данным». Оно позволяет
присвоить свойству Content значение, равное значению свойства Height.

КОПНЕМ ГЛУБЖЕ
Экранирование фигурных скобок
Если строковое значение атрибута свойства начинается открывающей фигурной
скобкой, то ее следует экранировать, чтобы анализатор не счел ее началом расширения
разметки. Это можно сделать, поставив перед ней пустую пару фигурных скобок, как в
следующем примере:


В механизме привязки к данным (см. главу 13) такой способ экранирования используется для задания спецификаторов формата, где фигурные скобки являются
стандартной частью синтаксиса.

Расширение разметки

57

Поскольку расширения разметки - это просто классы с конструкторами по умолчанию, то
их можно использовать в элементах свойств. Следующее описание кнопки Button
полностью эквивалентно предыдущему:


Такая трансформация допустима, поскольку у всех этих расширений разметки имеются
свойства, соответствующие аргументам конструкторов с параметрами (то есть
позиционным параметрам в синтаксисе атрибутов свойств). Например, в классе
StaticExtension определено свойство Member, которое имеет тот же смысл, что и аргумент,
ранее передававшийся конструктору с параметрами, а в классе RelativeSourcе есть
свойство Mode - ему также соответствует аргумент конструктора.

КОПНЕМ ГЛУБЖЕ
Расширения разметки и процедурный код
Каждое расширение разметки выполняет некоторую специфическую функцию.
Например, следующий код на C# эквивалентен XAML-описанию кнопки Button с
использованием расширений NullExtension, StaticExtension и Binding:
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
// Установить Background:
b.Background = null;
// Установить Height:
b.Height = System.Windows.SystemParameters.IconHeight;
// Установить Content:
System.Windows.Data.Binding binding = new System.Windows.Data.Binding();
binding.Path = new System.Windows.PropertyPath("Height");
binding.RelativeSource = System.Windows.Data.RelativeSource.Self;
b.SetBinding(System.Windows.Controls.Button.ContentProperty, binding);

Однако этот код работает иначе, чем компилятор или анализатор XAML, который
предполагает, что любое расширение разметки устанавливает нужные значение

58

Глава2. Все тайны XAML

во время выполнения (вызывая метод ProvideValue). Эквивалентный процедурный
код часто оказывается сложным и иногда требует знания контекста, известного
только анализатору (например, как разрешать префикс пространства имен, который
может встречаться в свойстве Member расширения StaticExtension). К счастью,
работать с расширениями разметки таким образом в процедурном коде
необязательно!

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

Свойство Content
В большинстве классов WPF имеется свойство (задаваемое с помощью атрибута),
значением которого является содержимое данного XML-элемента. Оно называется
свойством содержимого и в действительности представляет собой просто удобный
способ сделать XAML-представление более компактным. В некотором смысле свойство
содержимого похоже на свойства по умолчанию в старых версиях Visual Basic
(вызывавшие много нареканий).
Для свойства Content кнопки Button имеется специальное соглашение (что очень кстати),
поэтому описание


А составное содержимое Button, например


можно переписать так:


Дочерние объектные элементы

59

Нигде не требуется, чтобы свойство содержимого называлось именно Content; так, в
классах ComboBox, ListBox и TabControl (все из пространства имен
System.Windows.Controls) свойство содержимого названо Items.

Элементы коллекций
Язык XAML позволяет добавлять элементы в два основных вида коллекций,
поддерживающих индексирование: списки и словари.

Списки
Списком считается любая коллекция, в которой реализован интерфейс
System.Collections.IList, например System.Collections.ArrayList и многочисленные классы
коллекций, определенные в WPF. Следующий XAML-код добавляет два элемента в
список ListBox, свойство Items которого имеет тип ItemsCollection, реализующий
интерфейс IList:







Этот XAML-код эквивалентен такому коду на С#:
System.Windows.Controls.ListBox listbox = new System.Windows.Controls.ListBox();
System.Windows.Controls.ListBoxItem item1 =
new System.Windows.Controls.ListBoxItem();
System.Windows.Controls.ListBoxItem item2 =
new System.Windows.Controls.ListBoxItem();
item1.Content = "Item 1";
item2.Content = "Item 2";
listbox.Items.Add(item1);
listbox.Items.Add(item2);

Далее, поскольку Items - свойство содержимого для ListBox, то XAML-код можно еще
сократить:





Этот код работает потому, что свойство Items класса ListBox автоматически
инициализируется пустой коллекцией. Если бы оно инициализировалось значением null
(и, в отличие от доступного только для чтения свойства Items класса ListBox, допускало
чтение и запись), то пришлось бы поместить все элементы внутрь явно заданного
элемента XAML, который создает экземпляр коллекции. Поскольку встроенные в WPF
элементы управления устроены не так, то продемонстрируем этот подход для
воображаемого элемента OtherList-Box:

60

Глава2. Все тайны XAML










Словари
Коллекция System. Windows.ResourceDictionary используется в WPF очень часто, в чем
мы убедимся в главе 12 «Ресурсы». Этот класс реализует интерфейс
System.Collections.IDictionary, а значит, поддерживает добавление, удаления и
перечисление пар ключ/значение в процедурном коде, как любая хеш-таблица. В XAML в
любую коллекцию, реализующую интерфейс IDictionary можно добавить пару
ключ/значение.
Например,
следующий
XAML-код
добавляет
в
словарь
ResourceDictionary два цвета Color:





Здесь используется ключевое слово XAML Key (определенное в дополнительном
пространстве имен XML), которое обрабатывается специальным образом и позволяет
связать с каждым значением Color некий ключ. (В самом типе Color свойство Key не
определено.) Следовательно, этот XAML-код эквивалеяй тен такому коду на С#:
System.Windows.ResourceDictionary d = new System.Windows.ResourceDictionary();
System.Windows.Media.Color color1 = new System.Windows.Media.Color();
System.Windows.Media.Color color2 = new System.Windows.Media.Color();
color1.A = 255; color1.R = 255; color1.G = 255; color1.B = 255;
color2.A = 0; color2.R = 0; color2.G = 0; color2.B = 0;
d.Add("1", color1);
d.Add("2", color2);

КОПНЕМ ГЛУБЖЕ
Списки, словари и анализатор XAML2009
Анализатор WPF XAML всегда поддерживал только коллекции IList и IDictionary, но
функциональность анализатора XAML2009 (описанного далее в разделе «Введение в
XAML2009») несколько расширена. Сначала он проверяет интерфейсы IList и
IDictionary потом - ICollection и IDictionary, а затем - наличие методов Add
и GetEnumerator.

Дочерние объектные элементы

61

Отметим, что значение, заданное в XAML с помощью атрибута х.Кеу, рассматривается
как строка, если только не используется расширение разметки мы не работаем с
анализатором XAML2009 (см. далее раздел «Введение в XAML2009»). В указанных
случаях попытка преобразовать тип не предпринимается.
Еще о преобразовании типов
Потомком объектного элемента может быть обычный текст, как в следующем объявлении
элемента SolidColorBrush на XAML:
White

Эта запись эквивалентна следующей:


даже несмотря на то, что Color не описано как свойство содержимого. В данном случае
первый фрагмент работает потому, что существует конвертер типа, умеющий
преобразовывать такие строки, как "White" (или white", или "#FFFFFF") в объект типа
SolidColorBrush.
Хотя конвертеры типов играют важнейшую роль в обеспечении удобочитаемости XAML,
у них есть и негативная сторона: может показаться, что в XAML творится какое-то
волшебство, поскольку не всегда понятно, как элементы отображаются на объекты .NET.
Опираясь на уже известные нам факты, можно предположить, что в XAML нельзя
определить элемент, соответствующий абстрактному классу, потому что невозможно
создать
экземпляр
такого
класса.
Однако,
несмотря
на
то,
что
System.Windows.Media.Brush
является
абстрактным
базовым
классом
для
SolidColorBrush, GradientBrush и других конкретных классов кистей, мы все же можем
переписать предыдущий фрагмент XAML- кода в таком виде:
White

поскольку конвертер типов Brush понимает, что речь идет о SolidColorBrush. Выглядит
несколько необычно, но такая возможность очень важна для записи в XAML
примитивных типов, как будет показано ниже, во врезке «Расширяемая часть XAML».

КОПНЕМ ГЛУБЖЕ
Расширяемая часть XAML
Поскольку XAML предназначен для работы с системой типов .NET, то его можно
использовать практически с любым объектом .NET (и даже с COM-объектами благодаря интероперабельности с СОМ), в том числе определенным вами. При этом
совершенно неважно, относятся ли эти объекты к пользовательскому интерфейсу.

62

Глава2. Все тайны XAML

Однако классы необходимо определять с учетом возможности использования в
декларативном коде. Например, если в классе нет ни конструктора по умолчанию, ни
полезных открытых свойств, то им нельзя будет напрямую воспользоваться в XAML
(если только вы не работаете с XAML2009). Программные интерфейсы WPF тщательно
продуманы с тем, чтобы они отвечали декларативной модели XAML (обычных
принципов разработки для .NET недостаточно).
Сборки WPF помечены атрибутом XmlnsDef initionAttribute, который отображает
содержащиеся в них пространства имен .NET на пространство имен XML в XAMLфайле. Но что делать со сборками, которые разрабатывались без ориентации на XAML
и, следовательно, не содержат этого атрибута? Находящиеся
в них типы все равно можно использовать - нужно лишь добавить специальную
директиву, описывающую пространство имен XML. Например, вот обычный код на С#,
в котором используются классы .NET из сборки mscorlib.dll:
System.Collections.Hashtable h = new System.Collections.Hashtable();
h.Add("key1", 7);
h.Add("key2", 23);

А вот как его можно представить в XAML:

7
23


Директива clr-namespace позволяет использовать пространство имен .NET
непосредственно в XAML. Спецификация сборки в конце необходима только в случае,
когда нужные типы не находятся в той же сборке, где хранится скомпилированный
XAML-код. Обычно достаточно простого имени сборки (как в случае mscorlib), но
можно использовать и каноническое представление, поддерживаемое методом
System.Reflection.Assembly.Load (правда, без пробелов), которое включает
дополнительную информацию, например номер версии и/или маркер открытого
ключа.
Отметим два важных момента, которые проливают свет на интеграцию не только с
системой типов .NET, но и с конкретными типами .NET Framework:
 Дочерние элементы можно добавлять в родительскую хеш-таблицу Hashtable с
помощью стандартного синтаксиса XAML х:кеу, поскольку Hashtable, как и
другие классы коллекций в .NET Framework, реализует интерфейс Dictionary
начиная с версии 1.0.

 Тип System.Int32 можно использовать столь простым образом, поскольку уже
существует конвертер типа, умеющий преобразовывать строку в целое число.
Объясняется это тем, что конвертеры типов, поддерживаемые XAML, всего
лишь подклассы класса System.ComponentModel.TypeConverter, который также
существует со времени версии .NET Framework 1.0. Это тот же механизм
преобразования типов, что используется в Windows Forms (и позволяет,
например, вводить в сетке свойств в Visual Studio строки, которые
преобразуются в подходящий тип).

Сочетание XAML и процедурного кода

63

КОПНЕМ ГЛУБЖЕ
Правила обработки потомков объектных элементов в XAML
Мы рассмотрели все три типа потомков объектных элементов. Во избежание неоднозначности каждый компилятор или анализатор XAML при разборе и интерпретации дочерних элементов должен придерживаться следующих правил:
1. Если тип реализует интерфейс IList, вызвать IList. Add для каждого дочернего
элемента.
2. Иначе, если тип реализует интерфейс IDictionary, вызвать IDictionary. Add для I
каждого дочернего элемента, используя в качестве ключа значение атрибута i
х:Кеу, а в качестве значения - сам элемент. (Правда, анализатор XAML2009
проверяет IDictionary раньше IList и поддерживает также другие интерфейсы
коллекций, о чем упоминалось выше.)
3. Иначе, если у родителя есть свойство содержимого (помеченное атрибутом
System.Windows.Markup.ContentPropertyAttribute) и тип дочернего элемента совместим с этим свойством, считать дочерний элемент значением этого свойства.
4. Иначе, если дочерний элемент является простым текстом и существует конвертер
типа, который может преобразовать этот текст в тип родителя (и при этом для
родителя не установлены никакие свойства), подать дочерний элемент на вход
конвертера типа, а полученный результат считать экземпляром родителя.
5. Иначе считать содержимое неизвестным, что может являться поводом для
возбуждения исключения.
Правила 1 и 2 обеспечивают поведение, описанное в разделе «Элементы коллекций»,
правило 3 - поведение, описанное в разделе «Свойство Content», а правило 4поведение, описанное в разделе «Еще о преобразовании типов», которое чаще всего
ставит в тупик.

Сочетание XAML и процедурного кода
WPF-приложение можно полностью написать на любом .NET-совместимом языке
программирования. А для создания некоторых простых WPF-приложений достаточно
одного лишь XAML благодаря механизму привязки к данным (см. главу 13), триггерам
(см. следующую главу) и тому факту, что страницы, написанные на чистом XAML,
можно просматривать в браузере. Однако большинство WPF-приложений представляют
собой сочетание XAML и процедурного кода. В этом разделе мы рассмотрим два
способа совместного использования кода XAML и процедурного языка
программирования.

Загрузка и разбор XAML во время выполнения
В состав WPF входит анализатор XAML, работающий на этапе выполнения. Он
представлен двумя классами в пространстве имен System.Windows.Markup:Xamlreader и
XamlWriter. Их API предельно прост. Класс XamlReader содержит несколько
перегруженных вариантов статического метода Load, а класс Xaml- Writer - несколько

64

Глава2. Все тайны XAML

вариантов статического метода Save. Таким образом, программы, написанные на любом
.NЕТ-совместимом языке, могут без особых проблем воспользоваться XAML во время
выполнения. В версию .NET Framework 4 включен новый набор классов для чтения и
записи XAML, но в них немало подводных камней. Сейчас эти проблемы нам не очень
интересны, но ниже, в разделе «Трюки с классами чтения и записи XAML», мы вернемся
к этой теме.

Класс XamlReader
Перегруженные варианты метода XamlReader. Load разбирают XAML-код, его создают
соответствующие объекты .NET и возвращают экземпляр, представляющий корневой
элемент. Так, если XAML-файл MyWindow.xaml в текущем каталоге содержит в качестве
корневого узла объект Window (подробно рассматриваемый в главе 7 «Структурирование
и развертывание приложения») то для загрузки и получения объекта Window можно
использовать следующем код:
Window window = null;
using (FileStream fs =
new FileStream("MyWindow.xaml", FileMode.Open, FileAccess.Read))
{
// Получить корневой элемент. Мы знаем, что это Window
window = (Window)XamlReader.Load(fs);
}

В данном случае методу Load передается объект класса FileStream (из пространства имен
System.IO). Когда Load вернет управление, в памяти будет представлена вся иерархия
объектов из XAML-файла, так что сам файл больше не нужен. Поэтому поток FileStream
закрывается сразу по выходе из блока using, поскольку объекту XamlReader можно
передать произвольный поток Stream (или - в другом перегруженном варианте - объект
System.Xml.XmlReader), то для получения содержимого XAML-файла есть масса
возможностей.
Имея экземпляр корневого элемента, можно получить его дочерние элементы, если
воспользоваться соответствующими свойствами содержимое или свойствами коллекций.
В показанном ниже коде предполагается, что содержимым Window является объект
StackPanel, пятый дочерний элемент которое - кнопка OК:
Window window = null;
using (FileStream fs =
new FileStream("MyWindow.xaml", FileMode.Open, FileAccess.Read))
{
// Получить корневой элемент. Мы знаем, что это Window
window = (Window)XamlReader.Load(fs);
}
// Найти кнопку OK, перебирая дочерние элементы (мы
// пользуемся априорными знаниями о структуре документа!)
StackPanel panel = (StackPanel)window.Content;
Button okButton = (Button)panel.Children[4];

Сочетание XAML и процедурного кода

65

Имея ссылку на кнопку Button, можно делать с ней все, что угодно: задавать
дополнительные свойства (возможно, применяя логику, которую трудно или
невозможно выразить на XAML), присоединять обработчики событий или выполнять
какие-то действия, которые нельзя реализовать на XAML, например вызывать методы
кнопки.
Конечно же, использование «защитого» индекса и прочих предположений относительно
структуры пользовательского интерфейса нельзя считать удовлетворительным
решением, так как любое изменение XAML может привести к их нарушению. Вместо
этого можно было бы написать код обработки элементов XAML в более общем виде и
искать элемент Button, содержащий строку "ОК", но тогда придется выполнить слишком
много работы для такой простой задачи. Кроме того, если нас интересует кнопка с
графическим содержимым, то как ее найти среди других кнопок?
К счастью, XAML поддерживает именование элементов, поэтому их можно находить и
использовать посредством процедурного кода.

СОВЕТ
В классе XamlReader определен также метод экземпляра LoadAsync, который
загружает и разбирает XAML-код асинхронно. Этим методом имеет смысл пользоваться, например, чтобы не «подвешивать» пользовательский интерфейс на время, пока
загружается большой XAML-файл или производится загрузка по сети. Кроме того,
имеется метод CancelAsync для прерывания обработки и событие BadCompleted,
информирующее о ее завершении.
Однако метод LoadAsync ведет себя несколько странно. Он работает в потоке пользовательского интерфейса, многократно обращаясь к методу Dispatcher. BeginInvoke
(WPF пытается разбить работу на отрезки продолжительностью 200 мс).
К тому же обработка производится асинхронно, только если в корневом узле XAML
установлен атрибут х:SynchronousMode="Async". В противном случае LoadSync
загружает XAML в синхронном режиме, ничего не сообщая об этом.

Именование элементов XAML
В пространстве имен XAML определено ключевое слово Name, которое позволяет
назначить имя любому элементу. Для простой кнопки ОК, которая, как мы предполагаем,
находится где-то в окне Window, ключевое слово Name можно использовать следующим
образом:


Тогда приведенный выше код на C# можно переписать с использованием метода
Window.FindName, который рекурсивно просматривает всех потомков и возвращает
требуемый объект:
Window window = null;
using (FileStream fs =

66

Глава2. Все тайны XAML

new FileStream("MyWindow.xaml", FileMode.Open, FileAccess.Read))
{
// Получить корневой элемент. Мы знаем, что это Window
window = (Window)XamlReader.Load(fs);
}
// Находим кнопку OK, зная только ее имя
Button okButton = (Button)window.FindName("okButton");

Метод FindName имеется не только в классе Window. Он определен в классах FrameworkElement и FrameworkContentElement, которые являются базовыми для многих
важных классов WPF.

КОПНЕМ ГЛУБЖЕ
Именование элементов без x:Name
Синтаксис x.Name можно использовать для именования элементов, но в некоторых
классах определено специальное свойство, которое можно рассматривать как имя
элемента (оно назначается с помощью атрибута System.Windows.Markup.
RuntimeNamePropertyAttribute). Например, в классах FrameworkElement и FrameworkContentElement имеется свойство Name, поэтому они помечены атрибутом RuntimeNameProperty("Name"). Это означает, что для таких элементов можно просто задать
свойство Name, не используя синтаксис x:Name. Можно использовать любой из этих
механизмов, но не оба сразу. Наличие двух способов задания имени элемента вносит
некоторую путаницу, но иметь свойство Name удобно для использования в
процедурном коде.

СОВЕТ
Во всех версиях WPF для ссылки на именованный элемент можно использовать
расширение разметки Binding в значении свойства:



(В данном случае присваивание ссылки на поле ввода TextBox в качестве значения
атрибута Target элемента Label передает этому полю фокус ввода при нажатии
комбинации клавиш Alt+T.) WPF 4 включает новое, более простое расширение
разметки System.Windows.Markup.Reference, которое позволяет искать элементы на
этапе синтаксического разбора, а не выполнения. Его можно использовать следующим
образом:



Сочетание XAML и процедурного кода

67

Кроме
того,
если
свойство
помечено
конвертером
типа
System.Windows.Markup.NameReferenceConverter (как в данном случае), то строковое
имя неявно преобразуется в экземпляр, на который ведет ссылка:



Компиляция XAML
Загрузка и синтаксический анализ XAML во время выполнения представляют интерес для
динамического изменения внешнего облика приложения или для использования в .NETсовместимых языках, не поддерживающих компиляции XAML. Однако в большинстве
WPF-проектов используется механизм компиляции XAML, поддерживаемый MSBuild и
Visual Studio. Компиляция XAML включает три шага: преобразование XAML-файла в
специальный двоичный формат, включение результата в создаваемую сборку в качестве
двоичного ресурса и создание инфраструктуры, которая автоматически подключает
XAML к процедурному коду. В языках C# и Visual Basic поддержка компиляции XAML
реализована лучше всего.
Если вы не возражаете против использования XAML-файла совместно с процедурным
кодом, то для его компиляции нужно лишь добавить его в WPF- проект, созданный в
Visual Studio, указав в качестве действия при построении (Build Action) значение Page
(Страница). (В главе 7 объясняется, как воспользоваться таким содержимым в контексте
приложения.) Однако в типичном случае, когда XAML-файл компилируется и сочетается
с процедурным кодом, первым делом нужно задать подкласс для корневого элемента в
XAML- файле. Это можно сделать с помощью ключевого слова Class, определенного в
пространстве имен XAML. Например:

...


КОПНЕМ ГЛУБЖЕ
Поддержка откомпилированного XAML-кода в произвольном .NET-языке
Для использования откомпилированного XAML-кода в произвольном .NET-co
вместимом языке программирования необходимо выполнение двух требований:
наличие соответствующего поставщика CodeDom и целевого файла MSBiuld. Кроме
того, желательна (но не обязательна) поддержка в языке частичных классов.

68

Глава2. Все тайны XAML

В отдельном исходном файле (в том же самом проекте) можно определить этот подкласс
и добавить в него любые члены:
namespace MyNamespace
{
partial class MyWindow : Window
{
public MyWindow()
{
// Необходимо для загрузки содержимого, определенного в XAML-файле!
InitializeComponent();
...
}
Any other members can go here…
}
}

Этот файл часто называют застпраничным (code-behind file). Если в XAML-коде
имеются ссылки на обработчики событий (в таких атрибутах событий, как Click для
Button), то именно здесь их следует определить.
Ключевое слово partial в определении класса важно, поскольку реализаций класса
распределена по нескольким файлам. Если .NET-совместимьй язык не поддерживает
частичные классы (как, например, C++/CLI и J#), то в ХАМL файле необходимо задать
также ключевое слово Subclass в корневом элемента

...


При таком изменении XAML-файл полностью определяет подкласс (в данном случае
MyWindow2), но в качестве базового класса используется класс, определенный в
застраничном файле (MyWindow). Таким образом, разделение реализации между двумя
файлами моделируется с помощью наследования.
При создании WPF-проекта на языке C# или Visual Basic в Visual Studio или при
использовании пункта меню Add New Item... (Добавить новый элемент), чтобы добавить в
проект какие-то WPF-элементы, Visual Studio автоматически создает XAML-файл с
атрибутом х:Class в корневом элементе и застраничный исходный файл, содержащий
частичное определение класса, а также связывает их между собой, чтобы они правильно
обрабатывались при построении проекта.
Если вы пользуетесь программой MSBuild и хотите разобраться в том, что содержит файл
проекта, предполагающего наличие застраничного кода, то можете открыть любой файл
проекта на C# из числа прилагаемых к данной книге в обычном текстовом редакторе,
например Блокноте. Интересующую нас часть типичного проекта выглядит следующим
образом:

Сочетание XAML и процедурного кода

69






MyWindow.xaml
Code



СОВЕТ
Атрибут x:Class разрешается использовать только в компилируемых XAML-файлах.
Но иногда можно скомпилировать XAML-файл и без этого атрибута. Это просто
означает, что соответствующий застраничный файл отсутствует, так что пользоваться
средствами, нуждающимися в процедурном коде, нельзя. Поэтому добавление в
проект Visual Studio XAML-файла без атрибута x:Class - хороший способ
воспользоваться всеми преимуществами компиляции XAML в плане повышения
производительности и удобства развертывания без создания ненужного застраничного
файла.

BAML
Аббревиатура BAML расшифровывается как Binary Application Markup Language
(Двоичный язык разметки приложений). Это просто XAML, который был разобран,
разбит на лексемы и преобразован в двоичный формат. Хотя практически любой код
на XAML можно представить в виде процедурного кода, компиляция XAML в BAML
не генерирует исходный код на процедурном языке. В этом смысле BAML не похож
на промежуточный язык MSIL; это всего лишь сжатый декларативный формат,
который загружается и разбирается быстрее, чем простой XAML-файл (и к тому же
меньше по размеру). По существу, BAML- это деталь реализации процедуры
компиляции XAML. Тем не менее знать о его существовании полезно. На самом деле в
WPF 4 даже имеется открытый класс для чтения BAML-файлов (см. далее раздел
«Трюки с классами чтения и записи XAML»).

70

Глава2. Все тайны XAML

КОПНЕМ ГЛУБЖЕ
Когда-то здесь был CAML...
В предварительных версиях WPF была возможность компиляции XAML в формат
BAML или MSIL. Получающийся при этом MSIL-код назывался CAML, что означает
Compiled Application Markup Language (Скомпилированный язык разметки
приложения). Идея заключалась в том, чтобы предоставить возможность выбора
оптимизации по размеру (BAML) или скорости выполнения (CAML). Но потом
разработчики решили не отягощать WPF поддержкой двух независимых реализаций,
которые делали практически одно и то же. Формату BAML было отдано предпочтение,
поскольку он имеет несколько преимуществ: более безопасен, чем MSIL, более
компактен (поэтому объем загрузки при исполнении Wеb- сценариев меньше) и может
быть локализован даже после компиляции. Более того, CAML работал не настолько
быстрее BAML, как ожидалось. При этом генерировался объемный код, выполняемый
всего один раз. Это неэффективно, приводит к разбуханию DLL-библиотек,
преимущества кэширования не используются и т. д.

Генерируемый исходный код
В процессе компиляции XAML кое-какой процедурный код все же генерируется (если
использовался атрибут х:Class), но это всего лишь «клей», аналогичный тому, что
пришлось бы писать для загрузки и разбора независимого XAML-файла во время
исполнения программы. Таким файлам присваивается суффикс вида .g.cs (или .g.vb), где
g означает «сгенерированный(generated).
Каждый сгенерированный исходный файл содержит частичное определение класса,
указанного в атрибуте х: Class корневого объектного элемента. В нем находятся поля (по
умолчанию internal) для каждого именованного элемента в XAML-файле, при этом в
качестве имени поля используется имя элемента. Там же находится метод
InitializeComponent, который выполняет всю рутинную работу по загрузке внедренного
BAML-pecypca, присваиванию полям экземпляров объектов, первоначально
определенных в XAML-файле, и присоединению обработчиков событий (если они были
специфицированы в XAML.
Поскольку этот «склеивающий» код, помещенный в сгенерированный файл является
частью того же класса, который определен вами в застраничном файле (и поскольку
BAML внедряется в виде ресурса), то вам обычно не приходится задумываться о самом
существовании BAML и о процедуре его загрузки и разбора. Вы просто пишете код,
который ссылается на именованные элементы, как на любые другие члены класса, а
система построения заботится о том, как связать все воедино. Нужно только не забыть
вызвать метод InitializeComponent в конструкторе своего застраничного класса.

Сочетание XAML и процедурного кода

71

ПРЕДУПРЕЖДЕНИЕ
Не забывайте вызывать метод InitializeComponent в конструкторе своего
застраничного класса!
Если вы забудете это сделать, то в корневом элементе не окажется содержимого,
определенного в XAML-файле (потому что соответствующий ему BAML-код не
загружен), а все поля, представляющие именованные объектные элементы, будут равны
null.

КОПНЕМ ГЛУБЖЕ
Процедурный код внутри XAML
На самом деле XAML поддерживает еще и плохо документированную возможность
«внутристраничного кода», помимо застраничного (как в ASP.NET). Для этого
предназначено ключевое слово Code из пространства имен XAML:







При компиляции такого XAML-файла содержимое элемента x:Code копируется в
частичный класс, находящийся в .g.cs-файле. Отметим, что процедурный язык в
XAML-файле не указывается; он определяется проектом, содержащим этот файл.
Заключать процедурный код в скобки  необязательно, но это позволяет обойтись без замены знаков ‗<‘ на <, а амперсандов на &, поскольку секция
СОАТА полностью игнорируется анализаторами XML, а все остальное обрабатывается
как XML-документ. (Правда, в качестве платы за это удобство вы не должны включать
в код последовательность символов ]]>, поскольку она закрывает секцию DATA!)
Впрочем, не существует разумных причин засорять XAML-файлы таким
«внутристраничным кодом. Мало того что при этом стирается различие между пользовательским интерфейсом и логикой приложения, так еще подобные файлы не будут
отображаться в браузере, a Visual Studio не поддерживает для них стандартные средства
работы с кодом, в том числе IntelliSense и цветовую подсветку синтаксиса.

72

Глава2. Все тайны XAML

FAQ
Можно ли BAML декомпилировать обратно в XAML?
Разумеется, да, поскольку на основе BAML можно построить граф объектов и затем
сериализовать его в виде XAML независимо от того, как объекты были объявлены в
первоначальном коде.
Первым делом необходимо найти объект, который будет корневым элементом XAML.
Бели его еще нет, то можно вызвать статический метод System.Windows.Application.
LoadComponent, который загружает нужный объект из BAML:
System.Uri uri = new System.Uri("/WpfApplication1;component/MyWindow.xaml",
System.UriKind.Relative);
Window window = (Window)Application.LoadComponent(uri);

Да, этот код загружает BAML, несмотря на суффикс .xaml в имени файла. Этим он
отличается от предыдущего кода, где для загрузки XAML-файла использовался класс
FileStream, поскольку в случае LoadComponent имя файла задается в виде
универсального идентификатора ресурса (URI) и наличие физического файла с таким
именем не требуется. Метод LoadComponent может автоматически загрузить BAMLкод, внедренный в виде двоичного ресурса, если получит соответствующий URI
(который по соглашению, принятому в MSBuild, должен совпадать с именем исходного
XAML-файла). На самом деле автоматически генерируемый Visual Studio метод
InitializeComponent вызывает именно метод Application. LoadComponent для загрузки
внедренного BAML-кода, правда, другой перегруженный вариант. В главе 12 более
подробно описан механизм получения внедренных ресурсов по URI.
Имея корневой элемент, мы можем воспользоваться классом System. Windows.Markup.
XamlWriter, чтобы получить XAML-представление этого элемента (а следовательно, и
всех его потомков). Класс XamlWriter содержит пять перегруженных вариантов
статического метода Save; самый простой принимает экземпляр объекта и возвращает
соответствующий XAML-код в виде строки:
string xaml = XamlWriter.Save(window);

Если вас пугает, что BAML-код легко «вскрывается», то вспомните, что то же самое
можно сказать о любой программе, которая выполняется локально или локально
отображает пользовательский интерфейс. (Например, нетрудно разобраться в HTMLкоде, JavaScript-сценариях или CSS-стилях сайта.) Для популярной программы .NET
Reflector
имеется
специальная
надстройка
BamlViewer
(см.
http://codeplex.com/reflectoraddins), которая показывает BAML-pecypc, внедренный в
любую сборку, в виде декомпилированного XAML-кода.

Введение в XAML2009
Хотя XAML является языком общего назначения, сфера применения которого не
ограничивается WPF, компилятор и анализаторы XAML для WPF архитектурно
привязаны к WPF. Поэтому применение их в других технологиях создает зависимость

Введение в XAML2009

73

от WPF. В версии .NET Framework 4.0 это положение исправлено за счет новой сборки
System.Xaml, которая содержит средства для работы с XAML. WPF (равно как WCF и
WF) теперь зависят только от System.Xaml но не друг от друга.
Одновременно в .NET Framework 4.0 появилось много улучшений самого языка XAML.
Это второе поколение языка получило название XAML2009. (А чтобы не путаться,
первое поколение иногда называют XAML2006.) Сборка System.Xaml поддерживает
XAML2009, в отличие от прежних API (например, System.Windows.Markup.XamlReader и
System.Windows.Markup.XamlWriter из предыдущего раздела), которые поддерживают
только XAML2006.
Новые возможности XAML2009, описанные в этом разделе, не представляют собой
ничего особо революционного, но составляют приятный набор последовательных
улучшений XAML. Впрочем, радоваться рано: значительную часть новых возможностей
не удастся использовать в WPF-проектах, поскольку компилятор XAML все еще основан
на API XAML2006, равно как и конструктор и редактор WPF в Visual Studio, - не хватило
времени на полную интеграцию.
Во время написания данной книги еще не было ясно, когда WPF полностью перейдет на
XAML2009. (Отметим, что Silverlight также не поддерживает XAML2009; даже
спецификация XAML2006 поддерживается не полностью!) Однако в WPF 4 новыми
средствами можно пользоваться в автономных XAML- файлах при условии, что они
загружаются в программу, построенную на базе APIXAML2009. Таковыми, например,
являются программа XAMLPAD2009, приложенная в качестве примера к этой книге, и
Internet Explorer при использовании пространства имен XML netfх-2009.
Но изучить возможности XAML2009 интересно, пусть даже пока они не очень полезны.
Большинство из них касается расширения множества типов, которые можно использовать
в XAML напрямую. Это отличная новость для создателей библиотек классов, поскольку
XAML2009 накладывает гораздо меньше ограничений на совместимость библиотек с
XAML. Взятые по отдельности, новые средства не сильно повышают выразительность
языка, но в совокупности упрощают решение реальных задач.

Полная поддержка универсальных классов
В XAML2006 корневой элемент может быть экземпляром универсального класса
благодаря ключевому слову x:TypeArguments, значением которого является имя типа или
список имен типов через запятую. Но поскольку атрибут x:TypeArguments разрешается
использовать только в корневом элементе, то назвать XAML2006 дружественным к
универсальным классам было бы преувеличением.
Традиционно это ограничение обходили, создавая обычный класс, наследующий
универсальному. На такой класс уже можно ссылаться в XAML без ограничений.
Например:

74

Глава2. Все тайны XAML

С#:
public class PhotoCollection : ObservableCollection { }

XAML:





В XAML2009 атрибут x:TypeArguments может употребляться в любом элементе поэтому,
скажем,
объекты
класса
ObservableCollection
допустимо
создавать
непосредственно в XAML:





Здесь предполагается, что collections отображается на пространства
System.Collections.ObjectModel, которое содержит класс ObservableCollection.

имен

Словарные ключи произвольного типа
В XAML2009 преобразование типов применяется и к значениям атрибут х:Кеу, поэтому в
словарь можно добавлять значения с нестроковыми ключами, не прибегая к расширениям
разметки. Например:

One
Two


Здесь предполагается, что
System.Collections.Generic

collections

отображается

на

пространство

имен

КОПНЕМ ГЛУБЖЕ
Отключение преобразования типов для нестроковых словарных ключей
Для обратной совместимости в классе XamlObjectWriter из XAML2009 имеете
возможность отключить новый механизм автоматического преобразования типов. Это
свойство XamlObjectWriterSettings.PreferUnconvertedDictionaryKeys
принимая
значение true, System.Xaml не будет конвертировать ключи, если словарь реализует
неуниверсальный интерфейс IDictionary, при условии, что:
 System.Xaml уже потерпел неудачу при вызове метода IDictionary.Add для того
же экземпляра, или
 словарь принадлежит известному типу .NET Framework, и System. Xaml знает,
что для него необходимо преобразование.

Введение в XAML2009

75

Встроенные системные типы данных
В XAML2006 использовать встроенные типы данных .NET, например String или Int32,
было неудобно, так как требовалось ссылаться на пространство имен System из сборки
mscorlib; мы уже видели, как это выглядит:
7

В XAML2009 в пространство имен языка XAML добавлено 13 наиболее употребительных типов данных .NET. В предположении, что данному пространству имен
сопоставлен префикс х, это следующие типы: x:Byte , x:Boolean, x:Int16, x:Int32, x:Int64,
x:Single , x:Double , x:Decimal, x:Char , x:String , x:Object , x:Uri, and x:TimeSpan.
Следовательно, предыдущий фрагмент можно переписать в таком виде:
7

Но обычно в XAML-файле, где уже есть ссылка на пространство имен XAML, это
записывается проще:
7

Создание объектов с помощью конструктора с аргументами
В XAML2009 появилось новое ключевое слово х:Arguments, которое позволяет задать
один или несколько аргументов для передачи конструктору класса. Рассмотрим, к
примеру, класс System.Version, в котором имеется конструктор по умолчанию и четыре
конструктора с параметрами. В XAML2006 невозможно было создать экземпляр этого
класса, если не существовало подходящего конвертера типа (при условии, конечно, что
вас не устраивал конструктор по умолчанию, создающий версию 0.0).
В XAML2009 можно создать объект этого класса с помощью конструктора,
принимающего в качестве параметра простую строку:


При этом аргумент конструктора необязательно должен быть строкой; при
необходимости тип значения атрибута преобразуется.
В отличие от x.TypeArguments, ключевое слово х:Arguments не позволяет задавать в
качестве значения атрибута несколько аргументов в одной строке через запятую. Но
можно задавать их в виде элементов, вложенных в х:Arguments. Например, вызвать
конструктор класса System.Version, который принимает четыре целых числа, можно так:


4
0
30319
1



76

Глава2. Все тайны XAML

Создание экземпляров с помощью фабричных методов
С помощью нового ключевого слова x:FactoryMethod в XAML2009 можно задать
экземпляр класса, вообще не имеющего открытых конструкторов x:FactoryMethod
позволяет указать произвольный открытый статический метод, который возвращает
объект нужного типа. Например, в следующем XAML-коде используется объект типа
Guid, возвращаемый статическим методом Guid NewGuid:


Если x:FactoryMethod используется совместно с x:Arguments, то аргументы передаются
статическому фабричному методу, а не конструктору. Таким образом, в следующем
примере вызывается статический метод Marshal.GetExceptionForHR, который принимает
код ошибки HRESULT и возвращает соответствующее ему исключение .NET, которое
слой интероперабельности CLR возбуждает при возникновении такой ошибки:


На рис. 2.3 показано, как XAMLPAD2009 реагирует на размещение двух меток Label в
одной панели StackPanel.

Гибкость присоединения обработчиков событий
В XAML2006 нельзя было присоединять обработчики событий в автономном XAMLфайле. В XAML2009 это стало возможно при условии, что yдается найти корневой
экземпляр и в нем имеется метод с указанным именем и под ходящей сигнатурой. Кроме
того, в XAML2009 значением атрибута событи может быть любое расширение разметки,
которое возвращает соответствующий делегат:






84

Глава2. Все тайны XAML





Item 2


Item 3





Таблица 2.1. Поток узлов XAML, порождаемых XamlXmlReader при чтении разметки
в листинге 2.2

Трюки классами записи и чтения XAML

85

86

Глава2. Все тайны XAML

Обратите внимание, что все три элемента ListBoxItem в табл. 2.1 представлены одинаково,
так же как и оба элемента Button, хотя и возможно провести различие между
использованием свойства Name кнопки Button и директивы x:Name XAML. (В последнем
случае XamlMember наследует типу XamlDirective, свойство IsDirective которого равно
true.)
Также отметим, что узлы GetObject, EndMember и EndObject не сопровождаются никакой
дополнительной информацией; ее следует получать из других узлов в потоке. Из-за этого
для выполнения нетривиальных преобразований в формат XAML часто требуется
создавать собственный стек для хранения данных, относящихся к объектам и/или их
членам.

КОПНЕМ ГЛУБЖЕ
Совместимость разметки
Пространство
имен
совместимости
разметки
(http://schemas.openxmlformats.org/markupcoinpatibility/2006, обычно ему сопоставляется
префикс mс) содержит атрибут Ignorable, информирующий процессор XAML о
необходимости игнорировать все элементы/атрибуты из указанных пространств имен,
которым нельзя сопоставить типы или члены типов .NET. (В этом пространстве имен
имеется также атрибут ProcessContent, который отменяет действие Ignorable для некоторых типов в игнорируемых пространствах имен.)
Программа Expression Blend использует эту возможность для добавления в XAMLсодержимое свойств, имеющих смысл только на этапе конструирования. На этапе
выполнения эти свойства будут проигнорированы. Пример:

...


Значением атрибута me:Ignorable может быть список пространств имен, разделенных
пробелами, а значением атрибута me:ProcessContent — список элементов, также
разделенных пробелами.
Когда XamlXmlReader встречает игнорируемое содержимое, которое не может быть
разрешено, он не порождает для него никаких узлов. Если же игнорируемое содержимое можно разрешить, то узлы порождаются как обычно. Поэтому потребителям
не нужно специально ничего делать для корректной обработки совместимости
разметки.

Запись в объекты
Приложение XAMLPAD2009 не конвертирует XAML в объекты, находящиеся в памяти.
Оно лишь модифицирует XAML-содержимое, чтобы вать успешную визуализацию более
широкого спектра конструкций Wr XAML. Точнее, производятся две модификации.

Трюки классами записи и чтения XAML

87

Убираются все члены, относящиеся к событиям, так как если обработчик
события не найден, то XamlObjectWriter возбуждает исключение, например,
стаким сообщением: Failed to create a 'Click' from the text button_Click. Отметим,
что в классе XamlObjectWriter имеется свойство RootObjectlnstance, которому
можно присвоить объект с подходящими обработчиками событий, но проще
всего эти события просто выкинуть - для инструмента экспериментирования с
XAML такой подход вполне приемлем. Кроме этого, убирается атрибут х:Class,
потому что в автономном XAML-коде он недопустим.
•
Элемент Window конвертируется в Раде. В главе 7 эти элементы рассматриваются подробно, но смысл в том, что элемент Window не может быть потомком
другого элемента, a XAMLPAD2009 всегда пытается присоединить корневой
объект в качестве непосредственного потомка своего собственного пользовательского интерфейса. Существуют и другие способы справиться с этой
трудностью (например, увидев, что корневым элементом является Window,
создавать из него окно), но описанной выше замены одного узла XAML другим
для учебного примера достаточно.
В листинге 2.3 показан специализированный цикл обработки узлов, в котором
преобразование содержимого XAML-строки в объекты сопровождается двумя
дополнительными операциями.
•

Листинг 2.3. Цикл обработки узлов для преобразования XAML-строки в граф
объектов с модификациями
public static object ConvertXmlStringToMorphedObjectGraph(string xmlString)
{
// String -> TextReader -> XamlXmlReader
using (TextReader textReader = new StringReader(xmlString))
using (XamlXmlReader reader = new XamlXmlReader(textReader,
System.Windows.Markup.XamlReader.GetWpfSchemaContext()))
using (XamlObjectWriter writer = new XamlObjectWriter(reader.SchemaContext))
{
// Цикл обработки узлов
while (reader.Read())
{
// Пропустить события и x:Class
if (reader.NodeType == XamlNodeType.StartMember &&
reader.Member.IsEvent || reader.Member == XamlLanguage.Class)
{
reader.Skip();
}
if (reader.NodeType == XamlNodeType.StartObject &&
reader.Type.UnderlyingType == typeof(Window))
{
// Преобразовать Window в Page
writer.WriteStartObject(new XamlType(typeof(Page),
reader.SchemaContext));
}

88

Глава2. Все тайны XAML
else
{
// в противном случав вывести узел без изменений
writer.WriteNode(reader);
}
}
// По завершении работы XamlObjectWriter здесь будет
// экземпляр корневого объекта
return writer.Result;
}

}

В листинге 2.3 для пропуска членов-событий (IsEvent=true) и атрибутов x:Class
применяется метод Skip класса XamlReader. (Атрибут x:Class опознается с помощью
удобного статического класса System.Xaml.XamlLanguage, в котором все виды директив
XamlDirective и встроенных значений XamlType определены как свойства, доступные
только для чтения; это упрощает сравнение.) Когда считыватель позиционирован на узле
StartObject или StartMember, метод Skip сдвигает указатель потока на узел, следующий
за соответствующим узлом EndObject/EndMember (пропуская все вложенные
объекты/члены, что нам и нужно). Если же считыватель позиционирован на узле любого
другого типа, то вызов Skip эквивалентен повторному вызову Read: он переходит на
следующий узел.
Для замены Window на Раде нужно подменить только узел StartObject. Напомним, что с
узлом EndObject не ассоциированы никакие данные, его интерпретация зависит от
других узлов в потоке. Поэтому EndObject для Window вполне может стать EndObject
для Раде. Однако подобная подмена других членов Window членами Раде некорректна,
поскольку они уже были разрешены считывателем как члены Window до начала цикла
обработки узлов. В исходном коде, прилагаемом к книге, дополнительно создается
новый член Раgе для каждого члена Window, к которому такое преобразование
применимо.
В листингах 2.1 и 2.3 по завершении цикла обработки узлов в свойство
XamlObjectWriter.Result записывается экземпляр корневого объекта. Точнее, после
вывода каждого узла EndObject в XamlObjectWriter.Result помещается ссылка на
соответствующий ему объект. А так как последний записанный в поток узел EndObject
соответствует корневому элементу, то окончательным значением Result оказывается
корень графа объектов.

Запись в формате XML
Запись WPF-объектов в XAML-файл в формате XML - часто встречающаяся задача.
Поскольку в настоящее время класс XamlObjectReader не поддерживает WPF-объекты, в
листинге 2.4 показано, как можно конвертировать XML из одного варианта в другой,
совместно используя XamlObjectReader и XamlObjectWriter. При этом получается
простейший «очиститель XAML», который нормализует входной XML-документ,
убирая комментарии и формируя разметку с единообразно расставленными пробелами.

Трюки классами записи и чтения XAML

89

Листинг 2.4. «Очиститель XAML», нормализующий входной XML
public static string RewriteXaml(string xmlString)
{
// String -> TextReader -> XamlXmlReader
using (TextReader textReader = new StringReader(xmlString))
using (XamlXmlReader reader = new XamlXmlReader(textReader))
// TextWriter -> XmlWriter -> XamlXmlWriter
using (StringWriter textWriter = new StringWriter())
using (XmlWriter xmlWriter = XmlWriter.Create(textWriter,
new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
using (XamlXmlWriter writer = new XamlXmlWriter(xmlWriter, reader.SchemaContext))
{
// Простой цикл обработки узлов
while (reader.Read())
{
writer.WriteNode(reader);
}
return textWriter.ToString();
}
}

Здесь практически вся работа сводится к настройке считывателя и записывателя.
Экземпляр XamlXmlReader конструируется так же, как в предыдущем листинге, a
XamlXmlWriter конструируется из объекта System.IO.StringWriter. (XmlWriter можно
было сконструировать также из объекта StringBuilder.) Использование XmlWriter
позволяет организовать аккуратную печать (каждый элемент на отдельной строке с
отступами), а заодно удалить ненужные XML-объявления (). Но если вам это неважно и вы готовы смириться с выводом
содержимого в одну строку, то можно просто передать конструктору XamlXmlWriter
объект StringWriter (поскольку он наследует TextWriter), не оборачивая его в XmlWriter.
// TextWriter -> XamlXmlWriter
using (StringWriter textWriter = new StringWriter())
using (XamlXmlWriter writer = new XamlXmlWriter(textWriter,
reader.SchemaContext))
{
...
}

XamlServices
Чтобы пользователю приходилось писать меньше кода, самые распространенные случаи
употребления средств чтения и записи XAML инкапсулированы в простые статические
методы, определенные в классе System.Xaml.XamlServices, а именно:
 Load - есть несколько перегруженных вариантов, принимающих имя файла в
виде строки, объекты Stream, TextReader, XmlReader или XamlReader. Все

90

Глава2. Все тайны XAML

они возвращают корень соответствующего графа объектов, как и прежний метод
XamlReader. Load. Внутри Load вся работа производится объектами XamlXmlReader и
XamlObjectWriter, как в листинге 2.1.
 Parse - как и Load, метод Parse возвращает корень графа объектов, но на входе
принимает XAML-содержимое в виде строки. Внутри он создает из этой строки
объект StringReader, затем XmlReader и наконец XamlXmlReader, от имени
которого можно уже вызвать метод Load. Таким образом, Parse аналогичен
методу ConvertXmlStringToObjectGraph, представленному в листинге 2.1.
 Save - принимает на входе объект и, в зависимости от перегруженного варианта,
возвращает его содержимое в виде строки, объекта Stream, TextWriter, XmlWriter
либо XamlWriter или даже сохраняет содержимое объекта прямо в текстовом
файле. Внутри Save создает экземпляры XamlObjectReader и XamlXmlWriter
(если только ему уже не передан объект XamlWriter). Он присваивает свойствам
Indent и OmitXmlDeclaration объекта XamlWriter значение true, как в листинге
2.4.
 Transform - выполняет тривиальный цикл обработки узлов, применяя считыватель и записыватель, которые ему переданы.
На самом деле метод XamlServices.Transform работает чуть хитрее, чем показанный выше
тривиальный цикл обработки узлов. Он сохраняет информацию о номере строки и
позиции в ней, если считыватель и записыватель поддерживают интерфейсы для их
создания и использования (IXamlLinelnfo для считывателя и IXamlLinelnfoConsumer для
записывателя). Таким образом, Transform на самом деле выполняет следующее:
public static void Transform(XamlReader reader, XamlWriter writer)
{
IXamlLineInfo producer = reader as IXamlLineInfo;
IXamlLineInfoConsumer consumer = writer as IXamlLineInfoConsumer;
bool transferLineInfo = (producer != null && producer.HasLineInfo &&
consumer != null && consumer.ShouldProvideLineInfo);
// Улучшенный цикл обработки узлов
while (reader.Read())
{
// Передать информацию о строке
if (transferLineInfo && producer.LineNumber > 0)
consumer.SetLineInfo(producer.LineNumber, producer.LinePosition);
writer.WriteNode(reader);
}
}

Следовательно, от цикла обработки узлов из листинга 2.1 можно отказаться (и немного
улучшить результат), заменив его методом XamlServices.Trans, как показано в листинге
2.5. Впрочем, метод ConvertXmlStringToObjectGraph обще не нужен, поскольку он
дублирует XamlServices. Parse.

Трюки классами записи и чтения XAML

91

Листинг 2.5. Небольшое упрощение листинга 2.1
public static object ConvertXmlStringToObjectGraph(string xmlString)
{
// String -> TextReader -> XamlXmlReader
using (TextReader textReader = new StringReader(xmlString))
using (XamlXmlReader reader = new XamlXmlReader(textReader,
System.Windows.Markup.XamlReader.GetWpfSchemaContext()))
using (XamlObjectWriter writer = new XamlObjectWriter(reader.SchemaContext))
{
// Цико обработки узлов
XamlServices.Transform(reader, writer);
// По завершении работы XamlObjectWriter здесь будет
// экземпляр корневого объекта
return writer.Result;
}
}

ПРЕДУПРЕЖДЕНИЕ
Берегитесь подводных камней XamlServices в WPF XAML!
Быть может, вы думаете, что комбинация XamlServices.Parse и XamlServices.Save
позволит реализовать «очиститель XAML» из листинга 2.4 в следующем простом, хотя
и неэффективном виде:
public static string RewriteXaml(string xmlString)
{
return XamlServices.Save(XamlServices.Parse(xmlString));
}

Это неэффективно потому, что на внутреннем уровне строка сначала проходит через
XamlXmlReader, потом записывается в объект с помощью XamlObjectWriter (корень
которому возвращает XamlServices.Parse), затем эту иерархию объектов читает
XamlObjectReader и только после этого окончательная строка записывается в XmlWriter
с помощью XamlXmlWriter. Этот промежуточный шаг создания объектов проблематичен
не только по причинам производительности. Он также требует специальной обработки на
уровне XAML, в частности присоединения обработчиков событий или разрешения
директивы х:Class.
Но еще хуже то, что приведенный код просто не работает, поскольку XamlObjectWriter в
настоящее время не поддерживает WPF-объекты. Можно было бы вместо этого
воспользоваться более старыми классами XamlReader и XamlWriter:
return System.Windows.Markup.XamlWriter.Save(
System.Windows.Markup.XamlReader.Parse(xmlString));

Или, если нужна красивая печать:
using (StringWriter textWriter = new StringWriter())
using (XmlWriter xmlWriter = XmlWriter.Create(textWriter,
new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }))
{
System.Windows.Markup.XamlWriter.Save(
System.Windows.Markup.XamlReader.Parse(xmlString), xmlWriter);
return textWriter.ToString();
}

92

Глава2. Все тайны XAML

Но и этим подходам свойствен промежуточный шаг, связанный с преобразованием
XAML-разметки в объекты.

СОВЕТ
Набор инструментов Microsoft XAML Toolkit (доступен по адресу http://code.msdn.
microsoft.com/XAML), построенный на основе классов из пространства имен System.
Xaml, предлагает несколько очень интересных возможностей, например интеграцию
XAML с инструментом FxCop и объектную модель документа XAML. XAML DOM это набор API, совместимых с LINQ, которые еще больше упрощают исследование и
модификацию XAML-содержимого по сравнению со средствами чтения и записи,
описанными в этой главе. Этот набор инструментов также включает дополнительные
контексты схем: SilverlightSchemaContext для Silverlight XAML и UISchemaContext, в
котором реализована общая абстракция для WPF XAML и Silverlight XAML.

Ключевые слова XAML
В пространстве имен языка XAML (http://schemas.microsoft.com/winfx/2006/xaml)
определен ряд ключевых слов, которые должны особым образом обрабатываться
компилятором или анализатором XAML. В основном они управляют различными
аспектами того, как элементы интерпретируются в процедурном коде, но некоторые
полезны и сами по себе. С некоторыми из них мы уже встречались (Key, Name, Class,
Subclass и Code), а в табл. 2.2 перечислены они все. Мы используем традиционный
префикс х, потому что именно так они обычно употребляются в XAML и в документации.

КОПНЕМ ГЛУБЖЕ
Специальные атрибуты, определенные консорциумом W3C
В дополнение к ключевым словам пространства имен язык XAML поддерживает также
два специальных атрибута, определенных для XML консорциумом World Wide Web
Consortium (W3C): xml:space для управления разбором пробелов и xml.lang для
объявления языка и культуры документа. При этом префикс xml неявно отображается
на стандартное пространство имен XML http://www.w3.org/XML/1998/namespace.

Ключевые слова XAML

93

Таблица 2.2. Ключевые слова из пространства имен языка XAML со стандартным
префиксом х
Ключевое слово
Допустим в
Версия
Назначение
качестве
x:AsyncRecords

Атрибут корневого
элемента

2006+

Управляет размером
блока при асинхронной
загрузке XAML

x:Arguments

Атрибут или
вложенный элемент

2009

Задает аргумент (или
несколько аргументов,
если употребляется в
качестве элемента),
передаваемый
конструктору элемента.
При использовании в
сочетании с
x:FactoryMethod задает
аргумент(ы) фабричного метода

x:Boolean

Элемент

2009

x:Byte

Элемент

2009

x:Char

Элемент

2009

x:Class

Атрибут корневого
элемента

2006+

x:ClassAttributes

Атрибут корневого элемента, должен
использоваться
совместно с х:Class

2009

x:ClassModifier

Атрибут корневого элемента, должен
использоваться
совместно с х:Class

2006+

x:Code

2006+

x:ConnectionId

Элемент в любом месте
XAML, должен
использоваться
совместно с х: Class
Атрибут

x:Decimal

Элемент

2009

x:Double

Элемент

2009

Представляет класс
System.Boolean
Представляет класс
System.Byte
Представляет класс
System.Char
Определяет для
корневого элемента
класс, производный от
типа элемента. Может
сопровождаться
необязательным
префиксом
пространства имен
.NET
Не используется в
WPF; содержит
атрибуты, относящиеся
к Windows Workflow
Foundation
Определяет видимость
класса, указанного в
х:Class (по умолчанию
открытого). Значение
атрибута должно быть
задано в терминах
используемого
процедурного языка
(например public или
internal для С#)
Окружает процедурный
код, включаемый в
класс, указанный в
х:Class.
Не для открытого
применения
Представляет System.
Decimal
Представляет
System.Double

2006+

94

Глава2. Все тайны XAML
Таблица 2.2 (продолжение)
Ключевое слово
x:FactoryMethod

Допустим в качестве
Атрибут любого
элемента

Версия
2009

x:FieldModifier

Атрибут любого элемента, должен использоваться совместно с x:Name
(или эквивалентом)

2006+

x:Int16

Элемент

2009

x:Int32

Элемент

2009

x:Int64

Элемент

2009

x:Key

Атрибут элемента, родитель которого реализует
интерфейс IDictionary
Не используется в WPF
XAML

2006+

x:Name

Атрибут любого не корневого элемента, должен
использоваться совместно сх:Class

2006+

x:Object

Элемент

2009

x:Property

Не используется в WPF
XAML

2009

x:Shared

2006+

x:Single

Атрибут любого
элемента в
ResourceDictionary, принимается во внимание
только при компиляции
XAML
Элемент

x:String

Элемент

2009

x:Subclass

Атрибут корневого элемента, должен использоваться совместно с х:Class

2006+

x:Members

2009

2009

Назначение
Определяет статический
метод, вызываемый для
получения экземпляра
элемента вместо
конструктора
Определяет видимость
поля, генерируемого для
элемента (по умолчанию
internal). Как и в случае
x:ClassModifier, значение
этого атрибута должно
быть задано в терминах
процедурного языка
(например, public,
private,... для С#)
Представляет
System.Int16
Представляет
System.Int32
Представляет
System.Int64
Задает ключ элемента
при добавлении в
словарь родителя
Определяет
дополнительные члены
корневого класса, заданного в х:Class
Задает имя поля,
генерируемого для
элемента, по которому на
него можно ссылаться из
процедурного кода
Представляет
System.Object
Определяет свойство
внутри элемента х:
Members
Может принимать
значение false для
запрета использования
одного экземпляра
ресурса в нескольких
местах (см. главу 12)
Представляет System.
Single
Представляет
System.String
Определяет подкласс
класса, заданного в х: Class,
в котором хранится
содержимое, опредленное в
XAML. В качестве необязательного префикса
молжно указать
пространство имен

Ключевые слова XAML

95

Ключевое слово

Допустим в качестве

Версия

x:SynchronousMode

Атрибут корневого элемента

2006+

x:TimeSpan

Элемент

2009

x:TypeArguments

В XAML2009 атрибут
любого элемента, а в
XAML2006 атрибут
корневого элемента, используемый только совместно с х:Class

2006+

x:Uid

Атрибут любого
элемента

2006+

x:Uri
x:XData

Элемент
Элемент, используемый
в качестве значения любого свойства типа
IXml- Serializable

2009
2006+

Назначение
.NET (используется с
языками, не
поддерживающими
частичные классы)
Определяет, может ли
содержимое XAML
загружаться асинхронно
Представляет
System.TimeSpan
Делает класс
универсальным (как
List),
конкретизируемым
указанными аргументами (например,
List или
List). Может
содержать список
аргументов конкретизации через
запятую. Типам,
отсутствующим в пространстве имен по
умолчанию, должен
предшествовать
префикс пространств
имен XML
Помечает элемент
идентификатором для
локализации (см. главу
12)
Представляет System.Uri
Произвольный остров
данных XML, который
остается непрозрачным
для анализатора XAML
(см. главу 13)

В табл. 2.3 перечислены дополнительные элементы пространства имен XAML, которые
можно принять за ключевые слова, хотя на самом деле это расширения разметки
(реальные классы .NET в пространстве имен System.Windows.Markup). Суффикс Extension
в именах классов опущен, поскольку они обычно используются без суффикса.
Таблица 2.3. Расширения разметки в пространстве имен языка XAML в
предположении, что префикс пространства имен х определен стандартным образом
Расширение
Назначение
x:Array
Представляет массив .NET. Потомками
элемента х:Array являются элементы
массива. В элементе должен
присутствовать атрибут х :Туре,
определяющий тип массива
x:Null
Представляет ссылку null

96

Глава2. Все тайны XAML
Таблица 2.3 (продолжение)
Расширение
x:Reference

x:Static

x:Type

Назначение
Ссылка на именованный элемент. Должен
присутствовать единственный
позиционный параметр, задающий имя
этого элемента
Ссылка на любое статическое свойство,
поле, константу или элемент
перечисления, определенные в
процедурном коде. При компиляции
XAML это может быть даже неоткрытый
член, определенный в той же сборке.
Строка Member должна быть
квалифицирована префиксом
пространства имен XML, если тип не
находится в пространстве имен по
умолчанию
Представляет экземпляр типа System.Type
так же, как оператор typee в С#. Строка
TypeName должна быть квалифицирована
префиксом пространства имен XML, если
тип не находится в пространстве имен по
умолчанию

Резюме
Мы рассмотрели, как XAML сочетается с WPF, и - что самое главное - теперь вы
располагаете всей необходимой информацией для перевода почти всех примеров XAML
на язык типа C# и наоборот. Однако, поскольку конвертер типов и расширения разметки «черные ящики», прямой перевод не всегда очевиден. Но в любом случае можно вызвать
конвертер типа напрямую из процедурного кода, если непонятно, как именно выполняет
преобразование компилятор! (Многие классы, для которых имеются конвертеры типов,
даже содержат открытый статический метод Parse, который делает то же самое,
исключительно ради упрощения процедурного кода.)
Мне очень нравится, что даже простые элементы, которые можно было бы обработать в
XAML специальным образом (например, null или именованные ссылки), выражаются с
помощью того же механизма расширений разметкш который доступен сторонним
разработчикам. Это позволяет сохранить простоту XAML и гарантирует, что механизм
расширения работает действительно хорошо.
По мере дальнейшего изучения WPF вы, возможно, обратите внимание, что некоторые
API WPF в процедурном коде выглядят громоздко, поскольку типизированы для
использования совместно с XAML. Например, в WPF есть много мелких строительных
блоков (что позволяет создавать развитые ком позиции, описанные в предыдущей главе),
поэтому в WPF-приложениях приходится вручную создавать гораздо больше объектов,
чем, скажем, WindowsForms. Мощь XAML особенно наглядно проявляется, когда нужно
кратко описать глубокую иерархию объектов, поэтому команда разработчик WPF
потратила больше времени на реализацию средств, которые позволяют скрыть
промежуточные объекты в XAML (например, конвертеры типов), средств для их
сокрытия от процедурного кода (например, конструкторов которые создают внутренние
объекты от вашего имени).

Резюме

97

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

Возражение
набирать

1:

XML

слишком

многословен,

долго

Это правда. Никому не нравится вводить длинный XML-код, но ведь есть же
инструменты. Такие средства, как IntelliSense и визуальные конструкторы, могут
избавить вас от необходимости дотошно вводить бесконечные угловые скобки. А
прозрачная и детально разработанная спецификация XML позволяет легко интегрировать
в процесс разработки новые инструментальные средства (например, создать программу
экспорта XAML в формате вашего любимого инструмента), а также вносить в разметку
изменения вручную или искать ошибки.
Более того, для некоторых областей применения WPF - построения сложных путей и
фигур, 3D-моделей и т. д. - вводить XAML вручную практически нереально. На самом
деле по мере развития XAML со времени появления его бета-версии некоторые
ориентированные на человека приемы сокращенного ввода даже были исключены, чтобы
сделать формат более устойчивым и расширяемым, то есть более удобным для
поддержки со стороны инструментальных средств. Но я все же думаю, что знакомство с
XAML и умение видеть API WPF сквозь призму как процедурного, так и декларативного
кода остается лучшим способом изучить технологию. Это как понимание принципов
работы HTML без использования визуальных средств разработки.

Возражение
2:
системы,
низкопроизводительны

основанные

на

XML,

Язык XML создавался ради интероперабельности, а не ради максимально эффективного
представления данных. Так зачем нагружать WPF-приложения кучей данных
относительно большого объема, которые к тому же медленно разбираются?
Но ведь в типичном сценарии применения WPF XML компилируется в BAML, поэтому
на этапе выполнения вы не платите полную цену за размер и низкую производительность
разбора. BAML и меньше по размеру, чем исходный XAML, и оптимизирован для
эффективного исполнения. Таким образом, все отрицательные стороны XML в плане
производительности проявляются на этапе разработки, то есть там, где выгоды от
использования XML нужнее всего.

3
Основные принципы WPF
•
•
•

Обзор иерархии классов
Логические и визуальные деревья
Свойства зависимости

Перед тем как завершить часть I и переити к действительно интересным вещам, будет
полезно поговорить о некоторых важных концепциях WPF, с которыми программисты
.NET ранее не были знакомы. Именно из-за обсуждаемых в этой главе вопросов у WPF
установилась репутация технологии, очень сложной для изучения. Поэтому чем раньше
мы разберемся с ними, тем увереннее вы будете чувствовать себя при чтении этой книги и
другой документации по WPF.
Некоторые темы в этой главе не имеют никаких аналогов в прежнем опыте (например,
логические и визуальные деревья), другие являются обобщениями хорошо известных
понятий (скажем, свойства). По мере изложения мы будем демонстрировать применение
изучаемых концепций на примере очень простого элемента пользовательского
интерфейса — диалогового окна About (О программе).

Обзор иерархии классов
Классы, входящие в состав WPF, образуют очень глубокую иерархию наследования,
поэтому сразу трудно уложить в голове назначение и взаимосвязи различных классов. Но
есть несколько фундаментальных для работы WPF классов, о которых стоит упомянуть,
прежде чем двигаться дальше. На рис. 3.1 показаны 12 наиболее важных классов и
соотношения между ними.
 Object - базовый класс, которому наследуют все остальные классы .NET, и
единственный из представленных на рисунке, не имеющий прямого отношения к
WPF.
 DispatcherObject - базовый класс, предназначенный для объектов, к которым
можно обращаться только из того потока, где они были созданы. Большинство
классов WPF наследуют DispatcherObject и, следовательно, принципиально
небезопасны относительно потоков. Слово Dispatcher в имени класса относится

Обзор иерархии классов

99

к реализованному в WPF варианту цикла обработки сообщений Win32, который мы еще
будем обсуждать в главе 7.
 DependencyObject - базовый класс, предназначенный для объектов, поддерживающих свойства зависимости; это одна из центральных тем данной главы.
 Freezable - базовый класс для объектов, которые можно «заморозить в состоянии,
разрешающем только чтение, - ради повышения производительности. К
замороженным объектам можно безопасно обращаться из разных потоков, в
отличие от объектов всех прочих классов, производных от DispatcberObject.
Замороженный объект нельзя разморозить, однако можно клонировать, в
результате чего получается незамороженная копия. По большей части объекты
Freezable - это графические примитивы: кисти, перья, геометрические фигуры и
классы анимации.
 Visual - базовый класс для объектов, имеющих двумерное визуальное
представление. Визуальные объекты подробно рассматриваются в главе 15
«Двумерная графика».
 UlElement - базовый класс для двумерных визуальных объектов с поддержкой
маршрутизации событий, привязки команд, компоновки и захвата фокуса. Эти
механизмы обсуждаются в главе 5 «Компоновка с помощью панелей» и в главе 6
«События ввода: клавиатура, мышь, стилус и мультисенсорные устройства».
 Visual3D — базовый класс для объектов, имеющих трехмерное визуальное
представление. Рассматривается в главе 16 «Трехмерная графика».
 UIElement3D - базовый класс для трехмерных визуальных объектов с поддержкой
маршрутизации событий, привязки команд и захвата фокуса. Также
рассматривается в главе 16.

Рис - 3.1. Важнейшие классы, составляющие основу WPF

100

Глава3. Основные принципы WPF


ContentElement - базовый класс, аналогичный UIElement, но предназначенный для
тех частей содержимого, которые относятся к документам и потому не имеют
собственного механизма визуализации. Чтобы объект типа ContentElement
появился на экране, им должен владеть объект класса, производного от Visual.
Часто для правильной визуализации объект ContentElement нуждается в
нескольких объектах Visual (охватывающих несколько строк, столбцов и страниц).
 FrameworkElement - базовый класс, добавляющий поддержку стилей, привязки к
данным, ресурсов и нескольких механизмов, общих для всех элементов
управления в Windows, в частности всплывающих подсказок и контекстных меню.
 FrameworkContentElement - аналог FrameworkElement для содержимого. Этот класс
рассматривается в главе 11 «Изображения, текст и другие элементы управления».
 Control - базовый класс для таких хорошо знакомых элементов управления, как
Button, ListBox и StatusBar. Класс Control добавляет к своему базовому классу
FrameworkElement множество свойств, например Foreground, Background и
FontSize, а также возможность тотального изменения стиля. Элементы управления
WPF рассматриваются в части III.
В этой главе словом «элемент» без уточнений мы будем обозначать объект класса,
производного от UIElement или FrameworkElement, а иногда от ContentElement или
FrameworkContentElement. Разница между UIElement и FrameworkElement или
ContentElement и FrameworkContentElement здесь несущественна, потому что в WPF нет
никаких других открытых подклассов UIElement и ContentElement.

Логические и визуальные деревья
Естественность применения языка XAML для описания пользовательских интерфейсов
объясняется его иерархической природой. В WPF пользовательский интерфейс
представляет собой дерево объектов, которое называется логическим деревом.
В листинге 3.1 приведен первый вариант описания гипотетического диалогового окна
About (0 программе), в котором корнем логического дерева является объект Window. У
Window имеется дочерний элемент StackPanel (см. главу 5), содержащий несколько
простых элементов управления и еще один элемент StackPanel, который содержит две
кнопки Button.
Листинг 3.1. Описание простого диалогового окна About на XAML






Chapter 1
Chapter 2





You have successfully registered this product.



На рис. 3.2 показано, как выглядит это диалоговое окно (можете убедиться в этом,
скопировав текст листинга 3.1 в программу XAMLPAD2009, описанную в предыдущей
главе), а на рис. 3.3 - логическое дерево окна.

Рис. 3.2. Окно, соответствующее коду в листинге 3.1

Рис. 3.3. Логическое дерево, соответствующее коду в листинге 3.1

102

Глава3. Основные принципы WPF

Отметим, что логическое дерево существует даже для WPF-интерфейсом, созданных без
участия XAML. Логику листинга 3.1 можно было бы реализовать чисто процедурно, а
логическое дерево при этом не изменилось бы.
Идея логического дерева представляется очевидной, так зачем о нем вообще говорить?
Затем, что поведение чуть ли не всех механизмов WPF (свойств, событий, ресурсов и т.д.)
так или иначе связано с логическим деревом. Например, значения свойств иногда
автоматически распространяются вниз по дереву на дочерние элементы, а генерируемые
события могут распространяться как вниз, так и вверх. Такое поведение свойств
обсуждается ниже в этой главе, а поведение событий - в главе 6.
Логическое дерево в WPF фактически дает упрощенную картину того, что в
действительности происходит при визуализации элементов. Полное дерево содержащее
все визуализированные элементы, называется визуальным деревом. Можно представлять
себе визуальное дерево как раскрытое логической дерево, в котором каждый узел
является вершиной поддерева, содержащего его визуальные компоненты. Иными
словами, в визуальном дереве каждый элемент уже не «черный ящик», а раскрывает все
детали своей визуальной реализации. Например, ListBox - логически единый элемент
управления, однако по умолчанию его визуальное представление состоит из более
простых WPF-элементов: рамки Border, двух полос прокрутки ScrollBar и др.
В визуальном дереве представлены не все узлы логического дерева, а лишь элементы,
производные от классов System.Windows.Media.Visual или System.Windows.Media.Visual3D.
Прочие элементы (в том числе простые строки, присутствующие в листинге 3.1) не
включаются, потому что не обладают собственник поведением визуализации.

СОВЕТ
Некоторые несложные программы просмотра XAML, в частности XamlPadX,
упомянутая в предыдущей главе, позволяют просматривать визуальное дерево (и
значения свойств) объектов, визуализированных на основе XAML-кода.
На рис. 3.4 изображено визуальное дерево, получающееся при выполнении кода из
листинга 3.1 в системе Windows 7 с темой Aero. Здесь представлены внутренние
компоненты пользовательского интерфейса, которые в настоящий момент невидимы,
например две полосы прокрутки ScrollBar элемента ListBox и рамки Border всех меток
Label. Видно также, что элементы ButtonLabel и ListBoxItem составлены из одних и тех
же элементов за одним исключением - в Button вместо Border используется скрытый
элемент ButtonChrome. (У этих элементов управления имеются и другие визуальные
различия, обусловленые тем, что по умолчанию подразумеваются разные значения
свойств. Например, у кнопки Button поле Margin для всех четырех сторон по умолчанию
равно 10, а у метки Label поле равно 0.)

Логические и виртуальные деревья

Рис. 3.4. Визуальное дерево для листинга 3.1; выделены узлы логического дерева

103

104

Глава3. Основные принципы WPF

В визуальном дереве отражено внутреннее устройство WPF-элементов, поэтому оно
может оказаться весьма сложным. К счастью, несмотря на то, что визуальное дерево
является существенной частью инфраструктуры WPF, задумываться о нем имеет смысл,
только если вы собираетесь радикально изменять стили элементов управления (см. главу
14 «Стили, шаблоны, обложкв и темы») или выполнять низкоуровневое рисование (см.
главу 15). В частности, написание кода, зависящего от конкретной структуры
визуального дерева элемента Button, противоречит одному из основополагающих
принципу WPF - отделению внешнего вида от логики. Если кто-нибудь решит изменить
стиль кнопки, применяя технику, описанную в главе 14, то ее стандартное визуальное
дерево может быть заменено чем-то совершенно непохожим.

ПРЕДУПРЕЖДЕНИЕ
Если логическое дерево остается статичным, пока не вмешается программиста
(который может, например, динамически добавить или удалить элементы), то
визуальное дерево может измениться просто потому, что пользователь выбрал другую
тему Windows.
Однако логическое и визуальное деревья можно обойти с помощью взаимодополняющих
классов System.Windows.LogicalTreeHelper и System.Windows.Media.VisualTreeHelper. В
листинге 3.2 показан застраничный код для листинга 3.1, который при запуске в
отладчике выводит простое представление логическиго и визуального деревьев
диалогового окна About в порядке обхода в глубину. (Чтобы присоединить этот
процедурный код, необходимо добавить в листинг 3.1 атрибут x:Class="AboutDialog" и
соответствующую директиву xmlns:x.)
Листинг 3.2. Обход и распечатка логического и визуального деревьев
using
using
using
using

System;
System.Diagnostics;
System.Windows;
System.Windows.Media;

public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
PrintLogicalTree(0, this);
}
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
PrintVisualTree(0, this);
}

Логические и виртуальные деревья

105

void PrintLogicalTree(int depth, object obj)
{
// Напечатать объект с предшествующими пробелами,
// число которых соответствует глубине вложенности
Debug.WriteLine(new string(' ', depth) + obj);
// Иногда листовые узлы не принадлежат классу
// DependencyObject (например, строки)
if (!(obj is DependencyObject)) return;
// Рекурсивный вызов для каждого логического
// дочернего узла
foreach (object child in LogicalTreeHelper.GetChildren(
obj as DependencyObject))
PrintLogicalTree(depth + 1, child);
}
void PrintVisualTree(int depth, DependencyObject obj)
{
// Напечатать объект с предшествующими пробелами,
// число которых соответствует глубине вложенности
Debug.WriteLine(new string(' ', depth) + obj);
// Рекурсивный вызов для каждого логического
// дочернего узла
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}

Если вызвать любой из этих методов с параметром depth, равным 0, и текущим
экземпляром Window в качестве параметра obj, то получится текстовое представление
дерева с тем же узлами, что на рис. 3.2 и 3.3. Логическое дерево можно обойти внутри
конструктора, однако визуальное дерево остается пустым до первой компоновки элемента
Window. Именно поэтому метод PrintVisualTree вызывается в обработчике события
OnContentRendered, который выполняется после завершения компоновки.

СОВЕТ
Визуальные деревья, аналогичные изображенному на рис. 3.4, часто называют просто
деревьями элементов, поскольку они содержат элементы как логического, так и
визуального дерева. В таком случае термином «визуальное дерево» обозначают любое
поддерево, содержащее только визуальные (нелогические) элементы. Например, многие
сказали бы, что стандартное визуальное дерево Window содержит Border,
AdornerDecorator, два элемента AdornerLayer, ContentPresenter и больше ничего. Самая
верхняя панель StackPanel на рис. 3.4 обычно не рассматривается как визуальный
дочерний элемент ContentPresenter, хотя VisualTreeHelper и считает ее таковым.

106

Глава3. Основные принципы WPF

Обход и того и другого дерева иногда можно выполнить с помощью методов экземпляра
самих элементов. Например, в классе Visual есть три защищенных члена (VisualParent,
VisualChildrenCount и GetVisualChild) для доступа к родителю и потомкам визуального
элемента. А в классе FrameworkElement, базовом таких элементов управления, как Button
и Label, и в дополняющем его классе FrameworkContentElement имеются открытое
свойство Parent, которое представляет логического родителя, и защищенное свойство
LogicalChildren, описывающее список логических дочерних элементов. В подклассы этих
классов часто включают открытые члены, обеспечивающие тот или иной доступ к
логическим дочерним элементам, например открытую коллекцию Children. Некоторые
классы, к примеру Button и Label, раскрывают свойство Content и гарантируют наличие
только одного логического дерева.

СОВЕТ
В отладчике Visual Studio 2010 щелчок по значку лупы рядом с экземпляром класса,
производного от Visual, позволяет исследовать его визуальное дерево.

Свойства зависимости
В WPF появился новый тип свойств - свойства зависимости; они повсеместно
используются для реализации таких механизмов, как назначение стилей, автоматическая
привязка к данным, анимация и др. Поначалу вы, возможно, отнесетесь к этой идеи
скептически, потомучто она вносит дополнительные сложности в картину типов .NET,
где имеются простые поля, свойства, методы и события Но, поняв, какие именно задачи
решают свойста зависимости, вы, скорее всего, сочтете их желанным нововведением.
Свойство зависимости зависит от нескольких поставщиков, которые определяют его
значение во время выполнения. Поставщиком может быть анимация, постоянно
изменяющая значение свойства, родительский элемент, распространяющий значение
своего свойства на потомков, и т. д. Пожалуй, наиболее существенной особенностью
свойства зависимости является встроенная возможность генерировать уведомления об
изменениях своего значения!
Причина наделения свойств подобной интеллектуальностью— стремление описать
развитую функциональность на уровне декларативной разметки, механизм
декларативного описания интерфейсов в WPF опирается на использовании свойств. К
примеру, в классе Button имеется 111 открытых свойств (из них 98 наследуются от класса
Control и его предков)! Свойства можно задавать в XAML-коде (непосредственно или с
помощью инструментов конструирования) без написания процедурного кода. Но в
отсутствие механизмов, предлагаемых свойствами зависимости, было бы весьма трудно
получить желаемый результат без дополнительного кода.

Свойства зависимости

107

В этом разделе мы сначала вкратце ознакомимся с реализацией свойства зависимости,
чтобы сделать обсуждение более предметным, а потом внимательнее рассмотрим, как
свойства зависимости расширяют функциональность обычных свойств .NET в
следующих направлениях:
 Уведомление об изменениях
 Наследование значений свойств
 Поддержка нескольких поставщиков
Понимать большинство нюансов свойств зависимости необходимо только авторам
нестандартных элементов управления. Но даже обычный пользователь WPF должен
знать, что такое свойства зависимости и как они работают. Например, применять стили и
анимацию можно только к свойствам зависимости. Немного поработав с WPF, вы еще
будете жалеть, что не все свойства элементов являются свойствами зависимости!

Реализация свойства зависимости
На практике свойство зависимости - это обычное свойство .NET, которое включено в
состав дополнительной инфраструктуры, предоставляемой WPF. Для этого в WPF
предусмотрены специальные API; ни один .NET-совместимый язык программирования
(кроме XAML) ничего не знает о свойствах зависимости.
В листинге 3.3 показано, как в классе Button реализовано свойство зависимости IsDefault.
Листинг 3.3. Реализация стандартного свойства зависимости
public class Button : ButtonBase
{
// Свойство зависимости
public static readonly DependencyProperty IsDefaultProperty;
static Button()
{
// Зарегестрировать свойство
Button.IsDefaultProperty = DependencyProperty.Register(‚IsDefault‛,
typeof(bool), typeof(Button),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(OnIsDefaultChanged)));
...
}
// Обертка в виде обычного свойства .NET (необязательно)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}

108
//

Глава3. Основные принципы WPF
Метод, вызываемый при изменении свойства (необязательно)
private static void OnIsDefaultChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e) { … }
...

}

Статическое поле IsDefaultProperty типа System.Windows.DependencyProperty и является
свойством зависимости. По принятому соглашению все поля тип DependencyProperty
открытые, статические и имеют имя, оканчивающееся словом Property. Некоторые
компоненты инфраструктуры требуют обязательного соблюдения этого соглашения,
например средства локализации, загрузчики XAML и пр.
Обычно
для
создания
свойства
зависимости
вызывается
метод
DependencyProperty.Register - ему передается имя свойства (IsDefault), его тип (bool) и тип
класса, который будет владельцем свойства (Button). Дополнительно (с помощью
перегруженных вариантов метода Register) можно передать метаданные, уточняющие, как
WPF должна интерпретировать это свойство, а также обратные вызовы для обработки
изменения значения свойства, приведения тая па значения и проверки значения. В классе
Button метод Register вызывается в статическом конструкторе; при этом свойству
присваивается значение по-умолчанию false и задается делегат, который будет вызываться
в ответ уведомление об изменении.
Далее приведено обычное свойство IsDefault. Его аксессоры вызывают методы GetValue и
SetValue, унаследованные от System.Windows.DependencyObject, низкоуровневого
базового класса, которому должны наследовать все свойства зависимости. Метод
GetValue возвращает последнее значение, переданное SetValue, или, если метод SetValue
еще ни разу не вызывался, значение по умолчанию, заданное при регистрации свойства.
Обычное свойство .NET IsDefaulft (в этом контексте его иногда называют обертывающим
свойством) определять необязательно; клиенты класса Button могут напрямую
обращаться к методам GetValue и SetValue, поскольку они открыты. Однако наличие
свойства .NET позволяет клиентам более естественно программировать чтение и изменение значения, а кроме того, только таким образом можно установить свойство в XAML.
Со стороны WPF было бы правильно предоставить универсальные перегруженные
варианты GetValue и SetValue. Но это не сделано в первую очередь потому, что свойства
зависимости появились до того, как универсальные типы .NET получили широкое
распространение.

СОВЕТ
В дистрибутиве Visual Studio имеется сниппет propdp, который автоматически
генерирует определение свойства зависимости. Это намного быстрее, чем вводить
определение вручную!

Свойства зависимости

109

ПРЕДУПРЕЖДЕНИЕ
Во время выполнения обертывающие свойства .NET не вызываются при
задании значений свойств зависимости в XAML!
Хотя компилятор XAML требует, чтобы обертывающее свойство присутствовало, на
этапе выполнения WPF напрямую обращается к методам GetValue и SetValue. Поэтому
во избежание несогласованности между результатами установки свойства в XAML и в
процедурном коде не следует помещать в обертывающее свойство какой-нибудь код
помимо вызова GetValue/SetValue. Для реализации дополнительной логики
предназначены методы обратного вызова, задаваемые при регистрации. Все
стандартные обертывающие свойства в WPF следуют этому правилу, так что
предупреждение адресовано авторам новых классов, содержащих свойства
зависимости.

При поверхностном взгляде листинг 3.3 кажется излишне многословным способом
представить простое булевское свойство. Однако поскольку в реализации GetValue и
SetValue используется весьма эффективная система разреженного хранения, a
IsDefaultProperty - статическое поле (а не поле экземпляра), то на практике свойства
зависимости даже позволяют сэкономить память, выделяемую под экземпляр, по
сравнению с обычными свойствами .NET. Если бы все свойства элементов управления
WPF были обертками полей экземпляра (как большинство свойств .NET), то потребление
памяти существенно возросло бы из-за объема связанных с каждым экземпляром локальных данных. Только представьте себе - 111 полей для каждой кнопки, 104 поля для
каждой метки и т. д.! Но в действительности 89 из 111 открытых свойств класса Button и
82 из 104 открытых свойств класса Label - это свойства зависимости.
И экономией памяти достоинства свойств зависимости не исчерпываются. Реализация
устроена так, что код для доступа к свойству из разных потоков для извещения элементавладельца о необходимости повторной визуализации и многого другого централизован и
стандартизован, так что авторам свойств писать его не придется. Например, если после
изменения значения свойства необходимо перерисовать элемент (как в случае свойства
Background
класса
Button),
то
достаточно
указать
флаг
FrameworkPropertyMetadataOptions.AffectsRender при вызове перегруженного варианта
метода DependencyProperty.Register. Кроме того, реализация поддерживает три
вышеупомянутых механизма, которые мы теперь рассмотрим более подробно.

Уведомление об изменении
При изменении значения свойства зависимости WPF может автоматически
инициировать некоторые действия в соответствии с метаданными свойства. Это может
быть перерисовка элементов, пересчет компоновки, обновление привязки к данным и
многое другое. Одна из самых интересных черт встроенного механизма уведомления об
изменении — триггеры свойств, которые позволяют ассоциировать с изменением

110

Глава3. Основные принципы WPF

запрограммированные вами действия без написания процедурного кода.
Пусть, например, требуется, чтобы при наведении указателя мыши на кнопку в
диалоговом окне About в листинге 3.1 надпись на кнопке становилась синей. Без триггеров
свойств для этого нужно было бы присоединить к каждой кнопке обработчики событий
MouseEnter и MouseLeave:



Сами обработчики можно реализовать в застраничном коде на C# следующим образом:
// Сделать цвет фона синим, когда указатель находится над кнопкой
void Button_MouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null) b.Foreground = Brushes.Blue;
}
// Восстановить черный цвет фона, когда указатель покидает кнопку
void Button_MouseLeave(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null) b.Foreground = Brushes.Black;
}

А триггер свойства позволяет реализовать такое же поведение целиком на XAML.
Достаточно добавить такое коротенькое описание объекта Trigger:




Этот триггер срабатывает при изменении свойства IsMouseOver объекта Button, которое
принимает значение true одновременно с генерацией события MouseEnter и значение false
- при генерации события MouseLeave. Обратите внимание, что восстанавливать черный
цвет фона, когда IsMouseOver становится равным false, не нужно. WPF сделает это
автоматически!
Единственная проблема заключается в том, как ассоциировать этот триггер с каждой
кнопкой. К сожалению, из-за досадного ограничения невозможно применять триггеры
непосредственно к элементам, в частности к Button. Они могут располагаться только
внутри объекта Style, поэтому подробное смотрение триггеров свойств мы отложим до
главы 14. А пока, чтобы поэкспериментировать с применением триггера к кнопке, можете
добавить несколько промежуточных XML-элементов:

Свойства зависимости

111



Триггеры свойств - лишь один из трех видов триггеров, поддерживаемых WPF. Триггер
данных - разновидность триггера свойства, работающая для произвольных свойств .NET (а
не только свойств зависимости); такие триггеры также рассматриваются в главе 14. Триггер
события позволяет декларативно описывать, какие действия следует предпринять при
генерации маршрутизируемого события (см. главу 6). Триггеры событий всегда
подразумевают наличие анимации или звукового сопровождения, поэтому мы отложим их
рассмотрение до главы 17 «Анимация».

ПРЕДУПРЕЖДЕНИЕ
Не обманывайтесь насчет свойства элемента Triggers!
Свойство Triggers класса FrameworkElement содержит допускающую чтение и запись
коллекцию объектов типа TriggerBase (общий базовый класс всех трех типов
триггеров) - на первый взгляд это очень простой способ присоединить триггеры
свойств к таким элементам, как Button. Но, увы, эта коллекция может содержать только
триггеры событий, так что ее название и тип обманчивы. Попытка добавить в
коллекцию триггер свойства (или данных) приведет к исключению во время
выполнения.

Наследование значений свойств
Словосочетание «наследование значений свойств» (или просто «наследование свойств»)
относится не к традиционному для объектно-ориентированного программирования
наследованию классов, а к распространению значений свойств вдоль дерева элементов. В
листинге 3.4 приведен простой пример, расширяющий код из листинга 3.1, - мы явно
задали в элементе Window свойства зависимости FontSize и FontStyle. На рис. 3.5 показан
результат такого изменения. (Отметим, что благодаря удобному атрибуту SizeToContent
размер элемента Window автоматически подстраивается под размер содержимого!)

112

Глава3. Основные принципы WPF

Рис. 3.5. Диалоговое окно About, в котором для корневого элемента Window
установлены свойства FontSize и FontStyle

Листинг 3.4. Диалоговое окно About, в котором для корневого элемента Window
установлены свойства шрифта






Chapter 1
Chapter 2





You have successfully registered this product.



В большинстве случаев оба эти свойства распространяются вниз по дереву и
наследуются всеми потомками. Это относится даже к элементам Button и Boxltem,
которые расположены на три уровня ниже в логическом дереве. Свойство FontSize
первой метки Label не изменяется, потому что для нее явно указано значение FontSize 20,
отменяющее унаследованное значение 30. Напротив,

Свойства зависимости

113

значение FontStyle наследуется всеми элементами Label, ListBoxItem и Button, поскольку
явно оно нигде не переопределено.
Отметим, что на текст в строке состояния StatusBar эти свойства не оказывают влияния,
хотя класс StatusBar и поддерживает их, как, впрочем, любой элемент управления. В
подобных случаях поведение механизма наследования свойств видоизменяется по двум
причинам.
 Не всякое свойство зависимости принимает участие в наследовании свойств. (На
самом деле желание подключиться к этому механизму выражается явно путем
передачи флага FrameworkPropertyMetadataOptions.Inherits при вызове метода
DependencyProperty.Register.)
 Могут существовать другие источники значения свойства с более высоким
приоритетом (см. следующий раздел).
В данном случае наблюдаемое поведение обусловлено второй причиной. Некоторые
элементы управления, в частности StatusBar, Menu и ToolTip, устанавливают для себя
свойства шрифта в соответствии с текущими системными настройками. Это дает
пользователю возможность настраивать шрифты привычным способом - с помощью
панели управления. Но результат может обескуражить разработчика WPF-приложения,
потому что такие элементы препятствуют распространению наследования на
расположенные под ними части дерева элементов. Например, если в листинге 3.4
добавить Button в качестве логического дочернего элемента StatusBar, то свойства
FontSize и FontStyle сохранят подразумеваемые по умолчанию значения 12 и Normal
соответственно и, следовательно, эта кнопка будет отличаться от других кнопок,
расположенных вне StatusBar.

КОПНЕМ ГЛУБЖЕ
Наследование значений свойств в других местах
Механизм наследования свойств первоначально был разработан для дерева элементов,
а затем перенесен на некоторые другие контексты. Например, распространяющиеся
вниз значения могут применяться к элементам, которые выглядят как потомки с точки
зрения XML (благодаря синтаксису элементов свойств в XAML), но не являются
потомками в понимании логических или визуальных деревьев. Такими
псевдопотомками могут быть триггеры, присоединенные к элементам, или значения
любого свойства (а не только Content или Children) при условии, что объект является
производным от класса Freezable. Такое решение может показаться произвольным и к
тому же плохо документировано, но смысл его в том, чтобы в некоторых сценариях
XAML «просто работал» естественным образом, не требуя от вас никакого внимания.

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

114

Глава3. Основные принципы WPF

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

Рис. 3.6. Последовательность вычисления значения свойства зависимости

Шаг 1: определение базового значения
Свой вклад в вычисление базового значения вносят почти все поставщики значений
свойств. Ниже приведен перечень десяти поставщиков, которые могут устанавливать
значения большинства свойств зависимости, - в порядке убывания приоритета:
1.
Локальное значение
2.
Триггер в шаблоне родителя
3.
Шаблон родителя
4.
Триггеры в стиле
5.
Триггеры в шаблоне
6.
Установщики стиля
7.
Триггеры стиля темы
8.
Установщики стиля темы
9.
Наследование значения свойства
10. Значение по умолчанию
С некоторыми поставщиками значений свойств вы уже встречались, например, с
механизмом наследования свойств (9). Локальное значение (1) технически означает
любое обращение к методу DependencyObject.SetValue, но обычно имеет вид простого
присваивания свойства в XAML или процедурном коде (в силу способа определения
свойств зависимости, проиллюстрированного выше в примере свойства Button.IsDefault).
Под значением по умолчанию (10) понимается начальное значение, указанное при
регистрации свойства зависимости понятно, что его приоритет наименьший. Остальные
поставщики связаны со стилями и шаблонами, поэтому их рассмотрение мы отложим до
главы 14.
Описанная выше расстановка приоритетов объясняет, почему механизм наследования
значений свойств не оказал влияния на свойства Style элемента StatusBar в листинге 3.4.
Установка свойств шрифта в соответствии с настройками системы производится на
уровне установщиков стилей темы (8). Хотя приоритет этого поставщика выше, чем у
механизма следования свойств (9), переопределить параметры шрифта все равно можно

Свойства зависимости

115

достаточно воспользоваться поставщиком с еще более высоким приоритетом, например,
просто задать свойства локально в самом элементе StatusBar.

СОВЕТ
Если вы не можете понять, в какой именно момент данное свойство зависимости
получает значение, то попробуйте воспользоваться статическим методом DependencyPropertyHelper.GetValueSource. Он возвращает структуру ValueSource, содержащую несколько полей: перечисление BaseValueSource, которое показывает источник
базового значения (шаг 1) и булевские свойства IsExpression, IsAnimated и isCoerced,
содержащие информацию о шагах 2-4.
Если вызвать этот метод для элемента StatusBar в листинге 3.1 или 3.4, запросив
сведения о свойстве FontSize или FontStyle, то в качестве BaseValueSource будет
возвращено DefaultStyle, показывающее, что значение присвоено установщиком стиля
темы. (Стили, заданные в темах, иногда называют стилями по умолчанию. Триггеру
стиля темы соответствует элемент перечисления DefaultStyleTrigger.)
Не используйте этот метод в промышленном коде! В будущих версиях WPF допущения, сделанные вами относительно вычисления значения, могут оказаться
ложными. Кроме того, различная обработка значения свойства в зависимости от его
источника нарушает принципы проектирования WPF-приложений.

КОПНЕМ ГЛУБЖЕ
Очистка локального значения
Выше, в разделе «Уведомление об изменении», был продемонстрирован процедурный
код, который изменяет цвет фона кнопки на синий в ответ на событие MouseEnter и
восстанавливает черный цвет фона в ответ на событие MouseLeave. Проблема в том, что
в обработчике MouseLeave черный цвет устанавливается как локальное значение, тогда
как в начальном состоянии Button цвет фона поступает от установщика стиля из темы.
Если впоследствии будет выбрана другая тема и установщик попробует изменить
значение свойства Foreground (или же самое попробует сделать поставщик с более
высоким приоритетом), то попытка закончится неудачно, так как черный цвет
установлен локально.
На самом деле надо было бы очистить локальное значение и дать WPF возможность
установить его заново, получив значение от применимого поставщика с самым высоким
приоритетом. К счастью, в классе DependencyObject имеется как раз такой механизм:
метод ClearValue. В C# его можно вызвать от имени объекта Button b:
b.ClearValue(Button.ForegroundProperty);

(Button.ForegroundProperty - статическое поле класса DependencyProperty.) После вызова
ClearValue WPF пересчитывает базовое значение, просто не принимая во внимание
локальное.
Отметим, что триггер для свойства IsMouseOver, показанный в разделе «Уведомление
об изменении», не подвержен этой проблеме. Триггер либо активен, либо нет, а
неактивные триггеры при вычислении значения свойства игнорируются.

116

Глава3. Основные принципы WPF

Шаг 2: вычисление
Если значение, полученное на шаге 1, представляет собой выражение (объект класса,
производного от System. Windows. Expression), то WPF выполняет специальный шаг
вычисления для преобразования выражения в конкретное значение. Выражения чаще
всего появляются в результате привязки к данным (это тема главы 13).

Шаг 3: применение анимаций
Если работает одна или несколько анимаций, то любая из них способна изменить
текущее значение свойства (получив на входе значение, вычисленное на шаге 2) или
вообще подменить его. Таким образом, анимации (тема главы 17) могут отменить
решения всех прочих поставщиков значений - даже локальных! Начинающие изучать
WPF часто попадают в эту ловушку.

Шаг 4: приведение
После того как все поставщики значения свойства сказали свое слово, WPF передает
почти окончательное значение делегату CoerceValueCallback, если таковой был указан
при регистрации свойства зависимости. Этот делегат должен вернуть новое значение,
применяя соответствующую случаю логику. Например, встроенный в WPF элемент
управления ProgressBar с помощью подобного делегата приводит значение свойства
зависимости Value диапазона от Minimum до Maximum, то есть возвращает Minimum,
если входное значение меньше Minimum, и Maximum - если оно больше Maximum. Если
логика приведения изменяется во время работы программы, то можно вызвать метод
CoerceValue и заставить WPF заново выполнить шаги приведения и проверки.

Шаг 5: проверка
Наконец приведенное значение передается делегату ValidateValueCallback, если таковой
был указан при регистрации свойства зависимости. Он должен вернуть true, если входное
значение допустимо, и false в противном случае. Если возвращается false, то WPF
возбуждает исключение, отменяя все проделанные вычисления.

СОВЕТ
В версии WPF 4 в класс DependencyОbject добавлен новый метод SetCurrentValue. Он
напрямую обновляет текущее значение, не изменяя его источник. (Приведение типа и
проверка по-прежнему производятся.) Этот метод предназначен для элементов
управления, которые устанавливают значения в ответ на действия пользователя.
Например, элемент RadioButton изменяет значение свойства IsChecked других
элементов RadioButton в той же группе, когда пользователь выбирает новый вариант
переключателя. В прежних версиях WPF в таких случаях устанавливалось локальное
значение, а значит, игнорировались все прочие источники значений. Это могло
привести, например, к некорректной работе механизма привязки к данным. В WPF 4
класс RadioButton модифицирован и теперь пользуется методом SetCurrentValue.

Свойства зависимости

117

Присоединенные свойства
Присоединенное свойство - это частный случай свойства зависимости, которое можно
присоединять к произвольным объектам. Поначалу это может вас озадачить, однако у
этого механизма есть несколько применений a WPF.
Предположим, что в примере диалогового окна About свойства FontSize и FontStyle
заданы не для всего окна Window (как в листинге 3.4), а только для внутренней панели
StackPanel, чтобы они наследовались лишь двумя кнопками Button. Однако перенос
атрибутов свойств во внутренний элемент StackPanel работать не будет, потому что в
классе StackPanel нет никаких свойств, относящихся к шрифту! Поэтому необходимо
использовать присоединенные свойства FontSize и FontStyle, определенные в классе
TextElement. В листинге 3.5 продемонстрирован синтаксис XAML, предназначенный для
задания присоединенных свойств. В результате мы получаем желаемое наследование
значений свойств (рис. 3.7).

Puc. 3.7. Диалоговое окно About, в котором свойства PontSise и FontStyle
обеих кнопках унаследованы от внутренней панели StackPanel
Листинг 3.5. Диалоговое окно About, в котором свойства шрифта перенесены во
внутреннюю панель StackPanel






Chapter 1
Chapter 2





You have successfully registered this product.



118

Глава3. Основные принципы WPF

В элементе StackPanel необходимо записать TextElement.FontSize и TextElement.FontStyle
(а не просто FontSize и FontStyle), потому что в классе StackPanel таких свойств нет.
Когда анализатор или компилятор XAML встречает такой синтаксис, он предполагает,
что в классе TextElement (который иногда называется поставщиком присоединенных
свойств) имеются статические методы SetFontSize и SetFontStyle, которые умеют
устанавливать соответствующие свойства. Поэтому приведенное в листинге 3.5
объявление StackPanel эквивалентно следующему коду на С#:
StackPanel panel = new StackPanel();
TextElement.SetFontSize(panel, 30);
TextElement.SetFontStyle(panel, FontStyles.Italic);
panel.Orientation = Orientation.Horizontal;
panel.HorizontalAlignment = HorizontalAlignment.Center;
Button helpButton = new Button();
helpButton.MinWidth = 75;
helpButton.Margin = new Thickness(10);
helpButton.Content = "Help";
Button okButton = new Button();
okButton.MinWidth = 75;
okButton.Margin = new Thickness(10);
okButton.Content = "OK";
panel.Children.Add(helpButton);
panel.Children.Add(okButton);

Отметим, что элементы перечисления, например FontStyles.Italic, Orientation.Horizontal и
HorizontalAlignment.Center, в XAML-коде записывались просто как Italic, Horizontal и
Center. Это стало возможно благодаря конвертеру типа EnumConverter из каркаса .NET
Framework, который может преобразовывать любые строки без учета регистра букв.
Хотя в XAML-коде в листинге 3.5 логическое присоединение свойств FontSize и FontStyle
к StackPanel выглядит очень изящно, код на C# показывает, что никаких хитростей тут
нет, а есть просто вызов метода, который ассоциируем с элементом постороннее свойство.
Одной из любопытных особенностей абстракции присоединенных свойств является тот
факт, что никакие свойств .NET в ней на самом деле не участвуют!
На внутреннем уровне методы типа SetFontSize просто обращаются к тому методу
DependencyObject.SetValue, который вызывает аксессор обычного свойства зависимости,
но от имени не текущего экземпляра, а переданного объекта DependencyObject:

Свойства зависимости

119

public static void SetFontSize(DependencyObject element, double value)
{
element.SetValue(TextElement.FontSizeProperty, value);
}

А если в присоединенном свойстве определен статический метод GetXXX (где XXX имя свойства), то будет вызываться уже знакомый нам метод DependencyObject.GetValue:
public static double GetFontSize(DependencyObject element)
{
return (double)element.GetValue(TextElement.FontSizeProperty);
}

Как и в случае обертывающих свойств для обычных свойств зависимости, методы
GetXXX и SetXXX не должны делать ничего, кроме вызова методов GetValue и SetValue
соответственно.

КОПНЕМ ГЛУБЖЕ
О поставщиках присоединенных свойств
Самым странным в работе с присоединенными свойствами FontSize и FontStyle в
листинге 3.5 является тот факт, что они определены не в классе Button и даже не в
базовом классе Control, где определены обычные свойства зависимости FontSize и
FontStyle, а в, казалось бы, совершенно не относящемся к делу классе TextElement (а
также в классе TextBlock, которым можно было бы воспользоваться вместо TextElement)!
Но как это может работать, если поле TextElement. FontSizeProperty никак не связано с
полем Control.FontSizeProperty (a TextElement.FontStyleProperty - с полем Control.
FontStyleProperty)? Ключ к решению загадки - способ внутренней регистрации этих
свойств зависимости. В исходном коде класса TextElement имеются такие строки:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached(
"FontSize", typeof(double), typeof(TextElement), new FrameworkPropertyMetadata(
SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(TextElement.IsValidFontSize));

Это похоже на приведенный выше пример регистрации свойства зависимости IsDefault в
классе Button с тем отличием, что метод RegisterAttached оптимизирует обработку
метаданных свойства так, чтобы его можно было использовать в качестве
присоединенного.
Напротив, в классе Control свойство зависимости FontSize не зарегистрировано как
присоединенное! Вместо этого вызывается метод AddOwner для уже зарегистрированного в TextElement свойства, а этот метод возвращает ссылку на уже имеющийся
экземпляр:

120

Глава3. Основные принципы WPF

Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,
FrameworkPropertyMetadataOptions.Inherits));

Поэтому FontSize, FontStyle i все прочие относящиеся к шрифтам свойства зависимости,
наследуемые всеми элементами управления, - это те же самые свойства, что
раскрывает класс TextElement.
К счастью, в большинстве случаев класс, раскрывающий некоторое присоединенное
свойство (методы GetXXX и SetXXX), - это тот же класс, в котором определено
обычное свойство зависимости, так что путаницы не возникает.

КОПНЕМ ГЛУБЖЕ
Присоединенные свойства и механизм расширяемости
Как и в предшествующих технологиях, например Windows Forms, во многих классах
WPF определено свойство Tag (типа System.Object), которое предназначено для
хранения произвольных данных, ассоциированных с экземпляром. Однако
присоединенные свойства - более мощный и гибкий механизм присоединения данных к
объектам классов, производных от DependencyObject. Часто забывают, что
присоединенные свойства позволяют присоединять произвольные данные даже к
экземплярам запечатанных классов (а таких в WPF хватает)!
И еще одно замечание по поводу присоединенных свойств: хотя их установка в XAMLкоде опирается на наличие статического метода SetXXX, при написании процедурного
кода этот метод можно обойти и вызывать метод DependencyObject.SetValue напрямую.
Это означает, что в процедурном коде любое свойство зависимости можно
использовать как присоединенное. Например, в следующем фрагменте к объекту класса
Button присоединяется свойство IsTextSearchEnabled, определенное в классе
ItemsControl, после чего ему присваивается значение:
// Attach an unrelated property to a Button and set its value to true:
okButton.SetValue(ItemsControl.IsTextSearchEnabledProperty, true);

Хотя особого смысла в этом на первый взгляд нет и никакой новой функциональности
в классе Button волшебным образом не появится, тем не менее к значению этого
свойства можно будет обращаться из приложения или компонента и иногда это бывает
полезно.
Есть и более интересные способы расширять элементы подобным образом. Например,
свойство Tag в классе FrameworkElement является свойством зависимости, поэтому его
можно присоединить к экземпляру класса GeometryModel3D (класс, к которому мы еще
вернемся в главе 16, является запечатанным и не ет свойства Tag):
GeometryModel3D model = new GeometryModel3D();
model.SetValue(FrameworkElement.TagProperty, "my custom data");

И это лишь один из многих способов расширения WPF без применения традиция
онного наследования.

Резюме

121

Хотя в примере диалогового окна About присоединенные свойства использовались как
специфический механизм наследования значений свойств, чаще они применяются для
компоновки элементов в макете пользовательского интерфейса. (На самом деле
присоединенные свойства первоначально и были предназначены именно для системы
компоновки в WPF.) В различных производных от Panel классах определены
присоединенные свойства, которые рассчитаны на присоединение к потомкам с целью
задания способа компоновки. Поэтому любая панель Panel может применять свое
поведение к произвольным дочерним элементам, не требуя, чтобы в каждом из них были
определены соответствующие свойства. Заодно это позволяет без особого труда расширять систему компоновки и ей подобные, поскольку любой человек может написать
новый подкласс Panel с нестандартными присоединенными свойствами. В главах 5
«Компоновка с помощью панелей» и 21 «Компоновка с помощью нестандартных
панелей» эта тема рассматривается более подробно.

Резюме
В этой и двух предыдущих главах мы узнали, как на фундаменте каркаса .NET
Framework возводится здание WPF. Разработчики WPF могли бы раскрыть ее механизмы
с помощью типичных для .NET API, как в Windows Forms, и при этом все равно
получилась бы интересная технология. Но они решили поступить по-другому и добавили
несколько принципиально новых концепций, позволяющих раскрыть богатейший набор
возможностей таким способом, который существенно повышает продуктивность работы
программистов и дизайнеров.
Действительно, пристальное изучение новых концепций в этой главе показывает, что
общая картина стала не такой простой, как раньше: появились новые типы свойств,
различные деревья и разные способы достижения одного и того же результата
(декларативный или процедурный код)! Надеюсь, что вы сумеете по достоинству оценить
хотя бы часть этих новых механизмов. Далее в этой книге мы не будем подробно
обсуждать рассмотренные концепции, поскольку основное внимание сосредоточим на
решении конкретных задач разработки приложений.

II
Создание WPF-приложения

Глава 4 «Задание размера, положения и преобразований элементов»
Глава 5 «Компоновка с помощью панелей»
Глава 6 «События ввода: клавиатура, мышь, стилус и мультисенсорные
устройства»
Глава 7 «Структурирование и развертывание приложения»
Глава 8 «Особенности Windows 7»

4
Задание размера,
элементов
•
•
•

положения

и

преобразований

Управление размером
Управление положением
Применение преобразований

При создании WPF-приложения одной из первых встает задача о размещении
многочисленных элементов управления на поверхности окна. Процедура задания
размеров и положений элементов управления (и других элементов) называется
компоновкой, или версткой макета.
В WPF имеется богатая инфраструктура компоновки. В ее основе лежит механизм
взаимодействия между элементами-родителями и их потомками. Совместно они
договариваются об окончательных размерах и положении. Хотя в конечном итоге
именно родитель говорит своим детям, где они должны рисовать себя и сколько места
им отведено, но действует он не как диктатор, а как сотрудник; родитель спрашивает
своих детей, сколько места они хотели бы получить, и только потом принимает
окончательное решение.
Родительские элементы, поддерживающие компоновку нескольких детей, называются
панелями, они наследуют абстрактному классу System. Windows.Controls.Panel. Все
элементы, участвующие в процессе компоновки (как родители, так и потомки),
наследуют классу System.Windows.UIElement.
Поскольку тема компоновки в WPF является обширной и важной, то в этой книге ей
посвящено три главы:
• Глава 4 «Задание размера, положения и преобразований элементов»
• Глава 5 «Компоновка с помощью панелей»
• Глава 21 «Компоновка с помощью нестандартных панелей»
В этой главе мы сосредоточимся на дочерних элементах и рассмотрим вопрос о том, как
управлять компоновкой на уровне отдельных дочерних элементов. Эти аспекты
контролируются несколькими свойствами, большая часть которых перечислена на рис.
4.1 на примере произвольного элемента, находящегося внутри произвольной панели.
Свойства, относящиеся к заданию размера,

126

Глава4. Задание размера, положения и преобразований элемента

показания синим цветом, а относящиеся к положению, - красным. Дополнительно к
элементам могут применяться преобразования (показаны зеленьм цветом), влияющие
как на их размер, так и на положение.

Puc 4.1. Основные свойства, управляющие компоновкой дочерних элементов,
которые рассматриваются в этой главе
В следующей главе мы продолжим рассказ о компоновке и обсудим все многообразие
встроенных в WPF панелей, каждая из которых компонует дочерние элементы посвоему. Создание нестандартных панелей — отдельная непростая тема, которой
посвящена последняя часть этой книги.

Управление размером
Всякий раз, когда требуется произвести компоновку (например, после изменения размера
окна), дочерние элементы сообщают родительской панели свой предпочтительный
размер. Обычно элементы WPF стремятся подстроиться под размер своего содержимого,
то есть выбрать для себя размер, достаточный для размещения всего содержимого, но не
больше. (Так поступает даже элемент Window, но только если явно задано свойство
SizeToContent, как было в примерах из предыдущей главы.) Отдельные элементы могут
влиять на выбор этого размера с помощью нескольких простых свойств.

Свойства Height и Width
Во всех классах, производных от FrameworkElement, есть свойства Height (высота) и
Width (ширина) (типа double), а также MinHeight, MaxHeight, MinWidth и MaxWidth,
которыми можно пользоваться для задания допустимых диапазоне» значений. Все эти
свойства можно задавать в любой комбинации как в процедурном коде, так и в XAML.

Управление рамером

127

Обычно элемент стремится принять минимально возможный размер, поэтому если
задано свойство MinHeight или MinWidth, то при визуализации выбирается именно такая
высота или ширина при условии, что содержимое не вынуждает увеличить размер. Но
увеличение можно ограничить с помощью свойств MaxHeight и MaxWidth (при условии,
что эти значения больше соответствующих минимальных). Если одновременно с
минимальными и максимальными значениями заданы свойства Height и Width, то
последние имеют приоритет при условии, что попадают внутрь диапазона между Min и
Мах. По умолчанию MinHeight и MinWidth равны 0, a MaxHeight и MaxWidth - величине
Double.PositiveInfinity (которая в XAML записывается просто как "Infinity").

ПРЕДУПРЕЖДЕНИЕ
Избегайте явного задания размеров!
Если явно задавать размеры элементов управления, особенно производных от класса
ContentControl, например Button и Label, то возникает риск отсечения текста в случае,
когда пользователь изменяет системный шрифт или текст переводится на другие языки.
Поэтому лучше не задавать размеры явно, если без этого можно обойтись. К счастью,
благодаря наличию панелей необходимость явно задавать размеры возникает редко.

КОПНЕМ ГЛУБЖЕ
Специальное значение длины "Auto"
Свойства Height и Width класса FrameworkElement по умолчанию принимают значение
Double.NaN (NaN означает not a number — «не число»), которое означает, что размер
элемента подстраивается под размер содержимого. Это значение можно и явно задать в
XAML-коде в виде строки "NaN‖ (чувствительной к регистру) или более
предпочтительной строки "Auto" (нечувствительной к регистру), что обеспечивает
конвертер типа LengthConverter, ассоциированный с этими свойствами. Чтобы
проверить, выбирается ли размер элемента автоматически, можно воспользоваться
статическим методом Double.IsNaN.

Ситуация осложняется тем, что в классе FrameworkElement есть еще несколько свойств,
относящихся к размеру:
 DesiredSize (наследуется от UIElement)
 RenderSize (наследуется от UIElement)
 ActualHeight и ActualWidth
В отличие от остальных шести свойств, они являются не входными данными для
процедуры компоновки, а выходными — представляющими результат компоновки, и
потому доступны только для чтения. Свойство элемента DesiredSize вычисляется в
процессе компоновки на основе значений других свойств

128

Глава4. Задание размера, положения и преобразований элемента

(в том числе вышеупомянутых Width, Height, MinXXXи МахХХХ) и места, родитель
готов выделить. Оно используется панелями для внутренних целей
Свойство RenderSize представляет окончательный размер элемента по завершении
компоновки, a ActualHeight и ActualWidth в точности то же самое, RenderSize. Height и
RenderSize. Width. Запомните: каким бы способом ни был задан размер элемента - явно, с
помощью допустимых диапазонов значений или не задан вовсе, - родитель вправе
изменить окончательный размер элемента на экране. Поэтому три упомянутых свойства
бывают полезны в тех редких случаях, когда поведение программы зависит от размера
элемента. С другой стороны, значения остальных свойств, относящихся к размеру очень
интересны для формулирования алгоритма. Например, если свойств Height и Width не
заданы явно, то они будут иметь значение Double. NaN, каким бы ни оказался истинный
размер элемента.
В главе 21 использование всех этих свойств демонстрируется в контексте.

ПРЕДУПРЕЖДЕНИЕ
Будьте осторожны использовании в коде свойств ActualHeight и
ActualWidth (или RenderSize)!
При каждой компоновке значение RenderSize (а значит, также ActualHeight и
ActualWidth) обновляется. Однако компоновка производится асинхронно, поэтому
нельзя рассчитывать, что эти значения в любой момент правильны. Обращаться к ним
безопасно только внутри обработчика события LayoutUpdated, определенного в классе
UIElement.
Есть и другой способ - в классе UIElement имеется метод UpdateLayout, который
синхронно производит все отложенные обновления макета, но лучше его не применять. Мало того что частые обращения к UpdateLayout могут негативно сказаться
на производительности, так еще и нет гарантии, что используемые элементы
корректно обрабатывают реентерабельность в методах, относящихся к компоновке.

Свойства Margin и Padding
Очень похожие свойства Margin и Padding тоже связаны с размером элемента. Свойство
Margin определено для всех объектов, производных от FrameworkElement, свойство
Padding во всех элементах управления, производных от класса Control (а также в классе
Border). Различие в том, что Margin задает внешнее поле вокруг элемента, a Padding внутренний отступ между содержимым элемента и его границами.
Оба свойства имеют тип System.Windows.Thickness; это любопытный класс, который
может представлять одно, два или четыре значения типа double. Интерпретация этих
значений демонстрируется в листинге 4.1, где для меток задаются различные отступы
Padding и поля Margin. Второй набор меток ключей в рамки Border, поскольку иначе поля
были бы не видны.

Управление рамером

129

На рис 4.3 показано, как выглядит результат в случае, когда каждая метка помещена в
отдельный элемент Canvas (это панель, рассматриваемая в следующей главе). Хотя на
рисунке это и не показано, свойство Margin (но не Padding) может принимать
отрицательные значения.
Четыре разных отступа (Paddings):

Рис. 4.2. Результат установки свойств Margin и Padding
Листинг 4.1. Задание свойств Margin и Padding с одним, двумя и четырьмя
числовыми значениями



















130

Глава 4. Задание размера, положения и преобразований элементов

!-- 2 значение: первое значение относится к левому и правому,
полям, второе – к верхнему и нижнему : -->







Для элемента Label свойство Padding по умолчанию равно 5, но его можно заменить
любым другим допустимым значением. Именно поэтому в листинге 4.1 свойство
Padding для первой метки имеет значение 0. В противном случае эта метка выглядела
бы точно так же, как пятая (та, в которой демонстрируется неявно заданное поле
Margin, равное 0), и визуальное сравнение с другими значениями Padding было бы
неочевидно.
КОПНЕМ ГЛУБЖЕ
Синтаксис задания значений типа Thickness
Синтаксис задания значений через запятую, поддерживаемый свойствами Margin и
Padding, обеспечивает - догадайтесь, кто - конвертер типа System.Windows.ThicknessConverter, который конструирует объект типа Thickness из строки. В классе
Thickness определены два конструктора: первый принимает одно значение типа
double, второй - четыре. Следовательно, в программе на языке С# допустимы
следующие формы:
myLabel.Margin = new Thickness(10);
// To же, что Margin="10" в XAML
myLabel.Margin = new Thickness(20, 5, 20, 5); // To же, что Margin="20,5" в XAML
myLabel.Margin = new Thickness(0,10,20,30); // To же, что Margin="0,10,20,ЗО" в XAML

Обратите внимание, что удобный синтаксис с двумя числами доступен только через
конвертер типа!
FAQ
Какие единицы измерения применяются в WPF?
Конвертер LengthConverter, ассоциируемый с различными свойствами, касающимися
длин, поддерживает явное задание единиц измерения cm, pt, in и рx (по умолчанию).
По умолчанию все абсолютные измерения (в частности, величины свойств,
обсуждаемых в этом разделе) выражаются в независимых от устройства пикселах.
Такие «логические пикселы» представляют 1/96 дюйма независимо от разрешения
экрана, выраженного в точках на дюйм (DPI). Отметим, что количество независимых
от устройства пикселов всегда задается в виде значения с двойной точностью, то есть
может быть дробным.

Управление размером

131

Точная величина - 1/96 дюйма - не так существенна, а выбрана она была потому, что
на типичном экране с разрешением 96 DPI один независимый от устройства пиксел в
точности совпадает с одним физическим пикселом. Разумеется, понятие «истинного»
дюйма зависит от физического устройства отображения. Если приложение нарисует
отрезок длиной 1 дюйм на экране моего ноутбука, то при выводе изображения на
проектор длина этого отрезка, конечно, окажется больше!
Важно лишь, что все измерения не зависят от разрешающей способности. Но само
посебе это не мешает изображению уменьшаться при увеличении разрешения экрана.
Чтобы размер изображения не зависел от разрешения, необходим механизм
автоматического масштабирования, обсуждаемый в следующей главе.

Свойство Visibility
Может показаться странным, что мы обсуждаем свойство Visibility (определенное в
классе UIElement) в контексте компоновки, однако оно действительно относится
именно к этой теме. Тип свойства элемента Visibility - не Boolean, а перечисление
System.Windows.Visibility с тремя состояниями, то есть оно может принимать три
значения:


Visible – элемент виден и участвует в компоновке.



Collapsed – элемент не виден и не участвует в компоновке.



Hidden – элемент не виден, но тем не менее участвует в компоновке.

Свернутый (Collapsed) элемент, по существу, имеет нулевой размер, тогда как скрытый
(Hidden) элемент сохраняет свой первоначальный размер. (Так, значения его свойств
ActualHeight и ActualWidth не изменяются.). Разница между состояниями Collapsed и
Hidden продемонстрирована на рис. 4.3, где панель StackPanel со свернутой кнопкой:





сравнивается с панелью StackPanel со скрытой кнопкой:





Рис. 4.3. Скрытая кнопка занимает место на экране, а свернутая - нет

132

Глава 4. Задание размера, положения и преобразований элементов

Управление положением
В этом разделе мы не обсуждаем позиционирование элементов с помощью задания
координат (Х,У). Родительские панели определяют собственные механизмы (для
каждой панели свой), позволяющие дочерним элементам позиционировать себя (либо
посредством присоединенных свойств, либо просто в том порядке, в котором дочерние
элементы добавлялись на панель). Однако есть несколько механизмов, общих для всех
дочерних элементов типа FrameworkElement, и именно их мы сейчас и рассмотрим.
Все они касаются выравнивания, а сама концепция носит название «направление
потока».

Выравнивание
С помощью свойств HorizontalAlignment и VerticalAlignment элемент может управлять
распределением избыточного пространства, выделенного ему родителем. Значениями
свойств являются одноименные перечисления, которые определены в пространстве
имен System.Windows:


HorizontalAlignment - Left, Center, Right, Stretch



VerticalAlignment - Top, Center, Bottom, Stretch

По умолчанию оба свойства принимают значение Stretch, хотя в стилях тем для
различных элементов управления оно может быть переопределено. Чтобы посмотреть,
как свойство HorizontalAlignment влияет на компоновку, достаточно поместить
несколько кнопок Button на панель StackPanel и задаться них разные значения
перечисления:







Результат показан на рис. 4.4.

Рис. 4.4. Влияние HorizontalAlignment на размещение кнопок на панели StackPanel.

Управление положением

133

Эти два свойства полезны только в случае, когда родительская панель выделяет
дочернему элементу больше места, чем тому необходимо. Так, задание свойства
VerticalAligment для элементов на панели StaскРапеl, изображенной на рис. 4.4, ничего
не изменит, потому что каждому элементу уже выделена ровно такая высота, какая ему
требуется, — не больше и не меньше.
КОПНЕМ ГЛУБЖЕ
Взаимодействие между типом выравнивания Stretch и явным заданием размера
элемента
Даже если для элемента в качестве выравнивания задано растяжение (Stretch) по
горизонтали или по вертикали, приоритет все равно отдается явно заданной высоте
Height или ширине Width. Свойства MaxHeight и MaxWidth также более приоритетны, но только в том случае, когда их значения меньше размера, получившегося
после растяжения. Аналогично свойствам MinHeight и MinWidth приоритет отдается
лишь тогда, когда их значения больше размера, получившегося после растяжения.
Если свойство Stretch используется в контексте, где на размер элемента налагаются
ограничения, то оно действует как тип выравнивания Center (или Left, если элемент
слишком велик и не может быть отцентрирован внутри своего родителя).

Выравнивание содержимого
Помимо свойств HorizontalAlignment и VerticalAlignment, в классе Control имеются
свойства HorizontalContentAlignment и VerticalContentAlignment. Они определяют
порядок размещения содержимого внутри элемента управления. (То есть соотношение
между выравниванием и выравниванием содержимого примерно такое же, как между
полями и отступами.)
Свойства, управляющие выравниванием содержимого, принадлежат тем же типам
перечисления, что и соответствующие свойства выравнивания, следовательно, и
возможности у них точно такие же. Однако по умолчанию свойство
HorizontalContentAlignment равно Left, a VerticalContentAlignment равно Тор. Для
показанных выше кнопок мы этого не наблюдали, потому что значения
переопределены в стиле темы. (Вспомните порядок приоритетов различных
поставщиков значений свойств зависимости, описанный в предыдущей главе. У
значений по умолчанию приоритет самый низкий, поэтому они замещаются
значениями, заданными в стилях.)
На рис. 4.5 показано, как выглядят кнопки при различных значениях HorizontalContentAlignment. Для этого мы просто модифицировали предыдущий фрагмент
XAML-кода:




134

Глава 4. Задание размера, положения и преобразований элементов





Кнопка Button на рис. 4.5, для которой HorizontalContentAlignment=‖Stretch‖ выглядит
несколько неожиданно. Ее внутренний элемент TextBlock действительно растянулся,
однако класс TextBlock не является подклассом Control (он наследует непосредственно
FrameworkElement), поэтому к заключенному внутри него тексту понятие растяжения
неприменимо.

Рис. 4.5. Влияние HorizontalContentAlignment на размещение кнопок на панели
StackPanel

Свойство FlowDirection
Свойство FlowDirection, определенное в классе FrameworkElement (и еще нескольких),
позволяет изменить направление визуализации внутреннего содержимого элемента.
Оно применимо к некоторым панелям, где влияет на размещение дочерних элементов, а
также модифицирует способ выравнивания содержимого внутри дочерних элементов.
Тип этого свойства - перечисление System.Windows.FlowDirection, принимающее два
значения: LeftToRight (по умолчанию в классе FrameworkElement) и RightToLeft.
Идея FlowDirection заключается в том, что для языка, записываемого справа налево,
должно быть задано направление RightToLeft. Тем самым меняются местами понятия
«левый» и «правый» для таких свойств, как выравнивание содержимого. В следующем
фрагменте XAML присутствуют две кнопки, для которых задано одно и то же
выравнивание содержимого Тор и Left, но направление FlowDirection разное:





Результат представлен на рис. 4.6

Применение преобразований

135

Puc. 4.6. Влияние FlowDirection на кнопки с выравниванием содержимого Тор и Left
Отметим, что свойство FlowDirection не оказывает влияния на направление записи букв
в надписи внутри кнопок. Английские буквы всегда записываются слева направо,
арабские - справа налево. Но понятия левого и правого во всех остальных частях
интерфейса, которые обычно должны соответствовать направлению записи букв,
меняются местами.
Свойство FlowDirection необходимо задавать явно в соответствии с текущей культурой
(это достаточно сделать для элемента самого верхнего уровня). Это должно стать
частью процедуры локализации.

Применение преобразований
В WPF имеется целый ряд встроенных классов двумерных геометрических
преобразований (производных от System.Windows.Media.Transform), которые позволяют изменять размер и положение элементов независимо от ранее рассмотренных
свойств. Некоторые преобразования изменяют элементы и более экзотическими
способами, например, поворачивают или наклоняют их.
Во всех подклассах FrameworkElement имеется два свойства типа Transform, позволяющих применять преобразования:


LayoutTransform - применяется до компоновки элемента



RenderTransform (унаследовано от UIElement) - применяется после завершения
компоновки (непосредственно перед визуализацией элемента)

На рис. 4.7 показана разница между применением преобразования поворота
RotateTransform в режиме LayoutTransform и в режиме RenderTransform. В обоих
случаях преобразование применяется ко второй из трех кнопок. Но если оно применено
как LayoutTransform, то третья кнопка сдвигается вниз, а если как RenderTransform, то
третья кнопка размещается так, будто вторая не поворачивалась вовсе.
В классе UIElement имеется также полезное свойство RenderTransformOrigin,
представляющее начальную точку преобразования (которая остается неподвижной).
Для преобразования RotateTransform на рис. 4.7 началом является левый верхний угол
кнопки, вокруг которого кнопка поворачивается. Для преобразований, применяемых в
режиме LayoutTransform, понятие начальной точки не определено, потому что
положение преобразованного элемента диктуется правилами компоновки,
применяемыми родителем.

136

Глава 4. Задание размера, положения и преобразований элементов

Поворот, примененный
LayoutTransform

в

режиме

Поворот, примененный
RenderTransform

Рис. 4.7. Разница между применением преобразований
RenderTransform к средней из трех кнопок на панели StackPanel

в

режиме

LayoutTransform

и

Свойство RenderTransformOrigin имеет тип System.Windows.Point и по умолчание равно
(0,0). Этой точке соответствует левый верхний угол элемента, как показано на рис. 4.7.
Точка (0,1) представляет левый нижний угол, (1,0) - правый верхний угол, а (1,1) правый нижний угол. Можно задавать и числа, большие 1, - тогда начальная точка
окажется вне границ элемента. Дробные значения также допустимы. В частности, точка
(0.5,0.5) представляет середину объекта. На рис. 4.8 показано пять начальных точек,
обычно задаваемых в качестве значения свойства RenderTransformOrigin.

Рис. 4.8. Пять типичных значений RenderTransformOrigin, применяемых для поворота
кнопки, которая изображена на рис. 4.7
Благодаря конвертеру System.Windows.PointConverter значение RenderTransform.Origin
можно задавать в XAML в виде двух чисел, разделенных запятой (без скобок).
Например, в следующем фрагменте создается кнопка Button, повернутая относительно
своего центра (самая правая кнопка на рис. 4.8):


Применение преобразований

137

Возможно, вам непонятно, зачем вообще в приложении может понадобиться
повернутая кнопка. Да, для стандартных элементов управления со стилями по
умолчанию подобное преобразование выглядит нелепо. Часто это имеет смысл в
приложениях с сильно перегруженными темами, но даже для стилизованных по
умолчанию элементов преобразования в сочетании с анимацией могут создать
интересные эффекты.
В этом разделе мы рассмотрим все пять встроенных двумерных преобразований,
определенных в пространстве имен System.Windows.Media:


RotateTransform



ScaleTransform



SkewTransform



TranslateTransform



MatrixTransform

Преобразование RotateTransform
Преобразование RotateTransform, продемонстрированное в предыдущем разделе,
поворачивает элемент в соответствии со следующими тремя свойствами типа double:


Angle - угол поворота в градусах (по умолчанию 0)



CenterX - абсцисса центра поворота (по умолчанию 0)



CenterY - ордината центра поворота (по умолчанию 0)

Точка (CenterX,CenterY), равная по умолчанию (0,0), соответствует левому верхнему
углу. Свойства CenterX и CenterY принимаются во внимание, только если
преобразование применяется в режиме RenderTransform, потому что для преобразования в режиме LayoutTransform положение центра поворота определяется
родительской панелью.
FAQ
В чем разница между использованием свойств CenterX и CenterY для
преобразований вида RotateTransform и свойством RenderTransformOrigin
элемента типа UlElement?
Складывается впечатление, что, когда преобразование применяется к элементу типа
UIElement, свойства CenterX и CenterY избыточны, так как уже имеется свойство
RenderTransformOrigin. Ведь оба механизма определяют начальную точку
преобразования и оба работают, только если преобразование применяется в режиме
RenderTransform.
Однако же CenterX и CenterY задают абсолютное положение начальной точки, тогда
как RenderTransformOrigin - относительное. Значения задаются в независимых от
устройства пикселах, так что для правого верхнего угла элемента с шириной Width,
равной 20, свойство CenterX будет равно 20, a CenterY - 0, а не (1,0),

138

Глава 4. Задание размера, положения и преобразований элементов

как для RenderTransformOrigin. Кроме того, при комбинировании нескольких
преобразований RenderTransform(см. следующую главу) задание CenterX и CenterY
для отдельных преобразований обеспечивает более точный контроль. Наконец,
раздельные значения CenterX и CenterY типа double проще использовать для
привязки к данным, чем одно значение RenderTransformOrigin типа Point.
Тем не менее, свойство RenderTransformOrigin, вообще говоря, более полезно, чем
CenterX и CenterY. В типичном случае, когда элемент поворачивается относительно
своей середины, относительные координаты (0.5,0.5), задаваемые с помощью
RenderTransformOrigin, проще записать в XAML. Для достижения того же эффекта с
помощью CenterX и CenterY пришлось бы писать процедурный код, вычисляющий
абсолютные смещения.
Отметим, что задавать RenderTransformOrigin для элемента можно одновременно с
установкой значений CenterX и CenterY для его преобразования. В этом случае пары
значений X и Y комбинируются для вычисления окончательной начальной точки.
На рис. 4.7 и 4.8 показаны результаты поворота кнопки, а на рис. 4.9 демонстрируется,
что происходит, когда преобразование RotateTransform применяете в режиме
RenderTransform к внутреннему содержимому кнопок с двумя различными значениями
RenderTransformOrigin. Для этого простая строка в составе элемента Button заменяется
следующим явным элементом TextBlock:


Может показаться, что элементы TextBlock внутри Button в левой части рис. 4.9
повернуты не вокруг левого верхнего угла кнопки, но это потому, что сам элемент
TextBlock чуть больше содержащегося внутри него текста. Если закрасить текстовые
блоки бирюзовым фоном, то результат поворота будет более наглядным (рис. 4.10).

Поворот текста вокруг средней точки

Поворот текста вокруг средней точки

Рис. 4.9. Применение RotateTramsform к содержимому кнопок на панели StackPanel.

Применение преобразований

139

Рис. 4.10. Для внутренних элементов TextBlock, повернутых вокруг левого верхнего
угла, явно задан цвет фона
RotateTransform имеет параметризованные конструкторы, которые принимают
значения угла или угла и центра, для удобства выполнения преобразования из
процедурного кода.

Преобразование ScaleTransform
Преобразование ScaleTransform увеличивает или уменьшает элемент по горизонтали,
по вертикали или в обоих направлениях. У него есть четыре свойства типа double:


ScaleX - коэффициент изменения ширины элемента (по умолчанию 1)



ScaleY - коэффициент изменения высоты элемента (по умолчанию 1)



CenterX-начальная точка для масштабирования по горизонтали (по умолчанию
0)



CenterY - начальная точка для масштабирования по вертикали (по умолчанию 0)

Если ScaleX равно 0.5, то ширина рисуемого элемента уменьшается вдвое, а если
ScaleX равно 2, то вдвое увеличивается. Смысл свойств CenterX и CenterY такой же,
как для преобразования RotateTransform.
В листинге 4.2 преобразование ScaleTransform применяется к трем кнопкам Button на
панели StackPanel, демонстрируя возможность независимого изменения высоты и
ширины. На рис. 4.11 представлен результат.
Листинг 4.2. Применение ScaleTransform к кнопкам на панели StackPanel







Рис. 4.11. Результат масштабирования кнопок, произведенного в листинге 4 2
На рис. 4.12 изображены кнопки из листинга 4.2, для которых дополнительно явно
заданы свойства Center Х и Center Y. В качестве текста каждой кнопки указаны
координаты начальной точки. Обратите внимание, что зеленая кнопка не сдвинута
влево, как оранжевая, хотя CenterX в обоих случаях равно 70. Дело в том, что Center Х
принимается во внимание, только если значение отлично от 1, а CenterY - если ScaleY
не равно 1.
Как и во всех остальных классах преобразований, в ScaleTransform определено
несколько конструкторов для удобства создания объектов в процедурном коде.

Рис. 4.12. Кнопки из листинга 4.2, но с явно заданными центрами масштабирования
КОПНЕМ ГЛУБЖЕ
Взаимодействие между ScaleTransform и выравниванием типа Stretch
Если преобразование ScaleTransform применяется в режиме LayoutTransform к
элементу, который уже растянут в направлении масштабирования, то оно
принимается во внимание только в случае, когда размер после масштабирования
больше размера, получившегося в результате растяжения.

Применение преобразований

141

FAQ
Как преобразования, подобные ScaleTransform, влияют на свойства ActualHeight
и ActualWidth элемента типа FrameworkElement и на свойство RenderSize
элемента типа UIElement?
Применение преобразования к элементу типа FrameworkElement никогда не изменяет
значений этих свойств. Это справедливо вне зависимости от того применяется
преобразование в режиме RenderTransform или LayoutTransform. Поэтому из-за
преобразований эти свойства могут сообщать «ложный» размер элемента на экране.
Например, у всех кнопок на рис. 4.11 и 4.12 величины ActualHeight, ActualWidth и
RenderSize одинаковы.
Быть может, вас удивляет такая «ложь», но это правильное решение. Во-первых, не
вполне понятно, как следует выражать эти значения для некоторых преобразований.
Важнее, однако, то, что цель преобразования - изменить внешний вид элемента так,
чтобы сам элемент об этом ничего не знал. Создавая у элемента иллюзию того, что он
визуализируется нормально, мы можем единообразно преобразовывать произвольные
элементы.

FAQ
Как преобразование ScaleTransform влияет на свойства Margin и Padding?
Свойство Padding масштабируется вместе со всем содержимым (поскольку отступ
находится внутри элемента), а свойство Margin не масштабируется вовсе. Как и в
случае ActualHeight и ActualWidth, числовое значение свойства Padding не изменяется, несмотря на визуальный эффект масштабирования.

Преобразование SkewTransform
Преобразование SkewTransform наклоняет элемент в соответствии со значениями
четырех свойств типа double:
•

AngleX– угол наклона по горизонтали (по умолчанию 0)

•

AngleY–угол наклонапо вертикали (по умолчанию 0)

•

CenterX– начальная точка для наклона по горизонтали (по умолчанию 0)

•

CenterY– начальная точка для наклона по вертикали (по умолчанию 0)

Эти свойства по своему поведению очень похожи на свойства рассмотренных выше
преобразований. На рис. 4.13 показаны результаты применения SkewTransform в
качестве RenderTransform к нескольким кнопкам; центром наклона является левый
верхний угол.

142
элементов

Глава 4. Задание размера, положения и преобразований

Рис. 4.13. Применение преобразования SkewTransform к кнопкам на панели StackPanel

Преобразование TranslateTransform
Преобразование TranslateTransform просто параллельно
соответствии со значениями двух свойств типа double:

переносит

элемент

в

X - величина смещения по горизонтали (по умолчанию 0)
• Y- величина смещения по вертикали (по умолчанию 0)
•

TranslateTransform не дает никакого эффекта, когда применяется в режиме
LayoutTransfоrm, но применение его в режиме RenderTransform удобный способ
«подвинуть» элементы. Чаще всего это делается динамически в ответ на действия
пользователя (и, быть может, в составе анимации). Маловероятно, что при работе с
панелями, описанными в следующей главе, вы захотите использовать это
преобразование для компоновки статического пользовательского интерфейса.

Преобразование MatrixTransform
Преобразование MatrixTransform представляет собой низкоуровневый механизм
описания произвольного двумерного преобразования. У него есть единственное
свойство Matrix(типа System.Windows.Media.Matrix), представлении матрицу
аффинного преобразования размером 3x3. На случай если вы незнакомы с линейной
алгеброй, сообщу, что все рассмотренные выше преобразования (и их комбинации)
также можно осуществить с помощью MatrixTransorm.
Матрица устроена следующим образом:

Значения в последнем столбце фиксированы, а остальные шесть значений, но задать в
виде свойств объекта Matrix(с показанными на рисунке именами)

Применение преобразований

143

или в конструкторе, который принимает шесть чисел в порядке следования строк
матрицы.
КОПНЕМ ГЛУБЖЕ
Конвертер типа MatrixTransform
MatrixTransform единственное преобразование, конвертер типа которого позволяет
описывать его в XAML с помощью простой строки. (Класс конвертера называется
TransformConverter и, хотя он ассоциирован с абстрактным классом Transform, в
реальности поддерживает только тип MatrixTransform.) Например, чтобы переместить кнопку на 10 единиц вправо и на 20 единиц вниз, нужно написать такой код:


На рис. 4.14 показан результат применения всех трех преобразований к кнопке.

Рис. 4.14. Кнопка, подвергнутая
масштабирования и наклона

«пыткам»

преобразований

поворота,

Для повышения производительности WPF сначала вычисляет комбинированное
преобразование на основе потомков объекта TransformGroup, а затем применяет одно
результирующее преобразование (как если бы с самого начала применялось
преобразование MatrixTransorm). Отметим, что в составе группы TransformGroup
может несколько раз встречаться одно и то же преобразование. Например, два
поворота MatrixTransform на 45°, эквивалентные одному повороту на 90°.

Резюме
На этом мы завершаем обзор свойств компоновки, с помощью которых дочерние
элементы могут повлиять на способ своего размещения на экране. В этой главе вы
также получили представление о таких вещах, которых не было ни в Win32, ни в
WindowsForms: повернутых и наклоненных элементах управления!
Но самой важной частью механизма компоновки являются родительские панели. В
этой главе мы для простоты пользовались только панелью StackPanel, а в следующей
формально изучим как ее, так и все остальные панели.

Резюме

145

ПРЕДУПРЕЖДЕНИЕ
Не все элементы типа FrameworkElement поддерживают преобразования!
Элементы, содержимое которых не является «родным» для WPF, не поддерживают
преобразования, хотя и наследуют свойства LayoutTransform и RenderTransform.
Например, к их числу относится элемент HwndHost, выступающий в роли владельца
GDI-содержимого (обсуждается в главе 19 «Интероперабельность с другими
технологиями»). Элемент управления Frame, который, в принципе, может содержать
HTML-разметку (описывается в главе 9 «Однодетные элементы управления»),
поддерживает преобразования в полном объеме, только если в нем нет HTML. В
противном случае преобразование ScaleTransform можно применять для изменения
размера, но содержимое при этом не масштабируется.
На рис. 4.15 показано, что происходит, когда на панели StackPanel размещено несколько кнопок и фрейм Frame, содержащий веб-страницу (с ограничением размера
100x100). Когда вся панель поворачивается и масштабируется, фрейм честно
пытается выполнить масштабирование, но не поворачивается. В результате большая
часть повернутых кнопок оказывается перекрыта.

Панель StackPanel до преобразования

StackPanel после масштабирования и поворота
Рис. 4.15. Элемент Frame, содержащий HTML, частично реагирует на преобразование ScaleTransform, но игнорирует все остальные преобразования

5
Компоновка с помощью панелей
•
•
•
•
•
•
•
•

Панель Canvas
Панель StackPanel
Панель WrapPanel
Панель DockPanel
ПанельGrid
Примитивные панели
Обработка переполнения содержимого
Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели

Компоновка - важнейший фактор, обеспечивающий удобство работы с приложением на
различных устройствах, но без качественной поддержки со стороны платформы
реализовать этот механизм исключительно трудно. Можно, конечно, размещать
элементы пользовательского интерфейса статически, задавая координаты и размеры в
пикселах, и где-то это даже будет работать, однако такое решение «посыпется» под
воздействием самых разных факторов: разная разрешающая способность и линейные
размеры экранов, заданные пользователем настройки, к примеру, размер шрифта,
содержимое, изменяющееся непредсказуемым образом (например, после перевода
текста на другие языки). Добавим еще, что приложение, не позволяющее пользователю
изменять размеры своих частей (и разумно распоряжаться имеющимся местом) многих
раздражает.
Мой нетбук оснащен экраном с разрешающей способностью 1024x600. Outlook2010
адаптируется к нему отлично, но многие программы, в том числе VisualStudio2010,
испытывают трудности. Если я переведу экран в портретный режим (600x1024), то
Outlook2010 прекрасно распорядится имеющим местом, тогда как другие программы
(та же VisualStudio2010) справляются с этим куда хуже. (Ирония этой ситуации в том,
что VisualStudio, по крайней мере, частично, написана на WPF, a Outlook вообще не
использует WPF. Впрочем, описанное явление - результат не столько применения тех
или иных технологий, сколько недостатка внимания, которое разработчики уделяли
адаптации к экранам небольшого или необычного размера.)
WPFсодержит встроенные панели, которые помогают избежать неприятностей с
компоновкой.В начале этой главы мы рассмотрим пять основных встроенных панелей
(все в пространстве имен System.Windows.Controls) в порядке возрастания сложности
(и полезности):
• Canvas
• StackPanel

Панель Canvas

147

WrapPanel
• DockPanel
• Grid
•

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

Панель Canvas
Canvas(холст) - самая простая панель. Настолько простая, что использовать ее для
организации пользовательского интерфейса вообще не стоит. Canvas поддерживает
только «классическое» позиционирование элементов путем явного задания координат;
впрочем, координаты хотя бы задаются в независимых от устройства пикселах, в
отличие от прежних систем конструирования пользовательских интерфейсов. Панель
Canvas позволяет задавать координаты относительно любого, а не только левого
верхнего угла.
Позиционирование элемента на холсте осуществляется с помощью присоединенных
свойств: Left, Top, Rightи Bottom. Задавая значение Left или Right, вы определяете, что
ближайшая сторона элемента должна всегда отстоять на фиксированное расстояние от
соответствующей стороны холста. То же самое относится к свойствам Тор и Bottom.
По сути дела, вы указываете угол, к которому «примыкает» каждый элемент, а
значения присоединенных свойств выступают в роли полей (к которым добавляются
значения самого свойства Margin элемента). Если для некоторого элемента не задано
ни одно присоединенное свойство (то есть все они имеют значение по умолчанию
Double.NaN), то он помещается в левый верхний угол (что эквивалентно установке для
Left и Тор значения 0). Использование панели Canvas демонстрируется в листинге 5.1,
а результат показан на рис. 5.1.
Листинг 5.1. Расположение кнопок на панели Canvas







148

Глава 5. Компоновка с помощью панели

Right=0, Top=0




Рис. 5.1. Кнопки на панели Canvas из листинга 5.1
ПРЕДУПРЕЖДЕНИЕ
Для элемента нельзя задавать более двух присоединенных свойств Canvas!
При попытке одновременно установить свойства Canvas.Left и Canvas.Right
последнее будет проигнорировано. А при попытке одновременно установить
свойства Canvas.Top и Canvas.Bottom будет проигнорировано Canvas.Bottom. Таким
образом, невозможно пристыковать элемент более чем к одному углу холста.
В табл. 5.1 показано, как некоторые из обсуждавшихся в предыдущей главе свойств
компоновки дочерних элементов применяются к элементам, расположенным на панели
Canvas.
Таблица 5.1. Взаимодействие Canvas со свойствами компоновки дочерних элементов
Свойство
Margin

HorizontalAlignment и
VerticalAlignment
LayoutTransform

Допустимо ли внутри Canvas
Частично. Для двух сторон, использованных для
позиционирования элемента (по умолчанию Тор и Left), к
значениям присоединенных свойств прибавляются
соответствующие значения двух из четырех полей
Нет. Элементам назначается в точности та величина
вертикального выравнивания, которая им необходима
Да. Отличается от RenderTransform тем, что при
использовании LayoutTransform элементы всегда отстоят
на заданное расстояние от выбранного угла Canvas

Панель Canvas

149

СОВЕТ
Z-порядок по умолчанию (задающий, какие элементы располагаются «поверх»
других) определяется порядком добавления дочерних элементов к родителю. В
XAML это определяется порядком следования дочерних элементов в файле.
Элементы, добавленные позже, располагаются поверх элементов, добавленных
раньше. Так, на рис. 5.1 оранжевая кнопка располагается поверх красной, а зеленая поверх желтой. Это проявляется не только для встроенных панелей, допускающих
перекрытие элементов (в частности, Canvas), но и в случае, когда к перекрытию
приводит применение преобразования RenderTransform(как на рис. 4.7, 4.8, 4.11, 4.12
и 4.13 в предыдущей главе).
Однако Z-порядок любого элемента можно задать явно, указав для него присоединенное свойство ZIndex, определенное в классе Panel (и наследуемое всеми панелями). ZIndex- это целое число, по умолчанию равное 0; оно может принимать
любое целое значение (положительное или отрицательное). Элементы с большим
значением ZIndex рисуются поверх элементов с меньшим значением, то есть элемент
с наименьшим значением ZIndex оказывается позади всех остальных, а элемент с
наибольшим значением - впереди. В следующем примере свойство ZIndex задано
так, что красная кнопка располагается поверх оранжевой, несмотря на то, что в
списке дочерних элементов Canvas она встречается раньше:





Если для нескольких элементов задано одно и то же значение ZIndex, то их взаимное
расположение определяется порядком следования в коллекции Children панели, как в
случае по умолчанию.
Таким образом, для манипулирования Z-порядком из программы достаточно
изменить значение ZIndex. Чтобы поместить красную кнопку под оранжевую, можно
присвоить присоединенному свойству любое значение, меньшее или равное нулю. В
программе на С# это делается следующим образом (в предположении, что красная
кнопка называется redButton):
Panel.SetZIndex(redButton, 0);

Хотя панель Canvas слишком примитивная для создания гибких пользовательских
интерфейсов, она работает быстрее всех других панелей. Имейте это в виду, когда
захотите максимально точно контролировать размещение элементов и при этом
добиться наивысшей производительности. Например, Canvas очень удобна для точного
позиционирования примитивных фигур в векторных рисунках (см. главу 15
«Двумерная графика»).

150

Глава5. Компоновка с помощью панели

Панель StackPanel
Панель StackPanel популярна из-за своей простоты и удобства. Как следует из названия,
она последовательно размещает своих потомков в виде стопки. В примерах из
предыдущей главы мы пользовались панелью StackPanel, потому что она не требует
задавать присоединенные свойства для получения приемлемого пользовательского
интерфейса. На самом деле StackPanel - одна из немногих панелей, в которых вообще
не определены собственные присоединенные свойства!
КОПНЕМ ГЛУБЖЕ
StackPanel и размещение справа налево
Если свойство FlowDirection равно RightToLeft, то для панели StackPanel с горизонтальной ориентацией сборка стопки производится справа налево, а не слева направо,
как в случае по умолчанию.
В отсутствие присоединенных свойств единственный способ организова дочерние
элементы
воспользоваться
свойством
панели
Orientation
(типа
System.Windows.Controls.Orientation), которое может принимать значение Horizontal
или Vertical. По умолчанию подразумевается ориентация Vertical. На рис. 5.2 показано
несколько кнопок, для которых заданы только свойства Background и Content,
скомпонованные с помощью двух панелей StackPanel с разной ориентацией.
В случае ориентации Vertical элементы
располагаются один под другим

В случае ориентации
располагаются
один подHorizontal
другим
элементы располагаются слева
направо

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

Панель StrackPanel

151

Таблица 5.2. Взаимодействие StackPanel со свойствами компоновки дочерних
элементов
Свойство

Допустимо ли внутри StackPanel

Margin

Да. Свойство Margin управляет промежутком между
элементом и краями StackPanel, а также промежутком
между краями соседних элементов

HorizontalAlignment и
VerticalAlignment

Частично, поскольку выравнивание игнорируется в
направлении сборки стопки (так как дочерним элементам
отводится ровно столько места, сколько им необходимо).
Если Orientation="Vertical" ,то игнорируется значение
VerticalAlignment. Если Orientation="Horizontal", то
игнорируется значение HorizontalAlignment

LayoutTransform

Да. Отличается от RenderTransform тем, что при
использовании LayoutTransform оставшиеся в стопке
элементы сдвигаются вниз, чтобы освободить место. При
комбинировании компоновки Stretch преобразованием
RotateTransform или SkewTransform, применяемым в
режиме LayoutTransform, растяжение происходит, только
если угол кратен

Последняя фраза в комментариях к свойству LayoutTransform в табл. 5.2 нуждается в
пояснении. На рис. 5.3 видно, что при повороте элемента, который в нормальных
условиях был бы растянут, растяжение производится лишь в случае, когда края
элемента параллельны или перпендикулярны направлению растяжения. Это поведение
не является особенностью StackPanel, а присутствует всюду, где элемент растягивается
только в одном направлении. Эта странность проявляется, только когда преобразование
применяется в режиме LayoutTransform; RenderTransform она не относятся.
При повороте на 80° растяжения нет

При повороте на 90° растяжение есть

Рис. 5.3. Желтая кнопка повернута сначала на 80°, а затем на 90° с применением
преобразования LayoutTransform

152

Глава 5. Компоновка с помощью панели

КОПНЕМ ГЛУБЖЕ
Виртуализирующие панели
Важной деталью реализации нескольких элементов управления являются панели,
производные от абстрактного класса System.Windows.Controls. Virtualizing- Panel.
Наиболее интересная из них - панель VirtualizingStackPanel, которая работает, как
StackPanel, но для повышения производительности временно игнорирует все
элементы, не видные на экране (только во время привязки к данным). Поэтому
VirtualizingStackPanel является оптимальной панелью, когда требуется привязать к
данным по-настоящему много дочерних элементов, и класс ListBox использует ее по
умолчанию. Эту панель можно использовать также в TreeView о чем пойдет речь в
главе 10 «Многодетные элементы управления». Еще две виртуализирующие панели –
DataGridCellsPane lи DataGridRowsPresenter, они используются в классе DataGrid и
ассоциированных с ним (см. главу 11 «Изображения, текст и другие элементы
управления»).

Панель WrapPanel
Панель WrapPanel похожа на StackPanel. Но помимо организации дочерних элементов в
стопку она создает новые строки или столбцы, когда для одной стопки не хватает
места. Это полезно для отображения заранее неизвестного числа элементов, когда
компоновка должна отличаться от простого списка,- как, например, в Проводнике
Windows.
Как и StackPanel, панель WrapPanel не обладает присоединенными свойствами для
управления положением элементов. В классе WrapPanel определены три свойства,
контролирующие его поведение:


Orientation- аналогично одноименному свойству StackPanel с тем отличием, что по
умолчанию подразумевается значение Horizontal. Панель с горизонтальной
ориентацией выглядит, как вид Эскизы страниц в Проводнике Windows: элементы
располагаются один за другим слева направо, а когда место кончается, переходят
на следующую строку. Панель с вертикальной ориентацией выглядит, как вид
Список в Проводнике Windows: элементы располагаются один под другим, а когда
место кончается, начинается новый столбец.



ItemHeight- единая высота для всех дочерних элементов. Каким образом каждый
потомок распоряжается этой высотой, зависит от значений его свойств
VerticalAlignment, Height и пр. Элементы, ширина которых превышает ItemHeight,
отсекаются.



ItemWidth- единая ширина для всех дочерних элементов. Каким образом каждый
потомок распоряжается этой шириной, зависит от значений его свойств
HorizontalAlignment, Width и пр. Элементы, высота которых превышает ItemWidth,
отсекаются.

По умолчанию свойства ItemHeight и ItemWidth не установлены (точнее, имеют
значение Double.NaN).В этом случае панель WrapPanel с вертикальной ориентацией

Панель WrapPanel

153

отводит каждому столбцу ширину, равную ширине самого широкого элемента в нем, а
панель с горизонтальной ориентацией отводит каждой строке высоту, равную высоте
самого высокого элемента в ней. Поэтому по умолчанию ни в строках, ни в столбцах
отсечение не производится.
СОВЕТ
Можно заставить панель WrapPanel располагать элементы в одну строку или в один
столбец. Для этого следует присвоить свойству Width (в случае горизонтальной
ориентации) или свойству Height(в случае вертикальной ориентации) значение
Double.MaxValue либо Double.PositiveInfinity. В XAML это достигается с помощью
расширения разметки х: Static, поскольку ни то ни другое значение не
поддерживается конвертером типа System. Double.
На рис. 5.4 показано четыре динамических снимка экрана для панели WrapPanel с
горизонтальной ориентацией, полученных в результате изменения размера
объемлющего ее окна Window. На рис. 5.5 то же самое показано для панели с
вертикальной ориентацией. Если для панели WrapPanel отведено достаточно места и
свойства ItemHeight/ItemWidth не установлены, то она ведет себя как StackPanel.

Рис. 5.4. Изменение положения кнопок на панели WrapPanel с принимаемой по
умолчанию горизонтальной ориентацией по мере сужения окна.

Рис. 5.5. Изменение положения кнопок на панели WrapPanel с вертикальной
ориентацией по мере сужения окна
В табл. 5.3 показано, как некоторые из свойств компоновки дочерних элементов
применяются к элементам, расположенным на панели WrapPanel.

2

154

Глава 5. Компоновка с помощью панели

КОПНЕМ ГЛУБЖЕ
WrapPanel и размещение справа налево
Если свойство FlowDirection равно RightToLeft то для панели WrapPanel с
вертикальной ориентацией новый столбец создается слева от заполненного, а для
панели с горизонтальной ориентацией заполнение строки производится справа
налево.
Таблица 5.3. Взаимодействие WrapPanel со свойствами компоновки дочерних
элементов.
Свойство

Допустимо ли внутри WrapPanel

Margin

Да. Поля учитываются, когда WrapPanel вычисляет
размеры элементов, чтобы определить подразумеваемую
по умолчанию ширину или высоту стопки

HorizontalAlignment и
VerticalAlignment

Частично. Выравнивание можно задавать в направлении,
противоположном направлению роста стопки, как и в
случае StackPanel. Но выравнивание может быть полезно
и в направлении роста стопки, если значение ItemHeight
или ItemWidth таково, что в элементе имеется
дополнительное пространство для выравнивания

LayoutTransform

Да. Отличается от RenderTransform тем, что при
использовании LayoutTransform оставшиеся элементы
сдвигаются, чтобы освободить место, но только если не
установлено свойство ItemHeigra или ItemWidth (в
зависимости от ориентации). При комбинировании
компоновки Stretch с преобразованием RotateTransform
или
SkewTransform,
применяемым
в
режиме
LayoutTransform, растяжение происходит, только если
угол кратен 90°, как и в случае StackPanel

Панель WrapPanel обычно используется не для компоновки элементов управления
внутри окна Window, а для компоновки внутри вложенных элементов управления. Как
это делается, объяснено в главе 10.

Панель DockPanel
Панель DockPanel дает простой способ пристыковки элемента к одной из сторон,
растягивая его на всю имеющуюся ширину или высоту. (Отличие от Canvas
заключается в том, что элементы пристыковываются не к одному углу, а ко всей
стороне.) Кроме того, DockPanel позволяет расположить один элемент» так чтобы он
занял все место, свободное от пристыкованных элементов.
В классе DockPanel определено присоединенное свойство Dock(типа System.Windows.Controls.Dock), с помощью которого дочерние элементы могут управлять своим
положением. Оно может принимать четыре значения: Left (подразумевается по

Панель DockPanel

155

умолчанию, если свойство Dock не задано явно), Top, Right и Bottom. Отметим, что у
свойства Dock нет значения Fill, означающего, что нужно заполнить оставшееся место.
Вместо этого действует соглашение о том, что все оставшееся место отдается
последнему дочернему элементу, добавленному в DockPanel, если только свойство
LastChildFill не равно false. Если LastChildFill равно true(по умолчанию), то значение
свойства Dock, заданное для последнего добавленного элемента, игнорируется. Если же
оно равно false, то последний элемент можно пристыковать к любой стороне (по
умолчанию к левой, Left).
На рис. 5.6 показано пять кнопок на панели DockPanel (LastChildFill оставлено равным
true), скомпонованных следующим образом:








Порядок добавления кнопок на панель обозначен числом (и цветом).

Рис. 5.6. Кнопки на панели DockPanel
Как и в случае StackPanel, растяжение элементов определяется подразумеваемым по
умолчанию значением свойства HorizontalAlignment или VerticalAlignment. Если
элемент не будет занимать все пространство, выделенное ему панелью DockPanel, то
можно задать другое выравнивание. На рис. 5.7 показано, как будет выглядеть панель,
когда у всех кнопок, кроме одной, явно задано значение HorizontalAlignment или
VerticalAlignment:





156

Глава 5. Компоновка с помощью панели





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

Рис. 5.7. Кнопки на панели DockPanel не занимают все выделенное им место
Панель DockPanel полезна для организации верхнего уровня интерфейса внутри
элемента Window или Page, когда пристыкованные элементы по большей части
представляют собой другие панели, где и находится все самое важное. Так, обычно к
верхней стороне пристыковывается меню (Menu), справа и слева находятся какие-то
панели, а снизу - строка состояния (StatusBar). Центральную же часть занимают
основные данные приложения.
Порядок добавления дочерних элементов на панель имеет значение, потому что
каждому потомку выделяется все оставшееся место на стороне, к которой он
пристыковывается. (Можно провести аналогию с эгоистом, который, первым сев в
кресло в самолете или в аудитории, занимает оба подлокотника.)
На рис. 5.8 показаны те же пять кнопок, что и на рис. 5.6, но добавленные в другом
порядке (обозначенном числом и цветом). Обратите внимание, что компоновка
изменилась.

Рис. 5.8. Кнопки на панели DockPanel, скомпонованные иначе, чем на рис. 5.6

2

Панель DockPanel

157

DоскРаnеl поддерживает произвольное количество дочерних элементов, а не только
пять. Если к одной стороне пристыковано несколько элементов, то они просто
организуются в стопку соответствующего направления. На рис. 5.9 показана панель
DоскРаnеl с восьмью дочерними элементами — три слева, два сверху, два снизу и один
заполняет оставшееся пространство.

Рис. 5.9. К каждой стороне пристыковано несколько элементов
Таким образом, DockPanel является обобщением StackPanel. Если свойство LastChildFill
равно false, то DockPanel ведет себя, как горизонтальная панель StackPanel, когда все
потомки пристыковываются к левой стороне, и как вертикальная - когда все потомки
пристыковываются к верхней стороне.
В табл. 5.4 показано, как некоторые из свойств компоновки дочерних элементов
применяются к элементам, расположенным на панели DockPanel.
Таблица 5.4. Взаимодействие DockPanel со свойствами компоновки дочерних
элементов
Свойство

Допустимо ли внутри DockPanel

Margin

Да. Свойство Margin определяет, сколько места оставлять
между элементом и стороной панели, а также промежуток
между самими элементами

HorizontalAlignment и
VerticalAlignment

Частично. Как и в случае StackPanel, выравнивание в
направлении пристыковки игнорируется. Иначе говоря,
если Dock равно Left или Right, то не имеет смысла
задавать свойство HorizontalAlignment, а если Тор или
Bottom то свойство VerticalAlignment. Однако для
элемента, заполняющего оставшееся пространство,
применимы оба свойства, HorizontalAlignment и
VerticalAlignment

LayoutTransform

LayoutTransform Да. Отличается от RenderTransform тем,
что при использовании LayoutTransform оставшиеся
элементы сдвигаются, чтобы освободить место. При
комбинировании компоновки Stretch с преобразованием
RotateTransform или SkewTransform, применяемым в
режиме LayoutTransform, растяжение происходит, только
если угол кратен 90°, за исключением элемента,
заполняющего оставшееся пространство (потому что он
может растягиваться в обоих направлениях)

2

158

Глава 5. Компоновка с помощью панели

Панель Grid
Grid(сетка) - самая гибкая из всех панелей и, пожалуй, наиболее употребительная. (В
проектах VisualStudio и ExpressionBlend панель Grid используется по умолчанию.) Она
позволяет расположить дочерние элементы в несколько строк и несколько столбцов, не
полагаясь на режим автоматического переноса (как в WrapPanel). Кроме того,
предоставляется ряд интересных способов управления строками и столбцами. Работа с
панелью Grid очень напоминает использование элемента TABLE в HTML.
СОВЕТ
В WPF также имеется класс Table(в пространстве имен System.Windows.Documents),
предоставляющий примерно такие же возможности, как Grid. Но Table не наследует
классу Panel (и даже классу UIElement). Этот класс, производный от
FrameworkContentElement, предназначен для отображения содержимого документов
и рассматривается в главе 11.
Мы больше не будем демонстрировать разные способы компоновки на примере
раскрашенных кнопок, а с помощью панели Grid сконструируем интерфейс,
напоминающий начальную страницу старых версий VisualStudio. Код в листинге 5.2
определяет сетку размером 4x2, в ячейках которой находятся метка Label и четыре
элемента GroupBox.
Листинг 5.2. Первая попытка построить интерфейс, подобный начальной странице
VisualStudio

















ПанельGrid

159





Article #1
Article #2
Article #3
Article #4




В простейшем случае мы определяем количество строк и столбцов, помещая нужное
число элементов RowDefinition и ColumnDefinition внутрь элементов, соответствующих
свойствам сетки RowDefinitions и ColumnDefinitions. (Немного длинно, но удобно,
когда для отдельных строк и столбцов нужно задать разные размеры.) Затем
позиционируем дочерние элементы с помощью присоединенных свойств Row и
Column, принимающих целочисленные значения, начиная с 0. Если явно не описать
строки и столбцы, то по умолчанию сетка будет состоять всего из одной ячейки. А если
не указать для дочерних элементов свойства Grid.Row и Grid.Column, то по умолчанию
подразумевается значение 0.
Ячейки сетки можно оставлять пустыми, и, наоборот, в одной ячейке может находиться
несколько элементов. В таком случае они просто располагаются один над другим в
соответствии с заданным Z-порядком. Как и в случае панели Canvas, дочерние
элементы, находящиеся в одной ячейке, не взаимодействуют между собой, а просто
перекрывают друг друга.
На рис. 5.10 показан результат визуализации кода в листинге 5.2.

Рис. 5.10. Первая попытка имитировать начальную страницу VisualStudio- не слишком
удачная

160

Глава 5. Компоновка с помощью панели

Сразу бросается в глаза недостаток: список онлайновых статей (Online articles)
слишком мал. Кроме того, было бы лучше, если бы заголовок StartPage занимал всю
ширину сетки. К счастью, обе проблемы легко решить с помощью дополнительных
присоединенных свойств класса Grid: RowSpan и ColumSpan.
По умолчанию свойства RowSpan и ColumnSpan равны 1, но могут принимать любое
значение, большее или равное 1, - соответственно количество строк столбцов,
занимаемых данной ячейкой. (Если значение больше общего количества строк или
столбцов, то ячейка занимает столько строк или столбцов сколько возможно.) Таким
образом, если добавить в последний элемент GroupBox в листинге 5.2 атрибут:
Grid.RowSpan=‛3‛

а в элемент Label- атрибут
Grid.ColumnSpan=‛2‛

то получится куда более симпатичный результат, показанный на рис. 5.11

Рис. 5.11. Использование свойств RowSpan и ColumnSpan позволяет точнее
имитировать начальную страницу VisualStudio.
Сетка на рис. 5.11 все еще выглядит не вполне удовлетворительно, потому по
умолчанию высоты всех строк и ширины всех столбцов равны. В идеале нужно было
бы оставить больше места для списка онлайновых статей, а затем указав, что размеры
первой строки и первого столбца должны соответствовать содержимому. Такой
автоматический выбор размера достигается присваивания свойствам Height и Width
соответственно в элементах RowDefinition и ColumnDefinition специального значения
Auto, нечувствительного к регистру букв. После следующей модификации определений
в листинге 5.2:




ПанельGrid

161











получаем результат, показанный на рис. 5.12.

Рис. 5.12. Окончательный вид страницы после автоматического выбора размера
первой строки первого столбца

FAQ
Как задать для ячеек сетки Grid цвет фона, отступ и рамку по аналогии с
ячейками HTML-таблицы?
Не существует готового механизма, позволяющего задать эти свойства для ячейки
сетки, но его можно легко смоделировать, воспользовавшись тем, что в одной ячейке
может быть несколько элементов. Чтобы придать ячейке цвет фона, достаточно
поместить в нее прямоугольник Rectangle, задав для него такое значение свойства
Fill, при котором он займет всю ячейку. Чтобы получить отступ, можно задать режим
автоматического выбора размера и установить поле Margin для соответствующего
дочернего элемента. Чтобы получить рамку, мы снова воспользуемся
прямоугольником, но явно зададим обводку (свойство Stroke) нужного цвета или
поместим внутрь элемент Border.
Только не забудьте, что элементы Rectangle и Border нужно добавлять в сетку раньше
всех остальных дочерних элементов (или явно задать для них присоединенное
свойство ZIndex), чтобы Z-порядок обеспечил их размещение под основным
содержимым.

162

Глава 5. Компоновка с помощью панели

СОВЕТ
У панели Grid имеется простое свойство ShowGridLines; если оно равно true, то
ячейки будут разграничены желто-синими пунктирными линиями. В промышленном
приложении это не к чему, зато полезно для «отладки» комновоки. На рис.5.13
показан результат установки свойства ShowGridLines=‖True‖ для сетки,
изображенной на рис. 5.12.

Рис. 5.12. Результат задания свойства ShowGridLines для сетки Grid.

Задание размеров строк и столбцов
В отличие от элементов типа FrameworkElement, свойства Height и Width элементов
RowDefinition и ColumnDefinition поумолчанию неравны Auto (или Double.NaN). И, в
отличие от всех прочих свойств Height и Width в WPF, они имеют тип
System.Windows.GridLength, а не double. Поэтому панель Grid поддерживает три
способа задания размера в элементах RowDefinition и ColumnDefinition:




Абсолютный размер - числовое значение Height илиWidth означает, что размер
задан в независимых от устройства пикселах (как и все прочие свойства Height
и Width в WPF). В отличие от других способов задания размера, абсолютные
значения не позволяют строкам и столбцам увеличиваться или сжиматься при
изменении размера самой сетки Grid или находящихся внутри нее элементов.
Автоматический выбор размера – если Height или Width равно Auto (мы уже
экспериментировали с этой установкой выше), то дочерним элементам
выделяется столько места, сколько необходимо, но не больше (для свойств
Height и Width во всех остальных классах WPF это режим по умолчанию). Для
строки эта величина равна высоте самого высокого элемента, а для столбца ширине самого широкого элемента. Когда речь идет о тексте, режим лучше
задания абсолютных размеров, так как можно не опасаться отсечения из-за
выбора другого шрифта или локализации.

Панель Grid


163

Пропорциональное изменение размера - (иногда называется размером «звездочка») предусмотрен специальный синтаксис задания свойств Height и Width,
позволяющий распределить имеющееся пространство поровну или в
соответствии с заданными пропорциями. Если задано пропорциональное
изменение размера, строка и столбец увеличиваются или сжимаются при
изменении размера сетки.

С абсолютным и автоматическим заданием размера все понятно, но вот пропорциональное измерение требует пояснений. Звездочка работает следующим образом:


Если высота строки или ширина столбца равна *, то соответствующему
структурному элементу выделяется все оставшееся место.



Если размер * задан для нескольких строк или столбцов, то все оставшееся
место делится между ними поровну.



Перед символом * можно указывать коэффициент (например, 2* или 5.5*), тогда
соответствующей строке или столбцу будет выделено пропорционально больше
места, чем остальным строкам или столбцам, в размере которых присутствует
символ *. Столбец шириной 2* всегда в два раза шире столбца шириной * (это
означает в точности то же самое, что 1*) в той же самой сетке. Столбец шириной
5.5* в два раза шире столбца шириной 2.75* в той же самой сетке.

Под словами «оставшееся пространство» понимается высота или ширина сетки за
вычетом всех строк или столбцов, для которых задан абсолютный размер или его
автоматический выбор. На рис. 5.14 показано три разных случая задания размеров
простых столбцов в сетке.
По умолчанию высота любой строки и ширина любого столбца сетки равны *. Именно
поэтому на рис. 5.10 и 5.11 строки и столбцы распределены равномерно.

Рис. 5.14. Пропорционально измеренные столбцы сетки

164

Глава 5. Компоновка с помощью панели

FAQ
Почему в WPF не встроена поддержка процентного задания размеров, как в
HTML?
Самый распространенный способ использования процентов в HTML- задание
ширины или высоты элемента 100% - в большинстве панелей можно реализовать с
помощью присваивания свойству HorizontalAlignment или VerticalAlignment значения
Stretch. В более сложных случаях пропорциональное измерение в Grid эквивалентно
заданию размера в процентах, только нужно привыкнуть к синтаксису. Например,
если требуется, чтобы столбец занимал 25% ширины сетки, задайте для него размер *
и проследите, чтобы совокупная ширина остальных столбцов составляла 3*.
Разработчики WPF выбрали такой синтаксис, поскольку программисту не нужно
будет следить за тем, чтобы сумма процентов оставалась равной 100 при
динамическом добавлении или удалении столбцов. К тому же тот факт, что пропорциональное деление применяется к оставшемуся пространству (а не ко всей сетке),
делает поведение более понятным, чем в случае HTML-таблицы с ее смесью
пропорционального и абсолютногозадания размеров строк и столбцов.
КОПНЕМ ГЛУБЖЕ
Использование свойства GridLength в процедурном коде
Конвертер типа System.Windows.GridLengthConverter преобразует строки вида "100"
"auto" или "2*" в структуры GridLength. В С# для создания объекта GridLengtft можно
воспользоваться одним из двух конструкторов. Ключом является перечисление
GridUnitType, в котором определены все три вида значений.
Для задания абсолютного размера можно воспользоваться конструктором, прини
нимающим один аргумент типа double (например, 100):
GridLength length = new GridLength(100);

или конструктором, принимающим дополнительный аргумент типа GridUnitType
GridLength length = new GridLength(100, GridUnitType.Pixel);

В обоих случаях длина 100 выражается в независимых от устройства пикселах.
Конструкторы GridLength не поддерживают значение Double.NaN, поэтому для автоматического задания размера следует указывать значение GridUnitType.Auto:
GridLength length = new GridLength(0, GridUnitType.Auto);

Число, передаваемое в качестве первого параметра, игнорируется. Впрочем, рекомендуется просто пользоваться статическим свойством GridLength. Auto, которое
возвращает готовый экземпляр GridLength, в точности такой, как созданный в
показанной выше строке. Для задания пропорционального изменения размера можно
передать число вместе со значением GridUnitType.Star:
GridLength length = new GridLength(2, GridUnitType.Star);

Этот код эквивалентен записи 2* в XAML. Чтобы получить эквивалент *, нужно в
качестве первого аргумента передать 1, а в качестве второго - GridUnitType.Star.

Панель Grid

165

Интерактивное задание размера с помощью GridSplitter
Еще одна привлекательная особенность панели Grid— поддержка интерактивного
изменения размера строк и столбцов мышью или клавишами (или стилусом, или
пальцем — в зависимости от имеющегося оборудования). Достигается это с помощью
класса GridSplitter в том же самом пространстве имен. В сетку Grid можно добавить
произвольное число дочерних элементов GridSplitter, указав для них присоединенные
свойства Grid.Row, Grid.Column, Grid.RowSpan и/или Grid.ColumnSpan, как для любых
других потомков. Буксировка GridSplitter изменяет размер по меньшей мере одной
ячейки. Что происходит с остальными - изменение размера или просто перемещение зависит от заданного способа изменения размера: пропорционально или как-то иначе.
По умолчанию ячейки, на которых изменение размера отражается непосредственно,
определяются свойствами выравнивания GridSplitter. В табл. 5.5 описано
соответствующее поведение и цветом показано, как выглядит GridSplitter при
различных параметрах (если представлять себе ячейки таблицы как ячейки сетки)
СОВЕТ
Хотя GridSplitter, по умолчанию располагается в одной ячейке, его действие всегда
распространяется на весь столбец (при буксировке по горизонтали) или на всю
строку (при буксировке по вертикали). Поэтому лучше задавать для него свойство
ColumSpan или RowSpan, так чтобы он пересекал всю сетку.
Таблица 5.5. Ячейки, изменяемые непосредственно при буксировке GridSplitter с
различным выравниванием.

166

Глава 5. Компоновка с помощью панели

В классе GridSplitter свойство HorizontalAlignment по умолчанию равно Right, а
свойство VerticalAlignment - Stretch, поэтому по умолчанию он примыкает к правой
стороне указанной ячейки. Чтобы применение GridSplitter приносило нужный эффект,
следует задавать режим выравнивания Stretch хотя бы в одном направлении. В
противном случае может получиться маленькая точка, как видно в табл. 5.5.
Если для всех строк или столбцов задано пропорциональное изменение размера, то
буксировка GridSplitter изменяет коэффициенты для двух строк или столбцов. Если же
для всех строк или столбцов задан абсолютный размер, то буксировка изменяет только
размер верхней или нижней из двух соседних ячеек (в зависимости от направления
буксировки). Оставшиеся ячейки смещаются вниз или вправо, освобождая место.
Буксировка происходит точно так же, когда строки или столбцы изменяются
автоматически, но для той строки или столбца, размер которых меняется,
устанавливаются абсолютные значения.
Все аспекты изменения размера можно, конечно, задать с помощью свойств
выравнивания GridSplitter, однако в этом классе есть еще два свойства, позволяющих
управлять поведением явно и независимо: ResizeDirection (типа GridResizeDirection) и
ResizeBehavior (типа GridResizeBehavior). По умолчанию ResilzeDirection (направление
изменения размера) равно Auto, но может также принимать значение Rows или
Columns, правда, они принимаются во внимание лишь, если GridSplitter растягивается в
обоих направлениях (правая нижняя ячейка в табл. 5.5). ResizeBehavior (поведение при
изменении размера) по умолчанию равно BasedOnAlignment, при этом обеспечивается
поведение, описанное в табл.5.5. Но возможны также значения PreviousAndCurrent,
CurrentAndNew или PreviousAndNext, которые управляют тем, на какие две строки или
столбца изменение размера воздействует непосредственно.
СОВЕТ
Лучше всего поместить GridSplitter в отдельную строку или столбец с
автоматическим выбором размера. В таком случае он не будет перекрывать
содержимое соседних ячеек. Если вы все же решите поместить GridSplitterв одну
ячейку с другими элементами, то хотя бы добавляйте его последним (или задавайте
свойство ZIndex), чтобы для него был наибольшим!

Задание общего размера для строк и столбцов.
В классах RowDefinitions и ColumnDefinitions имеется свойство SharedSizeGroup
позволяющее задать режим, при котором линейные размеры нескольких строк и/или
столбцов будут оставаться одинаковыми даже в случае, когда размер любой из них
изменяется в процессе выполнения программы (например, с помощью GridSplitter).
Свойству SharedSizeGroup можно присвоить произвольное строковое значение
(чувствительное к регистру); оно интерпреруется как имя группы.

Панель Grid

167

Размеры всех строк или столбцов, находящихся в одной группе, изменяются
синхронно.
В качестве простого примера рассмотрим сетку с тремя столбцами, показанную на рис.
5.15; в ней свойство SharedSizeGroup не используется:












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

Компоновка по умолчанию Компоновка после буксировки
SharedSizeGroup вправо
Рис.5 .15. Простая сетка без использования свойства SharedSizeGroup
СОВЕТ
Чтобы элемент GridSplitter был виден и доступен для использования, его ширина
Width(или высота Height - в зависимости от ориентации) должна быть задана явно.

168

Глава 5. Компоновка с помощью панели

На рис. 5.16 показано, что происходит с той же сеткой, когда для первого и последнего
столбцов задано одинаковое значение SharedSizeGroup. Вначале для всех членов
группы устанавливается наибольший автоматически изменяемый или абсолютный
размер. После увеличения ширины первого столбца последний изменяется
соответственно. Средний столбец теперь оказался единственным изменяемым
пропорционально, поэтому он занимает все оставшееся место.
Ниже приведен XAML-код этой сетки.












Компоновка по умолчанию

Компоновка послебуксировки GridSplitter вправо

Рис. 5.16. Та же сетка, что на рис. 5.15, но для первого и последнего столбцов
установлено свойство SharedSizeGroup
Свойство IsSharedSizeScope следует установить потому, что группы размеров могут
применяться сразу к нескольким сеткам! Чтобы избежать потенциального конфликта
имен (и сократить расходы на необходимый в этом случаи ход логических деревьев),
все сетки, к которым применяется одно и то же значение свойства SharedSizeGroup,
должны находиться под общим родителемем, а свойство IsSharedSizeScope для них
должно быть равно true. Это не просто

Панель Grid

169

свойство зависимости в классе Grid,но еще и присоединенное свойство, которое можно
задавать для родителей, не являющихся сетками, например:

…can use SharedSizeGroup…
…can use SharedSizeGroup…

…can use SharedSizeGroup…



В разделе «А теперь все вместе: создание сворачиваемой, стыкуемой, изменяющей
размер панели, как в VisualStudio» в конце главы мы воспользуемся возможностью
задавать SharedSizeGroup для нескольких сеток, чтобы создать удобный
пользовательский интерфейс.

Сравнение Grid с другими панелями
Панель Grid - лучший выбор для особо сложной компоновки, потому что она умеет
делать все, на что способны другие панели, а также многое другое. Единственное, чего
ей не хватает, так это умения динамически генерировать новые строки и столбцы, как
WrapPanel. С помощью сетки можно верстать такие макеты, для которых иначе
потребовалось бы использовать несколько панелей. Например, начальную страницу,
изображенную на рис. 5.12, можно было бы создать, комбинируя DockPanel и
StackPanel. В этом случае DockPanel была бы внешним элементом, к ее верхней стороне
была бы пристыкована метка Label, а к левой – панель StackPanel(и в ней находились
бы первые три элемента GroupBox). Последний элементGroupBox заполнил бы все
оставшееся место.
Чтобы убедиться в том, что панель Grid действительно часто является оптимальным
решением, посмотрим, как можно с ее помощью смоделировать поведение других
панелей, памятуя о том, что в нашем распоряжении есть еще и дополнительные
возможности Grid.

Моделирование Canvas с помощью Grid
Если взять сетку с одной строкой и одним столбцом и для всех дочерних элементом
задавать любое значение HorizontalAlignment и VerticalAlignment, кроме Stretch,то
добавляемые в единственную ячейку элементы будут вести себя так же, как при
добавлении на панель Canvas. Задание HorizontalAlignment равным Left и
VerticalAlignment равным Тор эквивалентно установке для Canvas.Left и Canvas.Top
значения 0. Задание HorizontalAlignment равным Right и VerticalAlignment равным
Bottom эквивалентно установке для Canvas.Right и Canvas.Bottom значения 0. Кроме
того, задание для каждого элемента поля Margin может дать такой же эффект, как
присвоение таких же значений присоединенным свойствам Canvas. Именно так
поступает конструктор VisualStudio, когда пользователь помещает или передвигает
элементы на поверхности конструктора.

170

Глава 5. Компоновка с помощью панели

Моделирование StackPanel с помощью Grid
Сетка с одним столбцом и автоматически измеряемыми строками выглядит, так же, как
вертикальная стопка StackPanel, если каждый элемент вручи помещать в
последовательно нумеруемые строки. Аналогично сетка со строкой и автоматически
изменяемыми столбцами выглядит так же, как горизонтальная стопка, если каждый
элемент вручную помещать в последовательно нумеруемые столбцы.

Моделирование DockPanel с помощью Grid
С помощью свойств RowSpan и ColumnSpan легко сделать так, чтобы внешние элементы пристыковывались к сторонам сетки и автоматически растягивались как на
панели DockPanel. На рис. 5.12 метка Label, по сути, пристыкована к верхней стороне.
В табл. 5.6 показано, как некоторые из свойств компоновки дочерних элевм тов
применяются к элементам, расположенным на панели Grid.
Таблица 5.6. Взаимодействие Grid со свойствами компоновки дочерних элементов
Свойство

Допустимо ливнутри DockPanel

Margin

Да. Свойство Margin определяет, сколько места оставлять
между элементом и сторонами объемлющей его ячейки

HorizontalAlignment и
VerticalAlignment

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

LayoutTransform

Да. Отличается от RenderTransform тем, что при
использование LayoutTransform элементы остаются
внутри ячеек (если это ВОЗМОЖНО) и учитывается
величина поля Margin. В отличнее RenderTransform,
элемент, вышедший в результате масштабирования за
пределы ячейки, отсекается

СОВЕТ
Хотя складывается впечатление, что панель Grid может практически все, в случае,
когда количество дочерних элементов заранее неизвестно, лучше все же использовать StackPanel или WrapPanel(обычно при компоновкедискретных элементов
управления, описываемых в главе 10). Кроме того, DockPanel со сложными
подпанелями иногда предпочтительнее Grid, потому что изоляция, которую
обеспечивают подпанели, удобнее в ситуации, когда пользовательский интерфейс
изменяется. Если для этой цели использовать единственную сетку, то при добавлении
строк и столбцов для сохранения иллюзии стыковки придется изменять свойства
RowSpan и ColumnSpan.

Примитивные панели

171

Примитивные панели
Рассмотренные выше панели в общем случае полезны для компоновки на уровне как
приложения, так и отдельных элементов управления. Но в состав WPF входит также
несколько простых панелей, более удобных внутри элементов управления, - все равно,
идет ли речь о стилизации встроенного элемента (см. главу 14 «Стили, шаблоны,
обложки и темы») или о создании нового нестандартного элемента (см. главу 20
«Пользовательские и нестандартные элементы управления»). Они не настолько
универсальны, как предыдущие панели, но все же заслуживают беглого знакомства.
Все эти панели определены в пространстве имен System.Windows.Controls.Primitives, за
исключением
ToolBarTray,
которая
находится
в
пространстве
имен
System.Windows.Controls.

Панель TabPanel
TabPanel очень похожа наWrapPanel, но обладает некоторыми ограничениями, с одной
стороны, и дополнительными возможностями, с другой. Как следует нз ее названия, эта
панель используется в подразумеваемом по умолчанию стиле элемента TabControl, где
служит для организации вкладок. В отличие от WrapPanel, она поддерживает только
горизонтальную ориентацию стопки и вертикальное оборачивание. Когда происходит
оборачивание, элементы равномерно растягиваются, так что все строки занимают всю
ширину панели. Элемент управления TabControl рассматривается в главе 10.

Панель ToolBarPanel
Панель ToolBarPanel, по умолчанию используемая в стиле элемента ToolBar, напоминает StackPanel. Однако она работает совместно с панелью переполнения (см.
ниже) и организует элементы, не умещающиеся в ее пределах (главной области панели
инструментовToolBar). Элемент ToolBar рассматривается в главе 10.

Панель ToolBarOverflowPanel
Панель ToolBarOverflowPanel- это упрощенный вариант WrapPanel, поддерживающий
только горизонтальную ориентацию стопки и вертикальное оборачивание. Она
используется в подразумеваемом по умолчанию стиле ToolBar для отображения не
помещающихся элементов в области переполнения. Помимо возможностей WrapPanel
здесь имеется свойство Wrap Width, которое ведет себя, как свойство Padding. Однако
нет убедительных причин, по которым эту панель следовало бы предпочесть
WrapPanel.

Панель ToolBarTray
Панель ToolBarTray поддерживает только потомков типа ToolBar(и возбуждает
исключение InvalidOperationException при попытке добавить дочерний элемент
Другого типа). Она компонует элементы ToolBar последовательно (по умолчанию
горизонтально) и позволяет перетаскивать их, организуя дополнитеные строки, или
сворачивать и разворачивать соседние элементы ToolВаr.

172

Глава 5. Компоновка с помощью панели

Панель UniformGrid
Панель UniformGrid интересна, но ее практическая полезностьсомнительна. Это
упрощенный вариант сетки Grid, в которой все строки и столбцы имеют размер *, и
изменить это невозможно. Из-за этого в классе UniformGrid есть два простых свойства
типа double, задающих количество строк и столбцов, вместо куда более многословных
определений коллекций RowDefinitions и ColumnDefinitions. Кроме того, в нем нет
никаких присоединенных свойств; дочерние элементы добавляются по строкам, и в
каждой ячейке может быть только один потомок.
Наконец, если количество строк и столбцов явно не задано (или количество дочерних
элементов превосходит явно заданное количество ячеек), то UniformGrid
автоматически выбирает подходящие значения. Например, если количество элементов
от 2 до 4, то они размещаются в сетке 2x2, если от 5 до 9 - то в сетке 3x3, если от 10 до
16 - то в сетке 4x4 и т. д. На рис. 5.17 показано, как по умолчанию выглядит панель
UniformGrid, когда в нее добавлено восемь кнопок

Рис. 5.17. Восемь кнопок на панели UniformGrid

Панель SelectiveScrollingGrid
SelectiveScrollingGrid - подкласс Grid, используемый в подразумеваемом по умолчанию
стиле элемента управления DataGridRow. В дополнение к функциональности Grid он
позволяет «замораживать» некоторые ячейки, не препятствуя прокрутке остальных.
Этим поведением управляет свойство SelectiweScrollingOrientation, принимающее
следующие значения:


None - ячейки не могут прокручиваться ни в каком направлении



Horizontal — ячейки могут прокручиваться только по горизонтали



Vertical — ячейки могут прокручиваться только по вертикали



Both - ячейки могут прокручиваться в любом направлении. Это значения по
умолчанию

Обработка переполнения содержимого

173

Обработка переполнения содержимого
Встроенные панели делают все возможное для того, чтобы удовлетворить потребности
своих дочерних элементов в месте на экране. Но иногда они вынуждены выделять
потомкам меньше места, чем требуется, и бывает, что потомки отказываются
полностью рисовать себя, когда места недостаточно. Например, может случиться, что
для элемента явно задана ширина, превышающая ширину объемлющей панели. Или
список ListBox содержит так много элементов, что они не помещаются в объемлющее
его окно Window.В таких случаях возникает проблема переполнения содержимого.
Для ее разрешения можно применять различные стратегии:
•

отсечение

•

прокрутку

•

масштабирование

•

оборачивание

•

обрезку

В этом разделе рассматриваются первые три стратегии. Примеры оборачивания уже
встречались при обсуждении панели WrapPanel (а также TabPanel и
ToolBarOverflowPanel). Это единственный способ реализовать оборачивание для нетекстового содержимого (компоновка текста рассматривается в главе 11).
Обрезка - это более интеллектуальная форма отсечения. Она поддерживается только
для текста элементами TextBlock и AccessText, в которых есть свойство TextTrimming
(типа System.Windows.TextTrimming), принимающее значения None(по умолчанию),
CharacterEllipsis или WordEllipsis. В последних двух случаях отброшенный текст
заменяется многоточием (...), а не просто прерывается в произвольном месте.

Отсечение
Отсечение дочерних элементов - это тот способ, который панели применяют по
умолчанию, когда потомков становится слишком много. Отсечение может происходить
на краях панели или внутри нее (например, на краях ячейки сетки или в заполняемой
области DockPanel). Впрочем, этим поведением можно в какой-то степени управлять.
Во всех классах, производных от UIElement, есть булевское свойство ClipToBounds,
которое управляет тем, можно ли рисовать дочерние элементы вне границ родителя.
Однако, если внешний край элемента совпадает с внешним краем Window или
Page,отсечение все равно производится. Этот механизм не является средством рисовать
вне границ окна Window. (Непрямоугольные окна обсуждаются в главе 7
«Структурирование и развертывание приложения».)
Несмотря на то, что все панели наследуют свойство ClipToBounds,большая их часть
автоматически отсекает потомков вне зависимости от значения этого свойства. Но
панели Canvas и UniformGrid по умолчанию не отсекают свои дочерние элементы, и
обе поддерживают установку для свойства ClipToBounds значения true, чтобы
принудительно включить режим отсечения.

174

Глава 5. Компоновка с помощью панели

На рис. 5.18 показано, как свойство ClipToBounds влияетна изображение кнопки,
которая целиком не помещается на родительской панели Canvas(светлокоричневого
цвета)

Рис. 5.18. Свойство ClipToBounds определяет, будут ли дочерние элементы
рисоваться за пределами панели
Такое поведение означает, что в случае, когда ClipToBounds не равно true, размер
Canvas неважен; можно задать Height и Width равными 0, а содержимое равно будет
рисоваться, как будто Canvas занимает весь экран!
Элементы, производные от Control, также могут управлять отсечением своего
содержимого с помощью свойства ClipToBounds. Например, в классе Button
ClipToBounds по умолчанию равно false. На рис. 5.19 показано, что происходит, когда
для этого свойства установлено значение true, а внутренний текст масштабируется с
помощью преобразования ScaleTransform (примененного в режиме RenderTransform).

Рис. 5.19. Свойство ClipToBounds можно использовать в элементах управления
(например, Button) для управления рисованием внутреннего содержимого.
СОВЕТ
Панель Canvas можно вставлять в качестве промежуточного элемента для
предотвращения отсечения в других панелях. Например, отсечения большой кнопки
на границе Grid можно избежать, если вставить в соответствующую ячейку панель
Canvas(которая займет ее целиком), а уже внутрь Canvas поместить кнопку Button
Разумеется, если вы хотите, чтобы поведение кнопки при растяжения было таким же,
как если бы она была прямым потомком Grid, то придется написать код.
Тот же подход применим для предотвращения отсечения на границах внутренних
ячеек сетки, но если нужно, чтобы элемент «просачивался» в соседние ячейки, то
обычно лучше увеличить значения его свойств RowSpan и/или ColumnSpan.

Обработка переполнения содержимого

175

ПРЕДУПРЕЖДЕНИЕ
Отсечение производится
RenderTransform.

до

применения

преобразований

в

режиме

Если элемент увеличивается путем применения преобразования ScaleTransform в
режиме RenderTransform, то он может выйти за границы родительской панели, не
подвергаясь отсечению (при условии, что он не достигнет края Window или Page).
Уменьшение элемента с помощью ScaleTransform в режиме RenderTransform- вещь
более тонкая. Если бы немасштабированный элемент был подвергнут отсечению изза того, что вышел за границы родителя, то и масштабированный элемент отсекается
точно таким же образом, пусть он даже целиком помещается на родительской
панели! Объясняется это тем, что отсечение — часть процесса компоновки и к
моменту применения RenderTransform оно уже полностью определено. Если вы
хотите уменьшить большой элемент с помощью преобразования ScaleTransform, то
попробуйте применить его в режиме LayoutTransform - возможно, так получится
лучше.

Прокрутка
Во многих приложениях критически важна возможность прокручивать содержимое,
которое из-за его размера нельзя увидеть целиком. С WPF это просто стоит поместить
элемент внутрь элемента управления System.Windows.Controls.ScrollViewer, как он
сразу же становится прокручиваемым. ScrollViewer применяет элементы управления
ScrollВаг, которые автоматически присоединяются к содержимому, когда в этом
возникает необходимость.
В классе ScrollViewer имеется свойство Content, значением которого может быть одинединственный элемент, обычно в этом качестве выступает некая панель. Поскольку
Content - свойство содержимого с точки зрения XAML, то объект, нуждающийся в
прокрутке, можно описать в разметке как дочерний элемент:






На рис. 5.20 показано окно Window, содержащее простую панель StackPanel, — с
применением ScrollViewer и без него.
Элементы ScrollBar отвечают на различные события ввода, в том числе на нажатия
клавиш со стрелками для перемещения на небольшое расстояние, клавиш PageUp и
PageDown - на большее расстояние и сочетаний клавиш Ctrl+Home и Ctrl+End для
перемещения в начало и конец соответственно.

176

Глава 5. Компоновка с помощью панели

Без прокрутки

С прокруткой

Рис. 5.20. ScrollViewer позволяет прокручивать элемент, который не умещается в
отведенном ему пространстве
В классе ScrollViewer имеется еще ряд свойств и методов для манипулирувания из
программы, но самыми важными являются свойства VerticalScrolBarVisibility и
HorizontalScrollBarVisibility. Оба они имеют тип перечисления ScrollBarVisibility,
которое определяет четыре состояния полосы прокрутки:


Visible - полоса прокрутки всегда присутствует, даже если она не нужна. Если
необходимости в ней нет, то она выглядит неактивной и не реагирует на
события ввода. (Однако это не то же самое, что значение Disabled свойсва
ScrollBarVisibility.)



Auto - полоса прокрутки видна, если содержимое нуждается в прокрутке в
данном направлении. В противном случае полоса прокрутки отсутствует.



Hidden- полоса прокрутки всегда невидима, но логически существует, есть
содержимое можно прокручивать клавишами со стрелками. Поэтому
содержимое полностью доступно в данном направлении.



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

По умолчанию свойство VerticalScrollBarVisibility равно Visible, а свойство
HorizontalScrollBarVisibility равно Auto, поскольку именно это характерно для
большинства приложений. В зависимости от содержимого внутри ScrollViewer тонкое
различие между Hidden и Disabled может оказаться вовсе не таким тонким. Например,
на рис. 5.21 показаны два окна Window, содержащие ScrollViewer, внутри которого
находится одна и та же панель WrapPanel. Единственное различие заключается в том,
что в первом окне свойство HorizontalScrollBarVisibility равно Hidden, а во втором Disabled.
В случае Hidden панель WrapPanel получает столько места, сколько ей требуется (как
если бы HorizontalScrollBarVisibility было равно Visible или Auto), поэтому использует
его целиком и компонует дочерние элементы в одну строку. В случае Disabled для
панели выделяется только ширина, равная ширине родительского окна Window,
поэтому производится оборачивание, как если быэлемента ScrollViewer не
существовало.

Обработка переполнения содержимого

177

Рис. 5.21. Хотя горизонтальная полоса прокрутки в обоих случаях невидима, различная
установка свойства HorizontalScrollBarVisibility радикально изменяет внешний вид
панели WrapPanel.
СОВЕТ
В главе 3 «Основные принципы WPF» было объяснено, что подразумеваемое по
умолчанию визуальное дерево элемента ListBox содержит ScrollViewer. Воспользовавшись синтаксисом присоединенных свойств, его свойствам VerticalScrollBarVisibility и HorizontalScrollBarVisibility можно присвоить значения и тем самым
повлиять на поведение скрытого ScrollViewer:






Достаточно заменить ScrollViewer на Viewbox (и изменить заголовок окна) чтобы
получить результат, показанный на рис. 5.24:






И теперь мы видим все восемь кнопок, каким бы ни был размер окна!

Рие. 5.24. Та же панель StackPanel, что на рис.5.20, только помещен вовнутрь Viewbox,
а не ScrollViewer.
ПРЕДУПРЕЖДЕНИЕ
Viewbox отключает оборачивание!
Элемент Viewbox удобен во многих ситуациях, но не подходит в случаи, когда
требуется оборачивание, например при переносе текста на другую строку или
компоновке содержимого с помощью WrapPanel. Дело в том, что содержимому
сначала выделяется столько места в обоих направлениях, сколько оно запрашивает, а
уже потом производится масштабирование (если необходимо). На рис. 5.25 этот
эффект продемонстрирован на примере той же самой WrapPanel, что и на рис. 5.21.
но с заменой ScrollViewer на Viewbox.

Обработка переполнения содержимого

181

Рис.5.25. Панель WrapPanel, изображенная на рис. 5.21, не нуждается в
оборачивании, если помещена не в ScrollViewer, а в Viewbox.
В результате все содержимое располагается в одной строке, которая может получиться гораздо меньше, чем вам хотелось бы. Установка для свойства StretchDirection значения UpOnly вместо подразумеваемого по умолчанию Both не поможет. Компоновка содержимого Viewbox производится до потенциального масштабирования. Поэтому значение UpOnly предотвращает уменьшение кнопок, но они
по-прежнему располагаются в одной строке, как показано на рис. 5.26.

Рис. 5.26. Установка для Viewbox, показанного на рис. 5.25, режима
StretchDirection="UpOnly" предотвращает уменьшение кнопок, но не влияет на
внутреннюю компоновку WrapPanel
Получившийся результат аналогичен установке режима HorizontalScrollBarVisibility'Hidden" на рис. 5.21 с тем отличием, что прокрутить содержимое и увидеть то, что
скрыто, невозможно даже с помощью клавиатуры.

182

Глава 5. Компоновка с помощью панели

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер
панели
Давайте протестируем имеющиеся в WPF средства компоновки, попытавшись
сконструировать более сложный пользовательский интерфейс. В этом разделе мы
создадим панели по типу применяемых в VisualStudio - панели, умеющие
пристыковываться к основному содержимому или сворачиваться в кнопку вдоль края
окна. Если панель свернута, то при задержке указателя мыши над кнопкой
показывается панель, но не в пристыкованном виде, а поверх основного содержимого.
Вне зависимости от того, пристыкована панель или нет, ее размер можно изменить с
помощью разделителя. На рис. 5.27-5.31 изображено несколько последовательных
состояний пользовательского интерфейса в процессе работы с ним.

Puc.5.27. Сначала обе панели скрыты и видны только кнопки, пристыкованные к
правому краю.

Puc.5.28. Если задержать указатель мыши над кнопкой Toolbox, то появится
непристыкованная панель Toolbox, которая останется открыта, пока пользователь
не переместит указатель на основное содержимое или на кнопку другой панели.

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели

183

Рис. 5.29.Размер непристыкованной панели можно изменять, но она все равно будет
перекрывать основное содержимое.

Puc.5.30. Если пристыковать панель Toolbox, щелкнув noзначку канцелярской кнопки,
то размер области основного содержимого уменьшится в соответствии с
имеющимся пространством, а кнопка Toolbox пропадет.

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

184

Глава 5. Компоновка с помощью панели

Рис. 5.32. Если задержать указатель мыши над кнопкой SolutionExplorer, то появится
непристыкованная панель SolutionExplorer, перекрывающая все остальное содержимое
(включая пристыкованную панель Toolbox). Размер этой непристыкованной панели
можно изменять независимо, увеличивая или уменьшая перекрытую область

Puc. 5.33. Панель SolutionExplorer можно пристыковать, щелкнув no значку
канцелярской кнопки. При этом панель Toolbox отодвигается, а правая полоса с
кнопками исчезает, потому что кнопок, представляющих непристыкованные панели,
больше не осталось
Когда обе панели не пристыкованы, их размеры изменяются независимо от основного
содержимого и друг от друга. Если же обе панели пристыкованы (как на рис. 5.33), то
интерфейс ведет себя, как одна сетка Grid с тремя ячейками, которые могут меняться в
размерах, но никогда не перекрываются.
Ну и как подойти к реализации подобного интерфейса? Поскольку для интерактивного
изменения размера нужны разделители, кажется естественным взять за основу панель
Grid с разделителями GridSplitter. Никакая другая встроенная панель не предоставляет
интерактивных разделителей. Но поскольку непристыкованные панели должны
перекрываться и независимо менять размеры, то одной сетки недостаточно. Мы
воспользуемся тремя независимыми сетками - по одной для основного содержимого и
двух панеле и расположим их поверх друг друга. А чтобы обеспечить синхронизацию
трехнезависимых сеток, когда в этом есть необходимость (то есть когда панели

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели

185

пристыкованы), мы применим свойство SharedSizeGroup. На рис. 5.34 показано, как эти
сетки устроены и связаны между собой.

Рис. 5.34. Три независимые сетки Grid используются для реализации двух
сворачиваемых, стыкуемых, изменяющих размер панелей
В нижнем слое (слое 0) расположено основное содержимое, которое, растягиваясь,
заполняет сетку, когда обе панели свернуты. Задержка указателя мыши над любой
кнопкой изменяет видимость соответствующей панели в слое 1 или 2 с Collapsed на
Visible. Разделитель любой панели позволяет изменить соотношение места,
занимаемого ею и столбцом слева от нее (он пуст, поэтому сквозь него видно
содержимое, находящееся ниже, в слое 0).
Самое интересное происходит, когда наступает время пристыковать панель. При
пристыковке панели 1 основное содержимое необходимое уменьшить до ширины
пустого столбца 0 в слое 1. Поэтому в слой 0 динамически добавляется

186

Глава 5. Компоновка с помощью панели

пустой столбец такой же ширины, как панель 1. Поскольку мы не определяем жестко
ширину в коде, а используем свойство SharedSizeGroup, при работе с разделителем в
слое 1 нижний слой остается синхронизированным.
Та же техника применяется при стыковке панели 2, только теперь фиктивный столбец
необходимо добавить во все нижележащие слои (0 и 1). В результате обе панели видны
одновременно и не перекрываются, а размер основного содержимого в слое 0 остается
правильным в присутствии одной или двух пристыкованных панелей, а также при
полном их отсутствии. Отметим, что порядок следования панелей в случае, когда они
обе пристыкованы, фиксирован.
Все три сетки помещены (куда бы вы думали?) в сетку с одной строкой и одним
столбцом, чтобы они могли перекрывать друг друга и вместе с тем растягиваться,
занимая все отведенное им место, Z-порядок слоя 0 всегда наименьший, но Z-порядок
двух остальных слоев может меняться так, чтобы текущая непристыкованная панель
всегда была наверху.
В листинге 5.3 приведен XAML-код приложения, изображенного на рис. 5.27-5.33,
некоторые несущественные части для краткости опущены. Весь проект целиком
имеется
в
исходном
коде,
прилагаемом
к
книге
(по
адресу
http://informit.com/title/9780672331190).
Листинг 5.3. VisualStudioLikePanes.xaml — XAML-частъ реализации приложения,
изображенного на рис. 5.27—5.33




























Toolbox

… (содержимое панели находится в строке 1)

















188

Глава 5. Компоновка с помощью панели




Solution Explorer








На верхнем уровне окна Window расположена панель DockPanel, которая организует
меню, панель StackPanel, содержащую полосу с кнопками (повернутую на 90° с
помощью преобразования RotateTransform), и сетку из одной ячейки где находятся три
сетки, определяющие «слои». Отметим, что меню Menu добавляется в DockPanel
раньше, чем StackPanel, чтобы оно растянулось на ширину вдоль верхнего края окна.
В каждой сетке-слое имеется всего один столбец для хранения содержимого и во всех
трех случаях содержимое заключено в сетку. Каждый разделитель GridSplitter
пристыкован к левой стороне столбца с содержимым, поэтому он перекрывает
содержимое из других слоев. Отметим одну тонкость - заголовок каждой панели
помещен не в метку Label, а в элемент TextBlock, чтобы можно было установить
свойство TextTrimming-―CharacterEllipsis‖, тогда при уменьшении размера панели
система будет не просто отсекать текст заголовка, а заменять отброшенную часть
многоточием. Это выглядит более профессионалы!
В листинге 5.4 приведен застраничный код на С#, дополняющий код в листинге 5.3.
Листинг 5.4. VisualStudioLikePanes.xaml.csизображенного на рис. 5.27-5.33
using
using
using
using

System;
System.Windows;
System.Windows.Controls;
System.Windows.Media.Imaging;

public partial class MainWindow : Window
{
// Фиктивные столбцы для слоев 1 и 0:
ColumnDefinition column1CloneForLayer0;

С#-частъ

реализации

приложения,

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели
public MainWindow()
{
// Инициализировать фиктивные столбцы использованные когда панели пристыкованы:
column1CloneForLayer0 = new ColumnDefinition();
column1CloneForLayer0.SharedSizeGroup = "column1";
column2CloneForLayer0 = new ColumnDefinition();
column2CloneForLayer0.SharedSizeGroup = "column2";
column2CloneForLayer1 = new ColumnDefinition();
column2CloneForLayer1.SharedSizeGroup = "column2";
}
// Переключаем состояние:пристыковано/не пристыковано(Панель 1)
public void pane1Pin_Click(object sender, RoutedEventArgs e)
{
if (pane1Button.Visibility == Visibility.Collapsed)
UndockPane(1)
else
DockPane(1);
}
// Переключаем состояние:пристыковано/не пристыковано(Панель 2)
public void pane2Pin_Click(object sender, RoutedEventArgs e)
{
if (pane2Button.Visibility == Visibility.Collapsed)
UndockPane(2);
else
DockPane(2);
}
// Показываем панель 1 когда указатель мыши находится над её кнопкой
publicvoid pane1Button_MouseEnter(object sender, RoutedEventArgs e)
{
layer1.Visibility = Visibility.Visible;
// Коррекцируем Z-порядок, чтобы панель всегда была вверху:
Grid.SetZIndex(layer1, 1);
Grid.SetZIndex(layer2, 0);
// Скрываем вторую панель если она не пристыкована
if (pane2Button.Visibility == Visibility.Visible)
layer2.Visibility = Visibility.Collapsed;
}
// Показываем панель 2 когда указатель мыши находится над её кнопкой
public void pane2Button_MouseEnter(object sender, RoutedEventArgs e)

189

190

Глава 5. Компоновка с помощью панели

{
layer2.Visibility = Visibility.Visible;
// Коррекцируем Z-порядок, чтобы панель всегда была вверху:
Grid.SetZIndex(layer2, 1);
Grid.SetZIndex(layer1, 0);
// Скрываем вторую панель если она не пристыкована
if (pane1Button.Visibility == Visibility.Visible)
layer1.Visibility = Visibility.Collapsed;
}
// Скрываем все непристыкованые панели когда указатель мыши, перемещается в слой 0
public void layer0_MouseEnter(object sender, RoutedEventArgs e)
{
if (pane1Button.Visibility == Visibility.Visible)
layer1.Visibility = Visibility.Collapsed;
if (pane2Button.Visibility == Visibility.Visible)
layer2.Visibility = Visibility.Collapsed;
}
// Скрываем вторую панель если она не пристыкована, когда указатель мыши премещается
на панель 1
public void pane1_MouseEnter(object sender, RoutedEventArgs e)
{
// Скрываем вторую панель если она не пристыкована
if (pane2Button.Visibility == Visibility.Visible)
layer2.Visibility = Visibility.Collapsed;
}
// Скрываем вторую панель если она не пристыкована, когда указатель мыши премещается
на панель 2
public void pane2_MouseEnter(object sender, RoutedEventArgs e)
{
// Скрываем вторую панель если она не пристыкована
if (pane1Button.Visibility == Visibility.Visible)
layer1.Visibility = Visibility.Collapsed;
}
// Пристыковываем паель, при этом скрывается соответсвующая ей кнопка
public void DockPane(int paneNumber)
{
if (paneNumber == 1)
{
pane1Button.Visibility = Visibility.Collapsed;
pane1PinImage.Source = newBitmapImage(new Uri(‚pin.gif‛, UriKind.Relative));
//Добавляем клонированный столбец в слой 0:

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели
layer0.ColumnDefinitions.Add(column1CloneForLayer0);
// Добавляем клонированный столбец в слой 1, но только если панель 2 пристыкована
if (pane2Button.Visibility == Visibility.Collapsed)
layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else if (paneNumber == 2)
{
pane2Button.Visibility = Visibility.Collapsed;
pane2PinImage.Source = newBitmapImage(new Uri(‚pin.gif‛, UriKind.Relative));
// Добавляем клонированный столбец в слой 0
layer0.ColumnDefinitions.Add(column2CloneForLayer0);
// Добавляем клонированный столбец в слой 1, но только если панель 1 пристыкована
if (pane1Button.Visibility == Visibility.Collapsed)
layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
}
// Отстыковываем панель при этом становится видна соответсвующая ей кнопка
public void UndockPane(int paneNumber)
{
if (paneNumber == 1)
{
layer1.Visibility = Visibility.Visible;
pane1Button.Visibility = Visibility.Visible;
pane1PinImage.Source = new BitmapImage
(new Uri("pinHorizontal.gif", UriKind.Relative));
// Удаляем клонированные столбцы из слоев 0 и 1:
layer0.ColumnDefinitions.Remove(column1CloneForLayer0);
// Этот столбец присутсвует не всегда, но метод Remove
// молча игнорирует попытку удалить несущиствующий столбец
layer1.ColumnDefinitions.Remove(column2CloneForLayer1);
}
else if (paneNumber == 2)
{
layer2.Visibility = Visibility.Visible;
pane2Button.Visibility = Visibility.Visible;
pane2PinImage.Source = new BitmapImage
(new Uri("pinHorizontal.gif", UriKind.Relative));
// Удаляем клонированные столбцы из слоев 0 и 1:
layer0.ColumnDefinitions.Remove(column2CloneForLayer0);

191

192

Глава 5.Компоновка с помощью панели

// Этот столбец присутсвует не всегда, но метод Remove
// молча игнорирует попытку удалить несущиствующий столбец
layer1.ColumnDefinitions.Remove(column2CloneForLayer1);
}
}
}

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

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

6







События ввода: клавиатура, мышь, стилус и мультисенсорные
устройства
Маршрутизируемые события
События клавиатуры
События мыши
События стилуса
Мультисенсоряые события
Команды

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

Маршрутизируемые события
В главе 3 «Основные принципы WPF» показано, как с помощью свойств зависимости
WPF реализует дополнительную инфраструктуру поверх хорошо известной идеи
свойств .NET. Но этим дело не ограничивается - WPF еще и дополняет понятие
события. Маршрутизируемые события предназначены для работы с деревьями
элементов. Сгенерированное маршрутизируемое событие может распространяться
вверх или вниз по визуальному и логическому дереву, достигая каждого элемента
простым и естественным образом без написания дополнительного кода.
Маршрутизация событий позволяет большинству приложений вообще не задумываться
о наличии визуального дерева (что очень удобно для стилизации) и является основой
механизма композиции элементов в WPF. Например, кнопка Button генерирует событие
Click в результате обработке низкоуровневых событий MouseLeftButtonDown и
KeyDown. Но когда пользователь нажимает левую кнопку мыши, наведя ее указатель
на стандартную кнопку, он в реальности взаимодействует с визуальным дочерним
элементом ButtonChrome или TextBlock. Однако поскольку событие распространяется
вверх по визуальномудереву, то элемент Button в конечном итоге получит это событие
и сможет его обработать. Аналогично в случае кнопки Stop, подобной кнопке
медиаплеера (см. главу 2 «Все тайны XAML»), пользователь может нажать левую
кнопку мыши, поместив указатель над логическим потомком Rectangle.

194

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Поскольку событие распространяется вверх по логическому дереву, элемент Button
равно увидит событие и сможет на него отреагировать. (Впрочем, если: хотите
различать события элемента Rectangleи объемлющей его Button, ничто не помешает
вам это сделать.)
Таким образом, внутрь любого элемента, к примеру, Button, можно поместить сколько
угодно сложное содержимое (применяя технику, описанную в главе 1 «Стили,
шаблоны, обложки и темы»), но щелчок левой кнопкой мыши при наведении указателя
на любой внутренний элемент все равно приведет к возникновению события Click для
родительской кнопки. Не будь маршрутизируемых событий, авторы внутреннего
содержимого или клиенты кнопки должны были бы писать дополнительный код для
связывания всего воедино.
Реализация и поведение маршрутизируемых событий имеют много общего со
свойствами зависимости. Как и при обсуждении свойств зависимости, мы сначала
посмотрим, как реализуется простое маршрутизируемое событие, чтобы сделать
обсуждение более конкретным. Затем рассмотрим некоторые особенности
маршрутизируемых событий и применим это к диалоговому окну About из главы 3.

Реализация маршрутизируемого события
В большинстве случаев маршрутизируемые события внешне мало чем отличаются от
обычных событий .NET. Как и в случае со свойствами зависимости, .NET-совместимые
языки (кроме XAML) ничего не знают о том, что такое
маршрутизация.
Дополнительную поддержку предоставляют лишь различные классы WPF.
В листинге 6.1 показана схема реализации маршрутизируемого события Click в классе
Button. (На самом деле событие Click реализовано в базовом классе Button, но сейчас
это несущественно.)
Напомним, что свойства зависимости представлены открытыми статистическими
полями типа DependencyProperty с принимаемым по соглашению суффиксом Property.
Точно так же маршрутизируемые события представлены открытыми статическими
полями типа RoutedEvent с принимаемым по соглашении суффиксом Event. Так же, как
свойство зависимости, маршрутизируемое событие регистрируется в статическом
конструкторе, и дополнительно определяется обычное событие .NET- обертывающее
событие, чтобы было писать процедурный код и добавлять обработчик события в
XAML-коде спомощью стандартного синтаксиса атрибутов событий. Как и
обертывающее свойство, обертывающее событие не должно делать в аксессорах
ничего) кроме вызова методов AddHandler и RemoveHandler,
Листинг 6.1. Стандартномреализация маршрутизируемою сабытия
publicclassButton : ButtonBase
{
// Маршрутизируемое событие
publicstaticreadonly RoutedEvent ClickEvent;

Все вместе: создание сворачиваемой, стыкуемой, изменяющей размер панели

195

RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Button));
…
}
// Обертывающие событие
publicevent RoutedEventHandler Click
{
add { AddHandler(Button.ClickEvent, value); }
remove { RemoveHandler(Button.ClickEvent, value); }
}
protectedoverridevoid OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
…
// Сгенерировать событие
RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
…
}
…
}

Методы AddHandler и RemoveHandler наследуются не от класса DependencyObject, а от
UIElement. Они соответственно присоединяют и отсоединяют делегат от
маршрутизируемого события. Внутри метода OnMouseLeftButtonDown вызывается
метод RaiseEvent(также определенный в базовом классе UIElement), которому
передается поле типа RoutedEvent, соответствующее генерации события Click. В
качестве источника события передается текущий экземпляр Button (this). В листинге
это не показано, но на самом деле событие Click кнопки генерируется также в ответ на
событие KeyDown, то есть поддерживается нажатие кнопки посредством клавиши
пробела или Enter.

Стратегии маршрутизации и обработчики событий
В момент регистрации маршрутизируемого события задается одна из трех стратегий,
маршрутизации- вариантов распространения события по дереву элементов. Стратегии
описываются перечислением RoutingStrategy:


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



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

196

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства


Direct- событие возникает только в элементе-источнике. Точно так же ведут себя
обычные события .NET; различие лишь в том, что к маршрутизируемому
событию применяются и другие механизмы, в частности триггеры событий.

Сигнатуры обработчиков маршрутизируемых событий устроены так же, как сигнатуры
всех обработчиков событий в .NET: первый параметр – объект типа System.Object,
который обычно называют sender, второй (обычно называемый е)- экземпляр класса,
производного от System.EventArgs. Передаваемый обработчику параметр sender- это
всегда элемент, к которому присоединенданный обработчик. Параметр е является
объектом класса RoutedEventArgs(или производного от него) - подкласса EventArgs,
обладающего следующими полезными свойствами:


Source - элемент логического дерева, первоначально сгенерировавший событие.



OriginalSource - элемент визуального дерева, первоначально сгенерировавший
событие (например, в случае стандартной кнопки Button это буде дочерний
элемент TextBlock или ButtonChготе).



Handled - булевский флаг, которому можно присвоить значение самым
пометить, что событие обработано. Именно таким способом прерывается
туннелирование и всплытие.



RoutedEvent - сам объект маршрутизированного события (например,
ButtonClickEvent), который может быть полезен для различения событий в
случае, когда один и тот же обработчик используется для обработки разных
событий.

Наличие свойств Source и OriginalSource позволяет работать как с высокоуровневым
логическим, так и с низкоуровневым визуальным деревом. В прочем это различие
существенно только для физических событий, таких как события мыши. Более
абстрактные события могут и не иметь прямой связи с элементом визуального дерева
(например, событие Click вследствие поддержки клавиатуры), и тогда в качестве Source
и OriginalSource выступает один и тот же объект.

Маршрутизируемые события в действии
В классе UIElement определено много маршрутизируемых событий для клавиатуры,
мыши, мультисенсорных устройств и стилуса. Большая часть из них всплывающие, но
для многих есть и парные туннелируемые. Tyннелируемые события легко распознать,
потому что по принятому соглашению их имена начинаются со слова Preview. Такое
событие - также по соглашению генерируется непосредственно перед парным ему
всплывающим. Например, туннелируемое событие PreviewHouseMove генерируется
перед всплывающем событием MouseMove.
Идея, стоящая за такими парами событий, заключается в том, чтобы дать элементам
возможность отменить или иным способом модифицировать событие которое еще
только произойдет. По соглашению встроенные в WPF элементы предпринимают

Маршрутизируемые события

197

действия только в ответ на всплывающее событие (в случае если определена пара
событий - всплывающее и туннелируемое), гарантируя тем самым, что туннелируемые
события отвечают своему названию (preview означает «предварительный просмотр»).
Представим, к примеру, что требуется реализовать элемент TextBox, который
позволяет вводить только строки, отвечающие некоторому образцу или регулярному
выражению (например, номера телефонов и почтовые индексы). Если обрабатывать в
нем событие KeyDown, то лучшее, что можно сделать, — удалить текст, который уже
отображен в поле TextBox. Если же обрабатывать событие PreviewKeyDown, то можно
пометить его как «обработанное» и тем самым не только прервать туннелирование, но
и воспрепятствовать генерации всплывающего события KeyDown. В таком случае
TextBox вообще не получит уведомления о событии KeyDown и введенный символ не
появится в поле.
Чтобы продемонстрировать работу со всплывающими событиями, в листинге 6.2
приведена модификация диалогового окна About из главы 3 - к окну Window
присоединен обработчик события MouseRightButtonDown. В листинге 6.3 показана
реализация этого обработчика в застраничном коде на С#.
Листинг 6.2. Диалоговое окно About с обработчиком события в корневом элементе






Chapter 1
Chapter 2





You have successfully registered this product.



Листинг 6.3. Застраничный код для разметки в листинге 6.2
using
using
using
using

System.Windows;
System.Windows.Input;
System.Windows.Media;
System.Windows.Controls;

198

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

publicpartialclassAboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void AboutDialog_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// Ввести информацию о событии
this.Title = ‚Source = ‚ + e.Source.GetType().Name + ‚, OriginalSource = ‚ +
e.OriginalSource.GetType().Name + ‚ @ ‚ + e.Timestamp;
// В этом примере все возможные источники наследуют Control
Control source = e.Source as Control;
// Показать или скрыть расмки вокруг елмента-источника
if (source.BorderThickness != new Thickness(5))
{
source.BorderThickness = newThickness(5);
source.BorderBrush = Brushes.Black;
}
else
source.BorderThickness = newThickness(0);
}
}

Когда событие MouseRightButtonDown всплывает до элемента Window, его обработчик
выполняет два действия: выводит информацию о событии в строке заголовка окна и
рисует (а впоследствии стирает) толстую черную рамку вокруг элемента логического
дерева, над которым произошел щелчок правой кнопкой мыши. Результат изображен на
рис. 6.1. Отметим, что при щелчке по метке Label свойство Source предоставляет
ссылку на эту метку, a OriginalSource - ссылку на ее визуальный дочерний элемент
TextBlock.

Рис 6.1. Модифицированное окно About после щелчка правой кнопкой мыши по первой
метке

Маршрутизируемые события
199
Если запустить эту программу и последовательно щелкнуть правой кнопкой мыши по
всем элементам, то обнаружатся два любопытных эффекта:


Window не получает событие MouseRightButtonDown, если щелкнуть по любому
элементу списка ListBoxItem, Дело в том, что ListBoxItem сам обрабатывает это
событие, равно как и MouseLeftButtonDown (и прерывает всплытие), - это нужно
ему для реализации выбора элементов.



Window получает событие MouseRightButtonDown при щелчке по кнопке Button,
но никаких изменений во внешнем виде рамки не происходит. Это объясняется
структурой стандартного визуального дерева Button, которая была показана на
рис. 3.3. В отличие от элементов Window, Label, ListBox, ListBoxItem и
StatusBar, в визуальном дереве Button нет элемента Border.

КОПНЕМ ГЛУБЖЕ
Прерывание маршрутизации события - иллюзия!
Хотя присваивание значения true свойству Handled объекта RoutedEventArgs в обработчике маршрутизируемого события должно приводить к прерыванию туннелирования или всплытия, обработчики, присоединенные к элементам, находящимся выше или ниже в дереве, все равно могут запросить получение событий!
Сделать это можно только в процедурном коде с помощью перегруженного варианта
метода AddHandler, который принимает дополнительный булевский параметр
handledEventsToo.
Например, в листинге 6.2 можно удалить атрибут события и заменить его таким
обращением к AddHandler в конструкторе AboutDialog:
public AboutDialog()
{
InitializeComponent();
this. AddHandler(Window.MouseRightButtonDownEvent,
new MouseButtonEventHandle г (AboutDialog_MouseRightButtonDown), true);
}

Поскольку в третьем параметре передано значение true, то обработчик AboutDialog_MouseRightButtonDown получает событие щелчка правой кнопкой мыши по
ListBoxItem и рисует черную рамку!
Однако лучше не прибегать к этому приему, потому что для пометки события как
обработанного, очевидно, была какая-то причина. Более правильно было бы,
присоединить обработчик Preview-версии того же события.
Но в целом мы хотели подчеркнуть, что прерывание туннелирования и всплытия - на
самом деле иллюзия. Распространение события все равно продолжается, но по
умолчанию обработчики видят только те события, которые не помечены как уже
обработанные.

200

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Присоединенные события
Туннелирование и всплытие некоторого маршрутизируемого событиипроисходит
естественно, когда это событие определено в каждом элементе n дерева. Но WPF
поддерживает туннелирование и всплытие маршрутизируемсобытия даже для
элементов, в которых данное событие не определено! Вможно это благодаря понятию
присоединенного события.
Присоединенные события работают примерно так же, как присоединенные свойства (а
их использование в механизме туннелирования и всплытия напоминает использование
присоединенных свойств совместно с механизмом наследования значений свойств). В
листинге 6.4 мы снова изменили окно About добавив прямо в корень окна Window
обработку всплывающего события SelectionChanged, генерируемого списком ListBox, и
всплывающего события Click генерируемого обеими кнопками Button. Поскольку в
классе Window не определены события SelectionChanged и Click, то имена атрибутов
событий необходимо снабдить префиксами, содержащими имя класса, в котором
соответствующие событие определено. В листинге 6.5 представлен застраничный код
содержащий определения обоих обработчиков событий. Обработка сводится к выводу в
окне MessageBox информации о том, что произошло.
Листинг 6.4. Диалоговое окно About с двумя обработчиками присоединен событий в
корневом окне






Chapter 1
Chapter 2





You have successfully registered this product.



Маршрутизируемые события

201

Листинг $.5. Застраничный код для разметки в листинге 6.4.
using System.Windows;
using System.Windows.Controls;
publicpartialclassAboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
MessageBox.Show(‚You just selected ‚ + e.AddedItems[0]);
}
void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(‚You just clicked ‚ + e.Source);
}
}

Любое маршрутизируемое событие можно использовать как присоединенное.
Синтаксис присоединенных событий, показанный в листинге 6.4, допустим потому, что
компилятор XAML видит событие .NETSelectionChanged, определенное в классе
ListBox, и событие .NETClick, определенное в классе Button. Однако во время
выполнения вызывается метод AddHandler, который присоединяет оба события к
элементу Window. Поэтому эти два атрибута события эквивалентны следующему коду
в конструкторе Window:
public AboutDialog()
{
InitializeComponent();
this.AddHandler(ListBox.SelectionChangedEvent,
new SelectionChangedEventHandler(ListBox_SelectionChanged));
this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Click));
}

КОПНЕМ ГЛУБЖЕ
Консолидация обработчиков маршрутизируемых событий
Поскольку вместе с маршрутизируемым событием передается достаточно много
информации, при желании есть возможность обработать все туннелируемые и
всплывающие события в одном «мегаобработчике» на верхнем уровне. Он мог бы
исследовать объект RoutedEvent, определить, какое событие сгенерировано, привести
параметр RoutedEventArgs к типу соответствующего подкласса (например,
KeyEventArgs, MouseButtonEventArgs и т.д.), а потом предпринять соответствующие
действия.

202

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Например, код в листинге 6.5 можно было бы переписать, поместив обработчики
событий ListBox.SelectionGаnged и Button.Click в один метод GenerikHandler)
void GenericHandler(object sender, RoutedEventArgs e)
{
if (e.RoutedEvent == Button.ClickEvent)
{
MessageBox.Show(‚You just clicked ‚ + e.Source);
}
else if (e.RoutedEvent == ListBox.SelectionChangedEvent)
{
SelectionChangedEventArgs sce = (SelectionChangedEventArgs)e;
if (sce.AddedItems.Count > 0)
MessageBox.Show(‚You just selected ‚ + sce.AddedItems[0]);
}
}

Это возможно благодаря встроенному в каркас .NETFramework механизму
контравариантности делегатов, позволяющему использовать делегат с методом, в
сигнатуре которого указан базовый класс ожидаемого параметра (например,
RoutedEventArgs вместо SelectionChangedEventArgs). Метод GenericHandler просто
приводит параметр RoutedEventArgs к нужному типу, когда ему необходимо
получить дополнительную информацию, специфичную для события SelectionGanged.

События клавиатуры
Основные события клавиатуры, поддерживаемые всеми подкласса: UIEment, - это
всплывающие события KeyDown и KeyUp и парные им туннилируемые события
PreviewKeyDown и PreviewKeyUp. Обработчикам событий клавиатуры передается
аргумент типа KeyEventArgs, содержащий целый ряд свойств, в том числе:


Key, ImeProcessedKey, DeadCharProcessedKey, SystemKey - четыре свойства
принадлежащие типу перечисления Key, в котором определены все возможные
клавиши. Свойство Key определяет, какая клавиша вызвала генерацию события.
Если клавиша обрабатывается или будет обрабатываться редактором метода
ввода (InputMethodEditor - IME), то можно проверить значение свойства
ImeProcessedKey. Если клавиша является слепой в последовательности, то
свойство Key будет равно DeadCharProcessed, тогда как реальную клавишу
можно получить из свойства DeadCharProcessedKey. Если системная клавиша,
например Alt, то Key будет равно System, а сама клавиша берется из свойства
SystemKey



IsUp, IsDown, IsToggled - булевские свойства, сообщающие дополнительную
информацию о событии клавиатуры, хотя в некоторых случаях она точна. (Раз
уж вы обрабатываете событие KeyDown, то точно знаете, чтоклавиша нажата!)
Свойство IsToggled относится к клавишам с фиксируемым переключением
состояния, таким как CapsLock и ScrollLock.

События клавиатуры

203

СОВЕТ
Для получения информации о состоянии клавиатуры в любой момент времени, а не
только внутри обработчика события от нее можно воспользоваться статическим
классом System.Windows.Input и его свойством PrimaryDevice (типа KeyboardDevice).


KeyStates - свойство типа KeyStates, битового перечисления, состоящего из
произвольной комбинации битов None, Downи Toggled. Эти значения отображаются на свойства IsUp, IsDown и IsToggled соответственно. Поскольку Toggled
иногда комбинируется с Down, остерегайтесь определять значение KeyStates с
помощью простой проверки на равенство. Лучше всего пользоваться свойствами
IsXXX.



IsRepeat - булевское свойство, равное true, когда нажатие клавиши повторяется.
Например, так происходит, когда вы удерживаете нажатой пробельную клавишу и
получаете лавину событий KeyDown. Свойство IsRepeat будет содержать true для
всех событий KeyDown, кроме самого первого.



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

Одна из причин для обращения к классу KeyboardDevice - получение его свойства
Modifiers типа ModifierKeys(еще одно перечисление). Оно показывает, какие клавиши
нажаты одновременно с основной. Возможны следующие значения: None, Alt, Control,
Shift и Windows. Это битовое перечисление, поэтому не следует проводить проверку на
равенство, если только вы не заинтересованы в точной комбинации модификаторов.
Например, в следующем коде проверяется, что нажаты клавиши Alt и А, но при этом не
исключается нажатие комбинаций ALt+Shift+A, Alt+Ctrl+Aи т. д.:
protectedoverridevoid OnKeyDown(KeyEventArgs e)
{
if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt
&& (e.Key == Key.A || e.SystemKey == Key.A))
{
// Нажато сочетания Alt+A, возможно с Ctrl, Shift, или Windows
}
base.OnKeyDown(e);
}
On the other hand, the following code checks for Alt+A and nothing else:
protectedoverridevoid OnKeyDown(KeyEventArgs e)
{
if (e.KeyboardDevice.Modifiers == ModifierKeys.Alt
&& (e.Key == Key.A || e.SystemKey == Key.A))
{

204

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

// Нажато Alt+A и только Alt+A
}
base.OnKeyDown(e);
}

FAQ
Как узнать, какая из клавиш Alt, Ctrl и Shift нажата: левая или правая?
В перечислении Key имеются следующие значения: LeftAlt и RightAlt, LeftCtrl и
RightCtrl, LeftShift и RightShift. Но поскольку клавиша Alt обычно считается
системной, то она может определяться как System, маскируя тем самым, какая из
двух клавиш Alt в действительности нажата. К счастью, можно воспользоваться
методом IsKeyDown класса KeyboardDevice(или IsKeyUp либо IsKeyToggled, чтобы
узнать о состоянии конкретной клавиши, например левой или правой Alt+В
следующем коде проверяется нажатие комбинации [левая]Аlt+А:
protectedoverridevoid OnKeyDown(KeyEventArgs e)
{
if (e.KeyboardDevice.Modifiers == ModifierKeys.Alt
&& (e.Key == Key.A || e.SystemKey == Key.A)
&& e.KeyboardDevice.IsKeyDown(Key.LeftAlt))
{
Continued
// Нажато LeftAlt+A
}
base.OnKeyDown(e);
}

Иногда в этих событиях можно запутаться, но настоящие трудности при работе с
клавиатурой обычно возникают, когда речь заходит о фокусе ввода (Проблема еще
больше осложняется при взаимодействии с технологиями, отличными от WPF, но это
уже тема главы 19 «Интероперабельность с другими технологиями».) Элемент
UIElement получает события клавиатуры, только если владеет фокусом. Указать, может
ли некоторый элемент получать фокус, позволяет булевское свойство Focusable, по
умолчанию равное true. При изменении значения этого свойства возникает событие
FocusableChanged.
В классе UIElement определено еще много свойств и событий, относящихся к фокусу
клавиатуры. Отметим из них свойство IsKeyboardFocused, которое сообщает,
принадлежит ли фокус текущему элементу, и свойство IsKeyboardFоcusWithin,
сообщающее то же самое, но в отношении не только текущего элемента, но и его
потомков. (Эти свойства доступны только для чтения; чтобы передать фокус
клавиатуры, пользуйтесь методами Focus или MoveFocus.) Об изменении этих свойств
уведомляют события IsKeyboardFocusedChanged, IsKeyboardFocusWithinChanged,
GotKeyboardFocus,
LostKeyboardFocus,
PreviewQotKeyboardFocus
и
PreviewLostKeyboardFocus.

События мыши

205

События мыши
Все подклассы UIElement поддерживают следующие основные события мыши:


MouseEnter и MouseLeave



MouseMove и PreviewMouseMove



MouseLeftButtonDown,
MouseRightButtonDown,
MouseLeftButtonUp,
MouseRightButtonUp и более общие MouseDown и MouseUp, атакже Previewверсии всех шести событий



MouseWheel и PreviewMouseWheel

События MouseEnter и MouseLeave можно использовать для создания эффекта
ролловера, хотя более предпочтительно использовать триггер со свойством
IsMouseOver.
В подклассах UIElement имеется также свойство IsMouseDirectlyOver (и соответствующее ему событие IsMouseDirectlyOverChanged), которое позволяет исключить
дочерние элементы. Оно используется в тех редких случаях, когда вы точно знаете, с
каким визуальным деревом работаете.

FAQ
А где же событие для обработки нажатия средней кнопки мыши?
Эту информацию можно получить с помощью обобщенных событий MouseDown и
MouseUp (или их Preview-версий). Объект EventArgs, передаваемый их обработчиком, содержит свойства, показывающие, какая их следующих кнопок была нажата
или отпущена: LeftButton, RightButton, MiddleButton, XButtonl или ХВottоn2.
СОВЕТ
Если вы не хотите, чтобы элемент генерировал события мыши (или блокировал
события мыши, генерируемые лежащими под ним элементами), то можете присвоить
значение false его свойству IsHitTestVisible.
ПРЕДУПРЕЖДЕНИЕ
Прозрачные области генерируют события мыши, но null-области - нет!
Хотя и можно рассчитывать на то, что установка для свойства IsHitTestVisible
значения false подавит события мыши, но условия, при которых эти события вообще
генерируются, довольно запутанны. Если свойство Visibility элемента равно
Collapsed, то события мыши подавляются, но установка для свойства Opacity
значения 0 не влияет на генерацию событий. Еще одна тонкость касается областей,
для которых любое из свойств Background, Fill или Stroke равно null.

206

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

В таких областях события мыши не генерируются. Однако, если явно присвоить
любому из свойств Background, Fill или Stroke значение Transparent (или любой
другой цвет), то в такой области события мыши будут генерироваться, (null-кисть
внешне неотличима от прозрачной (Transparent) кисти, но с точки зрения проверки
положения указателя мыши ведет себя иначе.)

Класс MouseEventArgs
Обработчикам
всех
выше
упомянутых
событий
мыши
(кроме
IsMouseDirectlyOverChanged) передается объект класса MouseEventArgs. В нем есть
пять свойств типа MouseButtonState, содержащих информацию обо всех потенциально
возможных нажатиях кнопок мыши: LeftButton, RightButton, MiddleButton, XButtonl и
XButton2.MouseButtonState- это перечисление с двумя значениями: Ргеsed и Released. В
классе MouseEventArgs определен также метод GetPosition, который возвращает
структуру Point со свойствами X и Y, отражающими точные координаты указателя
мыши.
GetPosition - это метод, а не просто свойство, поскольку он позволяет получить
позицию указателя мыши несколькими способами: относительно левого верхнего угла
экрана или левого верхнего угла произвольного нарисованного элемента UIElement.
Чтобы узнать координаты относительно экрана, передайте в качестве единственного
параметра null. А для получения координат относительно элемента передайте в
качестве параметра интересующий вас элемент.
Обработчикам событий MouseWheel и PreviewMouseWheel передается объект класса
MouseWheelEventArgs, производного от MouseEventArgs. Этот класс добавляет
целочисленное свойство Delta, показывающее, на какой угол колесико мыши
повернулось с момента последнего события. Обработчикам всех 12 событий семейства
MouseUp/MouseDown передается объект класса MouseButtonEventArgs, еще одного
подкласса MouseEventArgs. Этот класс добавляет свойство ChangedButton, - которое
сообщает, какая кнопка изменила состояние (значение принадлежит перечислению
MouseButton); свойство ButtonState, которое информирует, нажата кнопка или
отпущена; и свойство ClickCount.
Свойство ClickCount показывает, сколько раз подряд была нажата кнопка мыши,
причем ведется подсчет нажатий, промежуток времени между которыми не превышает
системного параметра, описывающего скорость выполнения двойного щелчка (задается
на Панели управления). Класс Button генерирует событие Click, обрабатывая
низкоуровневое событие MouseLeftButtonDown, а его базовый класс Control генерирует
событие МоuseDoubleClick, сравнивая значение ClickCount с 2 в обработчике
МоuseDoubleClick, и событие РreviewMouseDoubleClick, делая то же самое в
обработчике PrevlewMouseLeftButtonDown. Имея такую поддержку, вы легко сможете
реагировать и на другие действии пользователя, например на тройное нажатие, двойное
нажатие средней и т. д.

События мыши

207

ПРЕДУПРЕЖДЕНИЕ
Панель Canvas генерирует свои собственные события мыши только в области,
определяемой ее свойствами Width и Height!
Легко забыть о том, что по умолчанию ширина Width и высота Height панели Canvas
равны 0, так как ее дочерние элементы рисуются вне границ холста. Но события
мыши самой панели Canvas(кроме событий, всплывающих от потомков)
генерируются только в области, занятой прямоугольником размером WidthxHeight(и
только при условии, что свойство Background не равно null). Поэтому по умолчанию
события мыши уровня Canvas генерируются только ее дочерними элементами.

Перетаскивание
Во всех подклассах UIElement определены события для работы с перетаскиванием:


DragEnter, DragOver, DragLeave, а так же PreviewDragEnter, PreviewDragOver и
PreviewDragLeave



Drop и PreviewDrop



QueryContinueDrag и PreviewQueryContinueDrag

Это перетаскивание элемента в буфер обмена и бросание содержимого буфера на
элемент в стиле Win32, а не перетаскивание и бросание самих элементов. Элемент
может принять участие в перетаскивании, установив значение true для свойства
AllowOrop.
Обработчикам событий из первых двух наборов передается объект типа DragEventArgs,
содержащий следующие свойства и методы:


GetPosition - такой же метод, как в классе MouseEventArgs



Data - свойство типа IDataObject, представляющее
илибросаемый объект буфера обмена Win32



Effects и AllowedEffects- битовое перечисление DragDropEffects, допускающее
произвольную комбинацию флагов Copy, Move. Link, Scroll, All и None



KeyStates - еще одно битовое перечисление (DragDropKeyStates), показывающее, какие кнопки мыши или клавиши-модификаторы были нажаты вовремя
перетаскивания
или
бросания:
LeftMouseButton,
RightMouseButton,
MiddleMouseButton, ShiftKey, ControlKey, AltKeyили None

перетаскиваемый

События QueryContinueDrag и PreviewQueryContinueDrag генерируются, если во время
перетаскивания изменяется состояние клавиатуры или какой-нибудь кнопки мыши. Это
позволяет обработчику без труда отменить всю операцию.
Обработчикам этих событий передается объект класса QueryContinueDragEventArgs,
имеющий следующие свойства:


KeyStates - аналогично одноименному свойству класса DragEventArgs

208

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства


EscapePressed- отдельное булевское свойство, показывающее, была ли нажата
клавишаEsc



Action- свойство, которое обработчик может установить, чтобы определить
судьбу операции перетаскивания; принадлежит перечислению DragAction и
принимает значение Continue, Drop или Cancel.

СОВЕТ
Для получения информации о состоянии мыши почти в любой момент времени, а не
только внутри обработчиков ее событий можно воспользоваться статическим
классом System.Windows.Input.Mouse. Чего нельзя сделать - так это получить
достоверную позицию указателя мыши от статического метода Mouse.GetPosition во
время перетаскивания. Вместо этого приходится либо вызывать метод Gsition
объекта OragEventArgs, переданного обработчику события, либо, минуя обработчики
событий, с помощью технологии PInvoke вызвать функцию Win32 API GetCursorPos,
которая даст правильные координаты.

Захват мыши
Предположим, что нужно поддержать перетаскивание и бросание самих элементов
UIElement, а не объектов буфера обмена. Легко представить себе, как это можно
реализовать с помощью событий MouseLeftButtonDown, MouseMove и MouseLeftButtonUp. В начале операции перетаскивания можно установить некую булевскую
переменную в обработчике события MouseLeftButtonDown элемента, потом в
обработчике MouseMove перемещать элемент, так чтобы он оставался под указателем
мыши, если эта переменная равна true, а в обработчике MouseLeftButtonUp сбросить
переменную, обозначив конец перетаскивания. Однако выясняется, что эта схема не так
уж хороша, потому что пользователь может двигать мышь слишком быстро или ее
указатель может оказаться поддругаяэлементом, в результате чего указатель потеряет
связь с элементом, который вы пытаетесь перетащить.
К счастью, WPF позволяет любому элементу UIElement в любой момент захватить
или освободить мышь. Когда элемент захватил мышь, он получает все события мыши,
даже если указатель оказывается вне занимаемой им области. После освобождения
мыши поведение событий возвращается в нормальное русло. Для захвата и
освобождения мыши предназначены два метода класса UIElement: CaptureMouse и
ReleaseMouseCapture. (И, разумеется, есть ряд свойств и событий, сообщающих о
состоянии захвата мыши, точнее, свойства IsMouseCaptured и IsMouseCaptureWithin и
события
GotMouseCapture,
LostMouseCapture,
IsMouseCaptureChanged
и
IsMouseCaptureWithinChanged.)
Поэтому для реализации перетаскивания необходимо захватить мышь в обработчике
MouseLeftButtonDown и освободить ее в обработчике MouseLeftButtonUp.
Единственная сложность - придумать оптимальный способ фактического перемещения

События стилуса

209

элемента в обработчике MouseMove. Выбор зависит от компоновки приложения, но,
скорее всего, вы будете применять к перетаскиваемому элементу преобразование в
режиме RenderTransform или LayoutTransform.

События стилуса
В WPF имеется специальная поддержка для цифрового пера, или стилуса,
применяемого в таких устройствах, как TabletPC. (Иногда ее называют поддержкой
рукописного ввода.) Если приложение специально не поддерживает стилус, то он
интерпретируется как обычная мышь и генерирует все относящиеся к мыши события, в
частности MouseDown, MouseMove и MouseUp. Такое поведение позволяет
использовать стилус в программах, которые не были разработаны специально для
TabletPC.
Но если вы хотите, чтобы пользователю было удобно работать именно со стилусом, то
можете
организовать
взаимодействие
с
экземпляром
класса
System.Windows.Input.StylusDevice. Получить его можно тремя способами:


Воспользоваться свойством StylusDevice объекта MouseEventArgs для доступа к
объекту в обработчиках событий мыши. (Если стилуса нет, то это свойство
равно null.)



Воспользоваться статическим классом System. Windows. Input.Stylus и его свойством CurrentStylusDevice - так можно получить доступ к стилусу в любой
момент. (Если стилуса нет, то это свойство тоже равно null.)



Обрабатывать события, специфичные для стилуса.

Все эти средства применимы не только к перьевым, но и к сенсорным дигитайзерам.

FAQ
Я и так могу получить данные стилуса, если буду работать с ним, как с мышью.
Так зачем еще какая-то дополнительная информация?
Перьевой и сенсорный дигитайзеры поддерживают два аспекта, отсутствующие у
обычной мыши (сейчас мы не говорим о мультисенсорном вводе, это тема следующего раздела): чувствительность к силе нажатия и более высокую разрешающую
способность.
Для приложения с рукописным вводом или предназначенного для рисования и то и
другое может сделать процесс ввода данных более естественным, чем с помощью
мыши. Стилус также позволяет выполнять недоступные для мыши действия, что
обеспечивает набор свойств и событий, обсуждаемых ниже в этом разделе. Кроме
того, поскольку система может одновременно распознавать несколько стилусов,
открывается возможность писать код, ориентированный на мультисенсорный ввод, при работе в Windows7 с установленным пакетом WPF3.5 SP1.

210

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Класс StylusDevice
Класс StylusDevice содержит ряд свойств, в том числе:


Inverted - булевское значение, показывающее, что стилус используется как
ластик (то есть экрана касается его обратный конец).



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



StylusButtons - коллекция объектов типа StylusButton. В отличие от мыши у
стилуса нет фиксированного списка кнопок. В каждом объекте StylusButton
имеется строковое свойство Name и идентификатор Guid, а также свойство
StylusButtonState, принимающее одно из значений: Up или Down.



TabletDevice - свойство типа System.Windows.Input.TabletDevice, предоставляющее детальную информацию о текущем оборудовании и возможностях стилуса
(в частности, чувствительность к силе нажатия или поддержка перемещений без
касания). Свойство Туре равно Stylus для перьевого и Touch- для сенсорного
дигитайзера.

В классе StylusDevice имеется метод GetPosition, работающий так же, как его аналог
для мыши. Но дополнительно есть более подробный метод GetStylusPoints, который
возвращает коллекцию объектов StylusPoint. В каждом объекте StylusPoint имеются
следующие свойства:
 X - абсцисса точки касания стилуса относительно элемента, на котором он
находится.
 Y - ордината точки касания стилуса относительно элемента, на котором он
находится.
 PressureFactor - значение от 0 до 1, показывающее давление, приложенное к
стилусу в момент регистрации точки. Чем больше значение, тем сильнее
нажатие (если оборудование вообще поддерживает чувствительность к силе
нажатия). Если чувствительность к силе нажатия не поддерживается, то
PressureFactor будет равно 0.5.
Метод GetStylusPoints возвращает именно коллекцию точек (и уровней давления), а не
одно значение, из-за высокой разрешающей способности стилус. Это означает, что
между двумя событиями MouseMove может быть обнаружено и зарегистрировано
много отдельных перемещений.

События
К стилусу относятся следующие события:
 StylusEnter и StylusLeave
 StylusMove и PreviewStylusMove
 StylusInAirMove и PreviewStylusInAirMove
 StylusDown, StylusUp, PreviewStylusDowri и PreviowStyiusUp

Мультисенсорные события

211



StylusButtonDown,
PreviewStylusButtonlip



StylusSystemGesture и PreviewStylusSystemGesture



StylusInRange,StylusOutOfRange, PreviewStylusInRangenPreviewStylusOutOfRange



GotStylusCapture и LostStylusCapture

StylusButtonUp,

PreviewStylusButtonDown

и

Обработчикам этих событий передается объект класса StylusEventArgs, свойство
StylusDevice которого дает доступ к объекту StylusDevice. Для удобства в нем
определены также члены InAir, Inverted, GetPosition и GetStylusPoints, обертывающие
одноименные члены класса StylusDevice.
Некоторым обработчикам в качестве аргумента передается объект одного из
подклассов StylusEventArgs:


StylusDownEventArgs передается обработчикам событий StylusDown и
Preview-StylusDown; добавляет целочисленное свойство TapCount, аналогичное
свойству ClickCount в событиях мыши.



StylusButtonEventArgs- передается обработчикам событий StylusButtonDown,
StylusButtonUp и их Preview-версий; добавляет свойство StylusButton, описывающее нажатую кнопку.



StylusSystemGestureEventArgs- передается обработчикам событий StylusSystemGestureи PreviewStylusSystemGesture; добавляет свойство SystemGesture,
принадлежащее типу перечисления SystemGesture и принимающее следующие
значения: Тар, RightTap, TwoFingerTap, Drag, RightDrag, Flick, HoldEnter,
HoldLeave, HoverEnter, HoverLeave, None.

СОВЕТ
В WPF определен объект Stroke (росчерк), с помощью которого можно визуально
представить информацию, хранящуюся в коллекции StylusPoints, и элемент
InkPresenter, содержащий коллекцию объектов Stroke. Во многих сценариях рисования и рукописного ввода можно также использовать элемент InkCanvas, описанный в главе 11 «Изображения, текст и другие элементы управления», который
основав на использовании InkPresenter. В InkCanvas встроена возможность работы со
стилусом, если таковой имеется, а также средства для сбора и отображения
росчерков. При использовании этого элемента вам вообще не придется обрабатывать
события стилуса самостоятельно!

Мультисенсорные события
При работе в ОС Windows 7 или более поздней, оснащенной оборудованием для
мультисенсорного ввода, можно воспользоваться новыми событиями, добавленными в
WPF4. Их можно разбить на две категории: простые события касания и события
манипулирования более высокого уровня.
Хотя такие мультисенсорные события, как события стилуса, можно представить в виде
событий мыши, обратное неверно. Нельзя получить от мыши событие касания

212

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

в одной точке» как если бы речь шла о касании экрана пальцем, не проделав
дополнительную работу по эмуляции сенсорного устройства.
СОВЕТ
Если вы хотите эмулировать мультисенсорный (или даже простой сенсорный) ввод
на «обычном» компьютере, то можете воспользоваться комплектом MultiPointMouseSDK(http://micro8oft.com/multipolnt/mouse-8dk),
который
позволяет
одновременно подключить к компьютеру до 25 мышей! Однако этого недостаточно.
Еще предстоит раскрыть функциональность Multipoint в виде специализированного
сенсорного устройства; соответствующая техника описана в статье по адресу
http://blogs.msdn.com/ansont/archive/2010/0l/30/customtouchdeuices.aspx.

Простые события касания
Простые события касания во многом похожи на события мыши:


TouchEnter и TouchLeave



TouchMove и PreviewTouchMove



TouchDown, TouchUp, PreviewTouchDown и PreviewTouchUp



GotTouchCapture и LostTouchCapture

Когда экрана касаются несколько пальцев одновременно, генерируются независимые
события для каждого пальца. Кроме того, благодаря описанной выше поддержке
стилуса для первого пальца генерируются также события мыши.
Обработчикам событий касания передается объект класса TouchEventArgs, держащий
следующие члены:


GetTouchPoint - метод, возвращающий объект TouchPoint. Этот объект
представляет точку касания в системе координат, связанной с элементом, которому она принадлежит. Аналог метода GetPosition для событий мыши.



GetlntermediateTouchPoints - метод, возвращающий коллекцию объектов
TouchPoint в координатах элемента, собранных за время, прошедшее между
текущим и предыдущим событиями касания. Аналог метода GetStylusPoints для
событий стилуса.



TouchDevice - свойство, возвращающее объект TouchDevice,

В классе TouchPoint имеется не только свойство Position, но и Size, показывающее,
какая часть пальца находится в контакте с экраном, а также cвойство Bounds, точно
описывающее область контакта. Кроме того, он дает информацию, которая уже
известна в контексте обработчика события, но может оказаться полезной в других
контекстах, - устройство TouchDevice и совершенное действие Action, которое может
принимать следующие значения: Down, Move, Up (из перечисления TouchAction).
С каждым нажатием пальцем ассоциирован отдельный объект TouchDevice
идентифицируемый целочисленным свойством Id. Этот идентификатор (или сам

Мультисенсорныесобытия

213

объект TouchDevice) можно использовать для отслеживания пальцев во время
обработки событий.
В листинге 6.6 события TouchDown, TouchMove и TouchUp используются для создания
картинок с изображением пальцев (но не самих отпечатков пальцев!) в местах их
соприкосновения с экраном. Это застраничный код для следующего простого окна,
содержащего элемент Canvas с именем canvas:











Результат показан на рис. 6.2.

Рис. 6.1, При касании экрана пятью пальцами мы видим пять картинок с
изображением пальца в местах касания.
Листинг 6.6. MuinWindoiv.xuml.cs- обработка событий TouehDown. TouchMove
иTouchUp
using System;
using System.Collections.Generic;
usingSystem.Windows;

214

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace TouchEvents
{
publicpartialclassMainWindow : Window
{
// Сопоставление изображения с обьектами TochDevice
Dictionary fingerprints =
new Dictionary();
public MainWindow()
{
InitializeComponent();
}
protectedoverridevoid OnTouchDown(TouchEventArgs e)
{
base.OnTouchDown(e);
// Захватываем данное сенсорное устройство
canvas.CaptureTouch(e.TouchDevice);
// Создаем новое изображение для этого казания
Image fingerprint = newImage{ Source = newBitmapImage(
newUri(‚pack://application:,,,/fingerprint.png‛)) };
// Перемещаем изображение в точку казания
TouchPoint point = e.GetTouchPoint(canvas);
fingerprint.RenderTransform = newTranslateTransform(
point.Position.X, point.Position.Y);
// Запоминаем изображение и помещаем его на лист
fingerprints[e.TouchDevice] = fingerprint;
canvas.Children.Add(fingerprint);
}
protectedoverridevoid OnTouchMove(TouchEventArgs e)
{
base.OnTouchMove(e);
if (e.TouchDevice.Captured == canvas)
{
// Находим нужное изображение
Image fingerprint = fingerprints[e.TouchDevice];
TranslateTransform transform =
fingerprint.RenderTransformasTranslateTransform;
// Перемещаем его в новое место
TouchPoint point = e.GetTouchPoint(canvas);
transform.X = point.Position.X;

Мультисенсорные события

215

transform.Y = point.Position.Y;
}
}
protectedoverridevoid OnTouchUp(TouchEventArgs e)
{
base.OnTouchUp(e);
// Освобождаем захваченное устройство
canvas.ReleaseTouchCapture(e.TouchDevice);
// Удаляем изображение с холста и из словаря
canvas.Children.Remove(fingerprints[e.TouchDevice]);
fingerprints.Remove(e.TouchDevice);
}
}
}

Эта программа работает по принципу схемы перетаскивания и бросания элементов,
описанной в разделе «События мыши», только элемент создается по событию
TouchDown, а удаляется по событию TouchUp. И мы решили не присоединять
обработчики всех трех событий, а переопределить соответствующие методы ОпХХХ
класса Window.
В методе OnTouchDown программа захватывает сенсорное устройство, чтобы операция
перетаскивания работала надежно. Но, в отличие от клавиатуры, мыши и стилуса, один
элемент может захватить сразу несколько сенсорных устройств. В данном случае холст
Canvas захватывает все такие устройства. Изображение Image создается из внедренного
ресурса с помощью синтаксиса, который мы рассмотрим в главе 12 «Ресурсы»,
позиционируется посредством преобразования TranslateTranform, после чего
помещается на холст и добавляется в словарь, к которому обращаются и другие
обработчики. Ключом словаря является сам объект TouchDevice.
Метод OnTouchMove находит изображение, соответствующее текущему объекту
TouchDevice, и перемещает его в текущую точку TouchPoint. Он проверяет, что
событие принадлежит одному из устройств TouchDevice, захваченных холстом. Метод
OnTouchUp освобождает захваченные сенсорные устройства, после чего удаляет объект
Image с холста и из словаря.
СОВЕТ
В версии Silverlight4 событий касания нет. Если вы хотите написать код, который
поддерживал бы мультисенсорный ввод и работал как в WPF, так и в Silverlight, то
можете воспользоваться низкоуровневым событием FrameReported, которое
присутствует в обеих системах. Событие FrameReported определено в статическом
классе System.Windows.InputTouch и сообщает о точках касания TouchPoint для всего
приложения в целом. Это не маршрутизируемое событие; выяснять, где произошло
касание, придется самостоятельно.

216

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

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

События манипулирования,
масштабирование

описывающие

сдвиг,

поворот

и

Мультисенсорный ввод обычно применяется пользователями для сдвига, поворота и
масштабирования элементов. Тут все просто, так как эти действия точно отображаются
на
преобразования
TranslateTransform,
RotateTransform
и
ScaleTransform
соответственно. А вот определить, когда эти преобразования следует применять и с
какими параметрами, куда сложнее.
Скользящее движение одного пальца, обычно обозначающее сдвиг, распознать
относительно просто, но определить, что пользователь произвел вращательное
движение двумя пальцами или жест, обозначающий масштабирование с помощью
описанных выше событий очень трудно. К тому же несогласованность алгоритмов,
применяемых разными разработчиками для распознавания жестов, только раздражала
бы пользователей.
На наше счастье, WPF предоставляет высокоуровневые события манипулирования,
позволяющие без труда поддержать сдвиг, поворот и масштабирование. Вот перечень
основных событий такого рода:


ManipulationStarting и ManipulationStarted



ManipulationDelta



ManipulationCompleted

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

Использование событий манипулирования
Когда для первого пальца генерируется событие TouchOown, возникает событие
ManipulationStarting, а затем ManipulationStarted. Для каждого события TouchMove
генерируется событие ManipulationDelta, а после того, как все пальцы подняты, событие ManipulationCompleted. События ManipulationStarting и ManipulationStarted
дают возможность настроить различные аспекты манипулирования, ограничить
допустимые манипуляции или вообще отменить операцию.
Событие ManipulationDelta сообщает подробную информацию об ожидаемом
параллельном переносе, повороте или масштабировании элемента; ее можно напрямую
использовать в соответствующем геометрическом преобразовании.
Информация передается в следующих свойствах класса ManipulationDelta


Translation - свойство типа Vector, содержащее значение X,Y



Scale - еще одно свойство типа Vector

Мультисенсорные события

217



Rotation - свойство типа double, определяющее угол поворота в градусах



Expansion - свойство типа Vector, которое при наличии Scale можно считать
избыточным; сообщает разницу в размерах, выраженную в абсолютных
независимых от устройства пикселах, а не в терминах коэффициентов масштабирования

Заметим еще, что объект ManipulationDeltaEventArgs, передаваемый обработчику
события ManipulationDelta, содержит два свойства типа ManipulationDeltaDeltaManipulation (сообщает об изменениях, произошедших с момента последней
генерации этого события) и CumulativeManipulation(сообщает об изменениях,
произошедших с момента генерации события ManipulationStarted). Так что, какой бы
способ использования данных вы ни выбрали, система найдет, чем вас порадовать!
В листинге 6.7 приведен застраничный код для показанного ниже окна Window он
позволяет двигать, поворачивать и масштабировать фотографию с помощью
стандартных жестов: движение одним пальцем по прямой, движение по кругу и
движение двумя пальцами в разные стороны.










Результат показан на рис. 6.3.

Рис. 6.3 Сдвиг, поворот и масштабирование изображения с помощью обработки
события ManipulationDelta

218

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Листинг 6.7. MainWindow.xaml.cs - работа с классом ManipulationDelta для сдвига,
поворота и масштабирования
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace ManipulationEvents
{
publicpartialclassMainWindow : Window
{
public MainWindow()
{
InitializeComponent();
canvas.ManipulationDelta += Canvas_ManipulationDelta;
}
void Canvas_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
MatrixTransform transform = photo.RenderTransform asMatrixTransform;
if (transform != null)
{
// Применить дельты к матрице потом воспользоватся
// созданной матрицей в преобразовании MatrixTransfotm
Matrix matrix = transform.Matrix;
matrix.Translate(e.DeltaManipulation.Translation.X,
e.DeltaManipulation.Translation.Y);
matrix.RotateAt(e.DeltaManipulation.Rotation,
e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
matrix.ScaleAt(e.DeltaManipulation.Scale.X, e.DeltaManipulation.Scale.Y,
e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
transform.Matrix = matrix;
e.Handled = true;
}
}
}
}

Для объекта Image с именем photo в разметке уже определено преобразования
MatrixTransform, применяемое в режиме RenderTransform , поэтому коду в обработчике
ManipulationDelta остается только записать в матрицу Matrix данные, взятые из объекта
Manipulation DeltaEventArgs. Методы RotateAt и ScaleAt используются для задания
центра поворота и масштабирования (e.ManipulationOrigin).
Манипуляции всегда производятся относительно контейнера манипулирования. По
умолчанию это элемент, для которого свойство IsManipulationEnabled=True; именно
поэтому в XAML-разметке для данного примера оно уставлено в элементе Canvas, а не
в Image. Назначить контейнером манипулирования можно любой элемент,

Мультисенсорные события

219

для этого достаточно обработать его событие ManipulationStarting и записать в свойство
ManipulationStartingEventArgs.ManipulationContainer ссылку на данный элемент.

Добавление инерции
События манипулирования поддерживают также придание объектам инерции,
благодаря которой их движение будет постепенно замедляться, а не сразу
останавливаться по завершении жеста. При этом ощущение от жеста получается более
реалистичным и открывается возможность поддержать «щелчки», так чтобы
расстояние, на которое перемещен объект, зависело от скорости щелчка.
Чтобы включить инерцию, следует обработать событие ManipulationInertiaStarting в
дополнение к другим событиям манипулирования. Именно ManipulationInertiaStarting, а
не ManipulationCompleted- первое событие манипулирования, которое генерируется
после убирания всех пальцев с экрана. В обработчике ManipulationInertiaStarting вы
можете решить, что именно поддерживать, для этого следует установить какие-то из
свойств
ManipulationInertiaStartingEventArgs.
TranslationBehavior,
ManipulationInertiaStartingEventArgs.RotationBehavior
и
ManipulationInertiaStartingEventArgs.ExpansionBehavior.
В
результате
система
продолжит
генерировать
события
ManipulationDelta(в
которых
свойство
ManipulationDeltaEventArgs.IsInertial будет равно true) до тех пор, пока «трение» не
заставит объект остановиться, а в этот момент будет сгенерировано событие
ManipulationCompleted. (Если в обработчике события ManipulationInertiaStarting ничего
не делать, то событие ManipulationCompleted генерируется сразу после него.)
Ниже приведен перечень свойств, которые можно установить для настройки инерции
при сдвиге, повороте или масштабировании:


TranslationBehavior -DesiredDisplacement, DesiredDeceleration, InitialVelocity



RotationBehavior - DesiredRotation, DesiredDeceleration, InitialVelocity



ExpansionBehavior – DesiredExpansion, DesiredDeceleration, InitialRadius, InitialVelocity

Обычно необходимо задавать только DesiredDeceleration (желательно замедление) или
специфические для конкретного поведения свойства DesiredDisplacement (желательный
сдвиг), DesiredRotation (желательный угол поворота) либо DesiredExpansion
(желательный коэффициент масштабирования). Последние три свойства нужны для
того, чтобы элемент не переместился слишком далеко. По умолчанию InitialVelocity и
InitialRadius инициализируются текущими значениями, что обеспечивает плавный
переход. Можно получить различные скорости в момент возникновения события
ManipulationInertiaStarting,
опросив
объект
ManipulationInertiaStartingEventArgs.InitialVelocities, в котором есть свойства
LinearVelocity, AngularVelocity и ExpansionVelocity.
В листинге 6.8 код из листинга 6.7 модифицирован, чтобы поддержать инерцию.

220

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Листинг 6.8.MainWindow.xaml.c8 -работа с классами ManipulationDelta
ManipulationInertiaStarting для сдвига, поворота и масштабирования с инерцией

и

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace ManipulationEvents
{
publicpartialclassMainWindow : Window
{
public MainWindow()
{
InitializeComponent();
canvas.ManipulationDelta += Canvas_ManipulationDelta;
canvas.ManipulationInertiaStarting += Canvas_ManipulationInertiaStarting;
}
void Canvas_ManipulationInertiaStarting(object sender,
ManipulationInertiaStartingEventArgs e)
{
e.TranslationBehavior.DesiredDeceleration = 0.01;
e.RotationBehavior.DesiredDeceleration = 0.01;
e.ExpansionBehavior.DesiredDeceleration = 0.01;
}
void Canvas_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
MatrixTransform transform = photo.RenderTransform asMatrixTransform;
if (transform != null)
{
// Применить дельты к матрице потом воспользоватся
// созданной матрицей в преобразовании MatrixTransform
Matrix matrix = transform.Matrix;
matrix.Translate(e.DeltaManipulation.Translation.X,
e.DeltaManipulation.Translation.Y);
matrix.RotateAt(e.DeltaManipulation.Rotation,
e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
matrix.ScaleAt(e.DeltaManipulation.Scale.X, e.DeltaManipulation.Scale.Y,
e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
transform.Matrix = matrix;
e.Handled = true;
}
}
}
}

Необходимо следить, чтобы элемент не ушел полностью за пределы экрана, особенно
если включена инерция. Можно воспользоваться событием

Мультисенсорные события

221

Manipu lationBoundaryFeedback, чтобы получать уведомления о том, что элемент достиг
границы контейнера манипулирования, и воспрепятствовать его перемещению.
СОВЕТ
WPF предлагает простой способ заставить окно колебаться, когда что-то проходит
через его границу, - как в эффекте прокрутки за конец списка, который сделался
популярным благодаря iPhone. Чтобы этого добиться, нужно в обработчике события
ManipulationDelta вызвать метод ReportBoundaryFeedback полученного объекта
ManipulationDeltaEventArgs. Тогда будет сгенерировано событие ManipulationBoundaryFeedback, которое будет обработано элементом Window, и результатом
станет желаемый эффект.

FAQ
ВклассеManipulationDeltaEventArgs есть методы Complete иCancel. В чем между
ними разница?
Метод Complete останавливает манипуляцию (как прямую, так и инерционную).
Метод Cancel тоже останавливает манипуляцию, но передает данные о касании
событиям мыши, так что поведение может быть частично продолжено для элементов,
умеющих работать с мышью, но не с сенсорными устройствами.
В листинге 6.9 инерция вращения используется для реализации «колеса фортуны»,
показанного на рис. 6.4. Это застраничный код для следующего окна Window:

















222

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

Рис. 6.4. Из-за инерции вращения колесо продолжает крутиться и после завершения
жеста, как в некоторых играх
Листинг 6.9. MainWindow.xaml.cs - реализация колеса фортуны с инерцией
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace SpinThePrizeWheel
{
publicpartialclassMainWindow : Window
{
public MainWindow()
{
InitializeComponent();
grid.ManipulationStarting += Grid_ManipulationStarting;
grid.ManipulationDelta += Grid_ManipulationDelta;
grid.ManipulationInertiaStarting += Grid_ManipulationInertiaStarting;
grid.ManipulationCompleted += Grid_ManipulationCompleted;
}
void Grid_ManipulationStarting(object sender,
ManipulationStartingEventArgs e)
{
e.Mode = ManipulationModes.Rotate;
}
void Grid_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
(prizeWheel.RenderTransformasRotateTransform).Angle +=
e.DeltaManipulation.Rotation;
}

Мультисенсорные события

223

void Grid_ManipulationInertiaStarting(object sender,
ManipulationInertiaStartingEventArgs e)
{
e.RotationBehavior.DesiredDeceleration = 0.001;
}
void Grid_ManipulationCompleted(object sender,
ManipulationCompletedEventArgs e)
{
// Колесо остановить пора сообщить что он выиграл
}
}
}

В листинге 6.9 обработчик события ManipulationStarting сообщает, что его интересуют
только повороты. Это необязательно, потому что обработчик события
ManipulationDelta только на данные об угле поворота и обращает внимание, но является
признаком хорошего тона (да и для производительности полезно). Обработчик
ManipulationDelta изменяет параметры преобразования RotateTransform, увеличивая
угол поворота Angle на величину e. DeltaManipulation.Rotation. Можно было бы вместо
этого просто записать в Angle значение е.CumulativeManipulation.Rotation, но тогда при
последующем запуске колесо начало бы вращение с угла 0°, что будет резать глаз и
выглядит неестественно.
В обработчике ManipulationInertiaStarting колесу придается очень небольшое
замедление, так что после прекращения контакта оно будет вращаться довольно долго.
Наконец, обработчик события ManipulationCompleted — самое подходящее место для
определения конечного положения колеса и награждения пользователя.
СОВЕТ
Можно воспользоваться встроенной в элемент ScrollViewer поддержкой сдвигов и
присвоить свойству PanningMode одно из значений HorizontalOnly, VerticalOnly,
HorizontalFirst, VerticalFirst или Both. В классе ScrollViewer имеются также свойства
PanningDeceleration и PanningRatio. Последнее используется как коэффициент при
вычислении расстояния для реализующего манипуляцию преобразования
TranslateTransform.
По умолчанию свойство PanningMode равно None, но некоторые элементы
управления WPF задают для своего внутреннего ScrollViewer другое значение, более
подходящее для стандартного стиля и позволяющее работать с мультисенсорными
устройствами без явных действий со стороны программиста.

224

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства

СОВЕТ
В доступном для скачивания наборе инструментов SurfaceToolkitforWindowsTouch
есть немало превосходных элементов управления WPF для MicrosoftSurface, которые
оптимизированы для работы с мультисенсорными устройствами. В их число входят
как варианты большинства стандартных элементов управления для сенсорного
рабочего стола (например, SurfaceButton и SurfaceCheckBox), так и совершенно
новые элементы (в частности, ScatterView и LibraryStack).

Команды
Хотя эта глава посвящена в основном событиям, важно иметь представление о
встроенной в WPF поддержке команд, более абстрактной и слабо связанной версии
событий. Если события жестко связаны с деталями конкретных действий пользователя
(например, нажатие кнопки или выбор элемента ListBoxItem из списка), то команды
представляют действия независимо от того, как они выглядят в пользовательском
интерфейсе. Каноническими примерами служат команды Cut (Вырезать), Сору
(Копировать) и Paste (Вставить). В приложениях эти действия часто представляются
сразу несколькими способами: пункты MenuItem меню Menu, пункты MenuItem меню
ContextMenu, кнопки Button на панели инструментов ToolBar, сочетания клавиш и т. д.
Наличие нескольких представлений таких команд, как Cut, Сору и Paste, можно
сравнительно неплохо обработать с помощью событий. Например, можно определить
обобщенные обработчики для каждого действия и присоединить их к соответствующим
событиям элементов интерфейса (событию Click кнопки Button, событию KeyDown
главного окна Window и т. д.). Кроме того, нужно будет активировать и деактивировать
элементы управления, когда соответствующие им действия недопустимы (например,
деактивировать операцию вставки в интерфейсе, когда в буфере обмена ничего нет). Но
реализация такой двусторонней связи довольно быстро становится утомительным
делом, особенно если вы не хотите жестко зашивать в код список элементов
управления, нуждающихся в обновлении.
К счастью, поддержка команд в WPF спроектирована так, чтобы максимально
упростить работу в подобных ситуациях. Предлагаемый механизм уменьшает объем
написанного вами кода (а иногда позволяет вообще не писать процедурный код) и дает
вам возможность более гибко изменять пользовательский интерфейс, не нарушая
стоящую за ним логику. Команды - не новинка, появившаяся только в WPF; в прежних
технологиях, в частности вбиблиотек ке классов MicrosoftFoundationClassLibrary
(MFC), тоже существовал подобный механизм. Разумеется, даже если вы знакомы с
MFC, изучить уникальные особенности команд в WPF все равно придется.
Мощь механизма команд основывается на трех основных особенностях:


В WPF определено много встроенных команд.

Команды



225

В команды встроена автоматическая поддержка жестов ввода (например,
сочетаний клавиш).
Встроенное поведение некоторых элементов управления WPF уже ориентировано на те или иные команды.

Встроенные команды
Командой называется любой объект, реализующий интерфейс ICommand (из
пространства имен System.Windows.Input), в котором объявлены три простых члена:


Execute - метод, который выполняет характерную для команды логику



CanExecute - метод, который возвращает true, если команда активирована, и
false, если она деактивирована



CanExecuteChanged - событие, которое генерируется при изменении значения
CanExecute

Чтобы создать команды Cut, Сору и Paste, нужно было бы сделать следующее: написать
три класса, реализующих интерфейс
ICommand; решить, куда поместить их
экземпляры (быть может, в статических полях класса главного окна); вызывать метод
Execute из соответствующих обработчиков событий (когда Can-Execute возвращает
true) и в обработчике события CanExecuteChanged переключать свойство IsEnabled
соответствующих элементов пользовательского интерфейса. Получается немногим
лучше, чем работа с самими событиями.
К счастью, в такие элементы управления, как Button, CheckBox и MenuItem, уже
встроена логика, позволяющая им взаимодействовать с любой командой от вашего
имени. В этих элементах имеется простое свойство Command(типа ICommand). Если
оно установлено, то элемент автоматически вызывает метод команды Execute(если
CanExecute возвращает true) всякий раз, как генерирует событие Click. Кроме того,
свойство IsEnabled автоматически синхронизируется со значением, возвращаемым
методом CanExecute, — для этого используется событие CanExecuteChanged.
Поскольку вся эта функциональность становится доступна в результате присваивания
простому свойству, то к ней можно обращаться из кода на XAML.
Но и это еще не все. В WPF уже определен целый ряд команд, поэтому вам не придется
писать реализующие ICommand классы для таких команд, как Cut, Сору и Paste, и
думать о том, где хранить соответствующие объекты. Встроенные в WPFкоманды
доступны в виде статических свойств пяти разных классов:



ApplicationCommands - Close, Copy, Cut, Delete, Find, Help, New, Open, Paste,
Print, PrintPreview, Properties, Redo, Replace, Save, SaveAs, SelectAll, Stop, Undo и
др.
ComponentCommands - MoveDown, MoveLeft, MoveRight, MoveUp, ScrollByLine,
ScrollPageDown, ScrollPageLeft, ScrollPageRight, ScrollPageUp, SelectToEnd,
SelectToHome, SelectToPageDown, SelectToPageUp и др.

226

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства




MediaCommands - ChannelDown, ChannelUp, DecreaseVolume, FastForward,
IncreaseVolume, MuteVolume, NextTrack, Pause, Play, PreviousTrack, Record,
Rewind, Select, Stop идр.
NavigationCommands - BrowseBack, BrowseForward, BrowseHome, BrowseStop,
Favorites, FirstPage, GoToPage, LastPage, NextPage, PreviousPage, Refresh, Search,
Zoom и др.
EditingCommands - AlignCenter, AlignJustify, AlignLeft, AlignRight, CorrectSpel
lingError, DecreaseFontSize, DecreaseIndentation, EnterLineBreak, EnterParagraph
Break, IgnoreSpellingError, IncreaseFontSize, IncreaseIndentation, MoveDownBy
Line,
MoveDownByPage,
MoveDownByParagraph,
MoveLeftByCharacter,
MoveleftByWordMoveRightByCharacter, MoveRightByWord и др.

Каждое из этих свойств возвращает не какой-то уникальный тип, реализующий
интерфейс ICommand, а объект одного и того же класса RoutedUICommand, который не
только реализует ICommand, но и поддерживает всплытие как маршрутизируемые
события.
В диалоговом окне About, к которому мы уже возвращались в начале этой главы,
имеется кнопка Help (Справка). В настоящий момент она не делает ничего, поэтому
воспользуемся ею для демонстрации работы встроенных команд - присоединим
некоторую логику с помощью команды Help, определенной все ApplicationCommands.
В предположении, что эта кнопка называется helpButton, ассоциирование ее с командой
Help в С# производится следующим образом:
helpButton. Command = ApplicationCommands. Help;

Во всех объектах RoutedUICommand определено свойство Text, которое содержит имя
команды для показа в пользовательском интерфейсе. (Только наличием этого свойства
класс RoutedUICommand отличается от своего базового класса RoutedCommand.)
Например, для команды Help свойство Text будет содержать строку Help (что и
неудивительно). Теперь вместо того, чтобы зашивать в код значение свойства кнопки
Content, мы можем написать:
helpButton. Content = ApplicationCommands. Help. Text;

СОВЕТ
Строка Text во всех командах RoutedUICommand автоматически локализуется при
использовании любого языка, поддерживаемого WPF! Это означает, что кнопка,
свойству Content которой присвоено значение ApplicationCommands.Help.Text, автоматически будет называться «Справка», если в текущей культуре пользовательского
интерфейса задан русский язык. Даже в контексте, где предполагается использование
изображений, а не текста (скажем, на панели инструментов), эту строку можно
использовать, например, в виде всплывающей подсказки. Разумеется,
ответственность за локализацию других строк в пользовательском интерфейсе попрежнему ложится на вас. Использование свойства Text в командах лишь позволяет
уменьшить количество нуждающихся в переводе терминов.

Команды

227

Если вы запустите окно About после этого изменения, то увидите, что кнопка
неактивна. Объясняется это тем, что встроенные команды не могут знать, ни когда им
следует активироваться и деактивироваться, ни какое действие они должны выполнять.
Эта логика делегируется клиенту команды.
Для подключения своего кода необходимо добавить объект CommandBinding к самому
элементу, который будет выполнять команду, или к любому его родителю (благодаря
всплытию маршрутизируемых команд). Во всех классах, производных от UIElement(и
ContentElement), имеется коллекция CommandBindings, в которой хранятся объекты
типа CommandBinding. Поэтому объект CommandBinding для кнопки Help можно
добавить прямо в корневой элемент Window окна About. В застраничном файле это
делается так:
this.CommandBindings.Add(newCommandBinding(ApplicationCommands.Help,
HelpExecuted, HelpCanExecute));

Здесь предполагается, что определены методы HelpExecutedиHelpCanExecute. Именно
их будет вызывать каркас, когда возникнет необходимость обратиться к реализациям
методов CanExecute и Execute команды Help.
В листингах 6.10 и 6.11 приведена очередная версия диалогового окна About, в которой
к кнопке Help привязана команда Help, причем сделано это целиком на XAML(хотя оба
обработчика все-таки нужно определять в застраничном файле).
Листинг 6.10. Окно About с поддержкой команды Help









Chapter 1
Chapter 2




You have successfully registered this product.

228

Глава 6 . События ввода: клавиатура, мышь, стилус и мультисенсорные устройства




Листинг 6.11. Застраничный код для разметки в листинге 6.10
using System.Windows;
using System.Windows.Input;
publicpartialclassAboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void HelpCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
System.Diagnostics.Process.Start(‚http://www.adamnathan.net/wpf‛);
}
}

Элемент CommandBinding для Window можно задавать в XAML, потому что в нем
определен конструктор по умолчанию, а внутренним полям можно присваивать
значения с помощью свойств. Свойству Content кнопки Button можно даже присвоить
значение свойства Text выбранной команды (все в XAML) благодаря широко
применяемой технике привязки к данным, которую мы будем рассматривать в главе 13.
Обратите еще внимание, как упрощается задание команды Help из-за наличия
конвертера типа. Класс CommandConverter знает все о встроенных командах, поэтому в
обоих местах свойству Command можно просто присвоить значение Help, не прибегая к
более громоздкому синтаксису {x:StaticApplicationCommands.Help}. (Для написанных
вами команд компилятор не окажет подобную любезность.) Метод HelpCanExecute в
застраничном файле написан так, что команда все время активна, а метод HelpExecuted
запускает вебраузер, передавая ему URL-адрес страницы со справкой.

Выполнение команд с помощью жестов ввода
Применение команды Help в простом окне About может показаться перебором -ведь
было бы достаточно простого обработчика события Click. Но у команды, помимо
локализованного текста, есть и еще одно достоинство: автоматическая привязка к
комбинации клавиш.
Обычно справка вызывается, когда пользователь нажимает клавишу F1. И надо же, в
окне About из листинга 6.10 при нажатии F1 автоматически вызывается команда Help,
как если бы вы нажали кнопку Help! А все потому, что для встроенных команд типа
Help определены подразумеваемые по умолчанию жесты ввода, которые приводят к

Команды

229

выполнению команды. Можно связать с командой и свой жест ввода, добавив в
коллекцию InputBindings подходящий объект KeyBinding и/или MouseBinding.
(Поддержка для стилуса или сенсорного ввода не предусмотрена.) Например, чтобы
назначить клавишу F2 в качестве активатора команды Help, можно добавить
следующее предложение в конструктор класса AboutDialog:
this.InputBindings.Add( new KeyBinding(ApplicationCommands. Help,
new KeyGesture(Key. F2)));

Но при этом активировать команду Help будут обе клавиши: F1 и F2. Чтобы подавить
подразумеваемую по умолчанию клавишу F1, нужно связать с ней специальную
команду NotACommand:
this.InputBindings.Add( new KeyBinding(ApplicationCommands. NotACommand, new
KeyGesture(Key.FI)));

Оба предложения можно представить и в XAML-разметке следующим образом:





Элементы управления со встроенными привязками к командам
Некоторые элементы управления WPF содержат собственные привязки к командампервый раз встретившись с этим явлением, воспринимаешь его, как чудо. Простейший
пример - элемент TextBox, в который встроены привязки к командам Cut, Сору и Paste
для взаимодействия с буфером обмена, а также к командам Undo и Redo. Это означает
не только то, что TextBox реагирует на стандартные комбинации Ctrl+X, Ctrl+C, Ctrl+V,
Ctrl+Z и Ctrl+Y, но и что в этих действиях могут принимать участие дополнительные
элементы.
Механизм встроенных привязок к командам демонстрируется в следующей автономной
XAML-разметке:





Эффект DropShadowEffect рассматривается в главе 15 «Двумерная графика»: он
придает кругу чуть более изысканный вид. С этой разметкой ассоциирован такой
застраничный файл:
using System.Windows;
using System.Windows.Input;

XAML-приложения для браузера

263

public partial class GadgetWindow : Window
{
public GadgetWindow()
{
InitializeComponent();
}
void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}
void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}

Чтобы окно можно было перемещать, обработчик события MouseLeftButtonDown
просто вызывает метод Window.DragMove. Все остальное метод DragMove сделает сам.
На рис. 7.10 показано, как выглядит это крохотное приложение.

Рис. 7.10. Невидимое окно Window с непрямоугольным (и полупрозрачным)
содержимым

XAML-приложения для браузера
WPF поддерживает создание приложений, способных работать непосредственно в веббраузере. Они называются XAML-приложениями для браузера (XAML Browser
Applications — XBAPs), хотя правильнее было бы говорить «WPF-приложения для
браузера». ХВАР-приложения утрачивают свою привлекательность по мере того, как
Silverlight по своим возможностям все больше приближается к WPF. Однако они попрежнему решают задачу выполнения в браузере WPF-содержимого с частичным
доверием без надоедливых вопросов.

264

Глава 7.Структурирование и развертывание приложения

FAQ
Работают ли ХВАР-приложения в любой операционной системе и в любом
браузере?
Нет. В отличие от приложений Silverlight, ХВАР-приложениям необходим полная
версия .NET Framework (3.0 или выше), поэтому они работают Только в Windows и
только в браузерах Internet Explorer (или в любой программе, поддерживающей
элемент управления ActiveX WebBrowser) и Firefox (при наличии версии .NET
Framework 3.5 или более поздней). Для работы с .NET Framework 4.0 в Firefox
необходимо скачать и установить дополнение. (Дополнение длз| версии 3.5
устанавливается автоматически.)
Создание XBAP-приложения мало чем отличается от создания стандартного
приложения Windows при условии, что разработчик не выходит за рамки подмножества
.NET, доступного для программыс частичным доверием. Перечислим основные
различия:

По умолчанию доступны не все средства WPF и .NET Framework/

Навигация интегрирована в браузер.

Развертывание осуществляется по-другому.
В этом разделе мы рассмотрим все три отличительные особенности XAMLприложений для браузера.
Итак, как же создается XBAP-приложение? В Visual Studio достаточно выполнить
следующие шаги:
1.
Создать новый проект, в Visual Studio его тип, как и положено, называется WPF
Browser Application.
2.
Сконструировать пользовательский интерфейс внутри элемента Page и написать
застраничный код.
3.
Откомпилировать и запустить проект.
Если у вас нет Visual Studio, то можете воспользоваться программой MSBuild, задав в
проекте соответствующие настройки(см. врезку «КОПНЕМ ГРУБЖЕ» ниже).

КОПНЕМ ГЛУБЖЕ
Как работают XAML-приложения для браузера
В файлах, которые генерироет Visual Studio, нет ничего специфического именно для
XBAP-приложений. Важны лишь некоторые настройки в файле проекта, например:
True
False
Internet

XAML-приложения для браузера

265

В файле проекта есть также настройки, предписывающие отладчику запускать
программу PresentationHost.exe, а не результат компиляции.
Стандартный исполняемый файл генерируется, но, если его запустить непосредственно, ничего не произойдет, поскольку инфраструктура прерывает выполнение, когда видит, что программа не работает в контексте браузера. Помимо
ЕХЕ-файла генерируются еще два XML-файла:

файл с расширением .manifest - манифест ClickOnce-приложения;

файл с расширением .xbap - манифест развертывания ClickOnce-приложения
(для приложений, отличных от ХВАР, такие файлы обычно имеют расширение
.application)
Ну вот, собственно, и все. ХВАР-приложения - это, по существу, ClickOnceприложения, способные работать только в онлайновом режиме, которые WPF обрабатывает особым образом для лучшей интеграции с браузером.

ПРЕДУПРЕЖДЕНИЕ
Остерегайтесь кэширования ClickOnce!
ХВАР-приложения основаны на технологии ClickOnce, в которой имеется механизм
кэширования, только мешающий на этапе разработки. Для достижения максимальной
производительности ClickOnce-приложение при первом запуске сохраняется в кэше.
Последующие запросы на запуск приложения удовлетворяются из кэша, если только
не изменился номер версии приложения. (Как и изолированное хранилище, кэш
ClickOnce реализован в виде скрытой папки, находящейся внутри папки Documents
конкретного пользователя.)
Поэтому, изменив код приложения, перекомпилировав его и снова запустив, вы не
увидите результата изменения, если одновременно не зададите другой номер версии!
По умолчанию Visual Studio увеличивает номер версии при каждой перекомпиляции
(из-за строки AssemblyVersion(‖1.0.*") в исходном файле AssemblyInfo), так что вы не
столкнетесь с этой проблемой, если явно не присвоите приложению фиксированный
номер версии.
Если вы считаете, что увеличение номера версии при каждой компиляции - неприемлемая практика, то можете в любой момент очистить кэш, воспользовавшись
инструментом mage.exe из Windows SDK. Достаточно выполнить команды. Если
SDK не установлен, то подойдет также команда
rundll32 %windir%\system32\dfshim.dll CleanOnlineAppCache

Ограниченный набор возможностей
В случае простенького WPF-пpилoжeния достаточно изменить в проекте несколько
настроек, перекомпилировать его и получить отлично работающее ХАМL-приложение
для браузера. Но обычно WPF-пpилoжeния не настолько просты. Разработка ХВАРприложений осложняется тем фактом, что они работают в зоне Интернета с частичным
доверием, а в этом контексте доступны не все API.

266

Глава 7.Структурирование и развертывание приложения

Например, если попытаться конвертировать стандартную версию приложения Photo
Gallery в ХВАР, то сразу обнаружится, что, например, следующий вызов приводит к
исключению безопасности (весьма многословному):
// Опа! Коду с частичным доверием не разрешено обращаться к этим данным!
AddFavorite(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures));

Встроенный в .NET Framework механизм разграничения доступа кода блокирует этот
вызов, потому что для его выполнения требуется разрешение FileIOPermission, которое
по умолчанию зоне Интернета не предоставляется. (Отметим, что пользователь в
принципе может расширить набор разрешений, предоставляемых в зоне Интернета, но
делать это не рекомендуется из соображений безопасности.)
Большинство разработчиков выясняют, что работает, а что не работает в зоне
Интернета, методом проб и ошибок. Некоторые средства не работают, потому что по
природе своей небезопасны — например, произвольный доступ к локальной файловой
системе или реестру, интероперабельность с неуправляемым кодом или создание новых
объектов Window. (Создавать элементы Popup можно, но они не смогут выйти за
границы объемлющего элемента Раgе.) Причины, по которым в зоне Интернета
запрещены другие средства, не всегда очевидны, так как ограничения являются
результатом особенностей реализации. Некоторые средства могут быть запрещены в
одном браузере и разрешены в другом. Например, WPF не разрешает использовать в
ХВАР-приложении элемент управления WebBrowser, если это приложение работает в
браузере Firefox.

СОВЕТ
Если требуется использовать общий код в стандартном приложении с полным доверием и в ХВАР-приложении с частичным доверием, то рекомендуется на этапе
выполнения определять, в какой среде приложение работает, и соответственно
модифицировать поведение программы. Сделать это можно с помощью статического
булевского свойства BrowserInteropHelper.IsBrowserHosted из пространства имен
System.Windows.Interop.
Несмотря на все ограничения, в зоне Интернета доступна весьма обширная
функциональность. Можно отображать форматированный текст и мультимедийные
данные, писать в изолированное хранилище и читать из него (до 512 Кб), открывать
произвольные файлы на веб-сервере. Можно даже запустить стандартное диалоговое
окно браузера с помощью команд меню Файл->Открыть и работать с локальными
файлами (получив явное разрешение пользователя). Делается это посредством метода
Microsoft.Win32.OpenFileDialog:
string fileContents = null;
OpenFileDialog ofd = new OpenFileDialog();
if (ofd.ShowDialog() == true) // результат может быть true, false, или null
{

XAML-приложения для браузера

267

using (Stream s = ofd.OpenFile())
using (StreamReader sr = new StreamReader(s))
{
fileContents = sr.ReadToEnd();
}
}

СОВЕТ
Еще одно различие между ХВАР и стандартным приложением Windows заключается
в способе передачи параметров (да и вообще любых внешних данных). Проще всего
передать параметры в URL-адресе HTML-страницы, содержащей ХВАР-приложение,
а для получения полного URL (вместе с параметрами) вызвать в самом приложении
метод BrowserInteropHelper.Source. Другой подход — сохранить информацию в
cookie браузера, а для получения этих данных вызвать метод Application.GetCookie.

FAQ
Как мне запустить свои собственные компоненты в зоне Интернета?
Используйте механизм, общий для всех компонентов .NET: если пометить сборку
атрибутом AllowPartiallyTrustedCallers и установить ее в глобальный кэш сборок (а
сделать это можно только, если пользователь доверяет вашему коду и готов
выполнить его), то любое ХВАР-приложение сможет обращаться к находящимся в
этой сборке открытым API.
Отметим, что помечать сборку атрибутом AllowPartiallyTrustedCallers следует лишь
после тщательного анализа. Любая ошибка проектирования или реализации, из-за
которой компонент может оказаться непригодным для работы в зоне Интернета,
открывает зияющую брешь в системе защиты. И если такое случится, пользователи,
возможно, никогда больше не будут доверять вашему коду.

FAQ
Как создать ХВАР-приложение с полным доверием?
Если вы хотите воспользоваться средствами, требующими более высокого уровня
доверия, и тем не менее выполнять приложение в браузере, то можете сконфигурировать ХВАР-приложение с полным доверием. Правда, для этого нужно
выполнить два хитрых шага:
1.
В манифесте ClickOnce-приложения (app.manifest) добавьте строку
Unrestricted="true" в XML-элeмeнт PermissionSet, как показано в следующем примере:


264
2.

Глава 7.Структурирование и развертывание приложения
В файле проекта (с расширением .csproj или.vbproj) измените строку

Internet

на такую:
Custom

Эквивалентные действия можно проделать и в Visual Studio - в окне свойств проекта
на вкладке Security (Безопасность).
После этого ХВАР-приложение можно будет развернуть и запустить в зоне Локальный компьютер. Такое приложение с полным доверием можно запускать и в зоне
Интернета, но только если пользователь явно включит вас (точнее, сертификат,
использованный для подписи манифеста) в список доверенных издателей.

Интегрированная навигация
Все элементы Раgе в ХВАР-приложении неявно вложены в элемент NavigationWindow.
В Internet Explorer 6 и Firefox вы увидите типичную панель с кнопками Назад и Вперед.
Обычно это нежелательно, так как немногие ХВАР-приложения нуждаются в
навигации. Но, даже если нуждаются, иметь отдельные кнопки Назад и Вперед прямо
под точно такими же кнопками браузера неестественно. Чтобы убрать ненужную
панель навигации, присвойте значение false свойству ShowsNavigationUI элемента Page.
К счастью, в версии Internet Explorer 7 и последующих журнал объекта
NavigationWindow объединен с собственным журналом браузера, что делает работу
гораздо более естественной. Отдельная панель навигации не показывается, а записи,
добавленные в журнал WPF, автоматически появляются в списке прямых и обратных
переходов, который показывает браузер, - наряду с веб-страницами.

СОВЕТ
Интеграция с журналом браузера в Internet Explorer 7 (и более поздних версиях)
применима только к странице Page верхнего уровня. Если ХВАР-приложение
работает в HTML-фрейме IFRAME, то панель навигации будет видна, если только не
сброшено в false свойство ShowsNavigationUI WPF-элемента Page.

Развертывание
Развернуть ХВАР-приложение так же просто, как любое другое ClickOnce приложение.
Все сводится к использованию Мастера публикации в Visual Studio (или инструмента
Mage из Windows SDK) и копированию файлов на веб-сервер либо в общую папку.
(Веб-сервер следует также правильно сконфигурировать для обслуживания данного
контента.)

XAML-приложения для браузера

269

Самое поразительное в ХВАР-приложении — тот факт, что пользователь может
установить и запустить его, просто перейдя по его URL-адресу, даже дополнений
никаких не требуется (в случае с Internet Explorer). Кроме того, в отличие от других
ClickOnce-приложений, браузер не выдает никаких предупреждений, касающихся
безопасности, если, конечно, ХВАР-приложение не требует каких-то нестандартных
разрешений. (Поэтому, чтобы начать работу с таким приложением, даже щелчка
мышью не потребуется!)

FAQ
При запуске ХВАР не выдаются предупреждения, касающиеся безопасности.
Разве это не гигантская брешь в защите?
Любая программа самим фактом своего запуска потенциально рискует открыть
брешь в системе защиты. Но наличие нескольких уровней защиты в самой ОС
Windows, в Internet Explorer и в каркасе .NET Framework вселяет в команду
разработчиков WPF уверенность в том, что хакеры не смогут воспользоваться
механизмом ХВАР, чтобы обойти защиту. Например, .NET Framework организует
«песочницу» (sandbox) поверх той, что уже активирована Internet Explorer. И хотя
теоретически уже такой защиты должно быть достаточно, WPF идет дальше и
исключает избыточные привилегии уровня операционной системы (например,
возможность загружать драйверы устройств) из маркера безопасности объемлющего
процесса — просто на тот невероятный случай, когда все остальные уровни защиты
взломаны.

СОВЕТ
Наряду с Silverlight, технология ХВАР — ключ к использованию WPF-содержимого в
разных окружениях. Например, Windows Media Center и гаджеты рабочего стола
Windows позволяют разработчику подключать HTML. Стоит разместить ХВАРприложение в такой HTML-странице, как вы получаете приложение для WPF Media
Center или WPF-гаджет рабочего стола!

Загрузка файлов по требованию
Технология ClickOnce поддерживает загрузку файлов по требованию приложения, так
что можно спроектировать небольшое приложение, которое быстро загружается само, а
затем по мере необходимости подгружает дополнительное содержимое, руководствуясь
собственной логикой. Эта возможность - настоящее спасение для больших ХВАРприложений, которые в противном случае загружались бы слишком долго, но она
применима и к приложениям других типов.
Чтобы воспользоваться ею, следует в проекте Visual Studio поместить несколько
автономных файлов в группу загрузки. Это можно сделать на вкладке, открываемой
командами меню Publish->Application Files (Публикация->Файлы приложения), на
странице свойств проекта. Затем вы можете запросить загрузку этих файлов из
программы и получить уведомление по завершении загрузки.

270

Глава 7.Структурирование и развертывание приложения

Для этой цели в пространстве имен System.Deployment.Application (сборка
System.Deployment.dll) имеются соответствующие API.
В листинге 7.3 показано, как отобразить в пользовательском интерфейсе информацию о
ходе загрузки основного содержимого приложения. Предполагается, что выполнение
приложения начинается с загрузки страницы Pagel, застраничный файл которой
приведен в листинге 7.3. (Как именно выглядит определенный в XAML-файле
пользовательский интерфейс, не столь существенно.) Класс Pagel инициирует загрузку
файлов, отнесенных к группе загрузке MyGroup, а по ее завершении переходит к
странице Раgе2 (которая предположительно нуждается в каких-то загруженных
файлах).
Листинг 7.3. Использование встроенной в технологию ClickOnce возможности
загрузки по требованию
using
using
using
using

System;
System.Windows.Controls;
System.Windows.Threading;
System.Deployment.Application;

public partial class Page1 : Page
{
public Page1()
{
InitializeComponent();
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
if (ApplicationDeployment.IsNetworkDeployed)
{
// Обработать событие, генерируемое по завершении загрузки
// всех файлов в группе MyGroup.
ApplicationDeployment.CurrentDeployment.DownloadFileGroupCompleted +=
delegate {
// Мы работаем в другом потоке, поэтому вызываем метод
// GotoPage2 в потоке ГИП с помощью Beginlnvoke
Dispatcher.BeginInvoke(DispatcherPriority.Send,
new DispatcherOperationCallback(GotoPage2), null);
};
ApplicationDeployment.CurrentDeployment.DownloadFileGroupAsync(‚MyGroup‛);
}
else
{
// Мы работаем не в контексте ClickOnce (быть может, просто
// под отладчиком), поэтому сразу переходим к Раgе 2.
GotoPage2(null);
}
}

Автономные XAML-страницы

271

// Переходит к Page2 по завершении загрузки. Принимает и возвращает объект
// только ради совместимости с сигнатурой метода DispatcherOperationCallback
private object GotoPage2(object o)
{
return NavigationService.Navigate(new Uri(‚Page2.xaml‛, UriKind.Relative));
}
}

Поддержка загрузки по требованию применяется, только когда приложение работает в
сети (а не локально под отладчиком), поэтому мы сначала опрашиваем свойство
ApplicationDeployment.IsNetworkDeployed, проверяя, можно ли на эту поддержку
рассчитывать. Если приложение развернуто не в сети, то все файлы уже находятся в
локальной файловой системе, поэтому мы сразу переходим к странице Раgе2. В
противном случае инициируем загрузку, вызывая метод DownloadFileGroupAsync. Но
предварительно присоединяем к событию DownloadFileGroupCompleted анонимный
делегат, чтобы навигацию можно было продолжить сразу по завершении загрузки. В
классе ApplicationDeployment определены и другие события на случай, если вы
захотите отображать более детальную информацию о ходе загрузки.

Автономные XAML-страницы
Если установлена версия .NET Framework 3.0 или более поздняя, то Internet Explorer
получает возможность непосредственно отображать с помощью WPF XAML-файлы
точно так же, как обычные HTML-файлы. Поэтому при определенных условиях вместо
HTML можно использовать XAML, обеспечивая улучшенную поддержку компоновки,
текста, графики и т.д. Правда, есть и ограничения: в автономных XAML-файлах не
должно быть процедурного кода и отображаться они могут только в Windows.
Впрочем, поэкспериментировать с этой возможностью все равно интересно.
Даже несмотря на отсутствие процедурного кода, в автономных XAML-файлах можно
создать довольно развитый динамический интерфейс - благодаря привязке к данным
(см. главу 13 «Привязка к данным»). На рис. 7.11 показана версия приложения Photo
Gallery в виде автономного XAML-файла. Она отображает статический набор
изображений, хранящихся на веб-сервере, но для реализации качественного увеличения
использует привязку к данным.

СОВЕТ
Если вы хотите, чтобы сайт мог воспользоваться всем богатством автономного
XAML, но при этом был способен показывать обычную HTML-страницу пользователям, не имеющим возможности просматривать XAML, то можете поддерживать
две версии контента и динамически выбирать подходящую. Для этого достаточно
проверить, есть ли в строке агента пользователя подстрока вида ". NET CLR 3.0".
Впрочем, я еще не встречал сайта, который применял бы такую уловку. Адаптивное
добавление Silverlight решает эту задачу гораздо лучше.

272

Глава 7.Структурирование и развертывание приложения

Рис. 7.11. Приложение Photo Gallery в виде автономной XAML-страницы все равно
обладает интересными возможностями

СОВЕТ
Чтобы одновременно использовать контент в виде НТМL и автономного ХАМL
достаточно поместить один или несколько ХАМL-файлов во фреймы IFRAME на
НТМL-странице.

Резюме
Средства WPF для создания приложения охватывают все, что необходимо для
стандартных приложений Windows, а также позволяют осуществлять навигацию, как в
браузере, и выполнять приложения в контексте браузера. В исходном коде приложения
Photo
Gallery,
прилагаемом
к
книге
(доступен
по
адресу
http://informit.com/title/9780672331190), демонстрируется, что иногда одна и та же
реализация пользовательского интерфейса применима как в традиционном приложении
Windows, так и в насыщенной веб-странице без какого бы то ни было кода.
Во всех рассмотренных в этой главе случаях развертывание приложения производится
легко и быстро. Единственная шероховатость - необходимость установить подходящую
версию .NET Framework. К счастью, вместе с Windows Vista по умолчанию
устанавливается WPF 3.0, а вместе с Windows 7 WPF 3.5. В последующих версиях
Windows, скорее всего, по умолчанию будет устанавливаться WPF 4 или более поздняя
версия. Так что это требование не является обременительным, если только вам не
нужна самая свежая версия .NET Framework.

8





Особенности Windows 7
Списки переходов
Настройка элементов на панели задач
Функция Aero Glass
Функция TaskDialog

В каждой версии Windows появляется много новой функциональности, интересной для
разработчиков, и Windows 7 - не исключение. Как и в Windows Vista, в версии Windows
7 реализован целый ряд новых идей в области пользовательского интерфейса, и все они
доступны приложениям. Обладая новыми возможностями, приложение обретет более
современный облик и больше понравится пользователям.
В начале этой главы мы посмотрим, как сделать внешний вид WPF-приложения более
соответствующим Windows 7 с помощью двух новых механизмов:
 Списки переходов
 Настройка элементов на панели задач
А затем продемонстрируем два средства, появившиеся еще в Windows Vista, но
сохраняющих актуальность и в Windows 7:
 Функция Aero Glass
 Функция TaskDialog

Списки переходов
Одно из самых значительных нововведений в области пользовательского интерфейса
Windows 7 - списки переходов для элементов на панели задач. Список переходов
содержит удобные ярлыки, а чтобы посмотреть его, достаточно щелкнуть по элементу
на панели задач правой кнопкой мыши или пальцем потянуть его вверх. На рис. 8.1
показан список переходов для Internet Explorer.
Даже если приложение ничего не делает для того, чтобы воспользоваться списком
переходов, оно все равно получает такой список по умолчанию. На рис. 8.2 показаны
два варианта подразумеваемого по умолчанию списка переходов для приложения Photo
Gallery из предыдущей главы: когда оно открыто и когда закрыто. (Список переходов
для закрытого приложения можно увидеть, только если это приложение закреплено на
панели задач.)

274

Глава 8.Особенности Windows 7

Рис. 8.1. Список переходов для Internet Explorer может содержать элементы из
разных категорий
Открытое приложение

Закрытое, но закрепленное приложение

Рис. 8.2. Список переходов, подразумеваемый по умолчанию для приложения Photo
Gallery
В WPF 4 имеется класс System.Windows.Shell.JumpList, который позволяет определить
собственный список переходов для приложения с помощью несложного управляемого
кода или даже целиком в XAML! Это не означает, что внутри списка переходов можно
использовать визуальные элементы WPF, но имеющаяся функциональность
раскрывается в виде управляемых объектов с простыми свойствами.
Чтобы связать с приложением список переходов, нужно установить присоединенное
свойство с забавным названием JumpList.JumpList для экземплярами Application,
записав в него ссылку на экземпляр класса JumpList. А в процедурном коде следует
вызвать метод JumpList.SetJumpList. Если объект JumpList создается или
модифицируется в процедурном коде, то для отправки информации об изменениях
оболочке Windows следует вызвать метод Apply этого объекта.
В классе JumpList имеется также свойство содержимого JumpItems, которое может
содержать элементы двух типов: JumpTask и JumpPath; оба они наследуют
абстрактному классу JumpItem.

Списки переходов

275

Элемент JumpTask
Сточки зрения пользователя, элементы JumpTask представляют выполняемые действия,
например Start InPrivate Browsing (Начать просмотр InPrivate) или Open new tab
(Открыть новую
вкладку) на рис. 8.1. С точки зрения разработчика, объект JumpTask
278
представляет запускаемую программу (задачу операционной системы). Обычно они
применяются для запуска программы-владельца списка с аргументами командной
строки, определяющими, что она должна делать.
В листинге 8.1 демонстрируется использование нескольких элементов JumpTask в
файле App.xaml, взятом из примера Photo Gallery из предыдущей главы и немного
модифицированном. Получившийся список переходов показан на рис. 8.3. Отметим,
что три нижних пункта (два, если приложение закреплено и закрыто) присутствуют
всегда, поэтому наше определение списка переходов влияет лишь на то, что находится
выше этих стандартных пунктов.
Листинг 8.1. App.xaml - создание списка переходов с простыми элементами JumpTask










Puс. 8.3. Список переходов с тремя простыми элементами JumpTask
У каждого элемента JumpTask имеется атрибут Title — строка, отображаемая в списке,
— и необязательный атрибут Description, то есть всплывающая подсказка.

276

Глава 8.Особенности Windows 7

Поскольку никаких других свойств не задано, первый элемент Jump.Task просто
перезапускает приложение Photo Gallery. Это дублирует действие стандартного
элемента, расположенного в конце списка переходов, и в реальном приложении не
имеет смысла. Но вот следующие два элемента JumpTask передают новому экземпляру
Photo Gallery дополнительные аргументы командной строки, инструктируя приложение
о необходимости предпринять какие-то другие действия. Photo Gallery может
прочитать эти аргументы из свойства Environment.CommandLine и отреагировать
соответствующим образом.

СОВЕТ
С точки зрения пользователя, типичная задача в списке переходов не запускает
новый экземпляр программы, а приводит к выполнению каких-то действий в уже
запущенном экземпляре. Чтобы добиться подобного поведения, можно написать
приложение так, чтобы у него всегда было не более одного работающего экземпляра
(эта тема обсуждалась в предыдущей главе), и передать этому экземпляру
информацию, указанную в командной строке.
Если у приложения имеется нестандартный список переходов, то его элементы
появляются также в меню Пуск, когда данное приложение становится текущим. На рис.
8.4 показано, как список переходов, описанный в листинге 8.1, автоматически
добавляется в меню Пуск.

Рис. 8.4. В меню Пуск автоматически появляется список переходов, показанный на
рис. 8.3

Списки переходов

277

ПРЕДУПРЕЖДЕНИЕ
Отладчик Visual Studio взаимодействует со списками переходов!
При запуске под отладчиком в Visual Studio приложение представляется в виде файла
vshost32.exe, 280
как показано на рис. 8.5. Свой список переходов вы видите, но значки
могут выглядеть иначе, а щелчок по ним не работает (потому что приводит к запуску
vshost32.exe, а не вашей программы). Еще хуже обстоит дело с элементами JumpPath,
описанными в следующем разделе, — они не появляются вовсе. Чтобы обойти эту
проблему, можно сбросить флажок Enable the Visual Studio hosting process (Включить
ведущий процесс Visual Studio) в разделе Debug (Отладка) на странице свойств
проекта.

Puc. 8.5. Ведущий процесс отладчика Visual Studio оказывает влияние на список
переходов

ПРЕДУПРЕЖДЕНИЕ
Списки переходов разделяются всеми экземплярами приложения!
Списки переходов ассоциированы с приложением, а не с его конкретным окном или
работающим экземпляром. Все элементы, помещенные в список переходов,
сохраняются, даже когда приложение не работает. Если будет запущен второй
экземпляр приложения, который включит в список переходов другие элементы, то
они заменят элементы, ранее помещенные первым экземпляром.

Настройка поведения JumpTask
У элемента JumpTask имеется ряд свойств для установки значков и запуска
приложений, отличных от определенных владельцем списка. Эти свойства
демонстрируются в листинге 8.2, а на рис. 8.6 показан результат.

278

Глава 8.Особенности Windows 7

Листинг 8.2.Арр.хат1 - демонстрация дополнительных свойств элемента JumpTask











Рис. 8.6. Запуск других программ с помощью элементов JumpTask
Каждый из показанных в листинге элементов JumpTask устанавливает какое-то
дополнительное свойство, добавляющее очередную возможность. В первом элементе
установлено свойство ApplicationPath для запуска программы

Списки переходов

279

magnify.exe. Отметим, что в ApplicationPath можно указывать переменные окружения,
так что некоторые пути можно задавать в XAML, а не строить их в процедурном коде.
Во втором элементе JumpTask установлено свойство IconResourcePath, задающее путь к
значку. Значок должен быть ресурсом Win32, внедренным в EXE- или DLL-файл.
282и независимый файл с расширением .ico, но тогда придется указать
(Можно задать
полный путь, не содержащий переменных окружения, так что делать это в XAMLразметке неразумно.) Прописав путь к ЕХЕ-файлу, вы можете без труда получить
значок для программы по умолчанию. Если свойство IconResourcePath равно null, как в
первом элементе JumpTask, то берется программа-владелец списка переходов. Именно
поэтому для первого элемента JumpTask показывается значок Photo Gallery.

СОВЕТ
В файлах %WINDIR%\System32\shell32.dll и %WINDIR%\System32\imageres.dll
имеется много готовых значков, которые вполне можно использовать в элементах
JumpTask. Не гарантируется, что во всех версиях они одинаковы, но польза все равно
есть.
В третьем элементе JumpTask установлено свойство WorkingDirectory, которое влияет
на способ запуска программы (в данном случае Блокнота). В нем, как и в свойствах
ApplicationPath и IconResourcePath, можно использовать переменные окружения.
В последнем элементе JumpTask не только задается свойство Arguments, запускающее
браузер Internet Explorer в режиме «без надстроек», но и с помощью свойства
IconResourcelndex изменяется его значок. Именно поэтому на рис. 8.6 браузер
представлен значком с изображением домика, а не синим логотипом «е». В EXE- или
DLL-файл можно внедрить длинный список ресурсов значков. Если оставить значение
IconResourcelndex по умолчанию, то есть 0, то будет взят самый первый значок (тот,
что отображается в оболочке Windows). Если же в EXE- или DLL-файле имеются
дополнительные значки, то ими можно воспользоваться, задав значение
IconResourcelndex, большее нуля. Задав недопустимый индекс, вы получите
стандартный значок, как на рис. 8.5.

СОВЕТ
Если вы не хотите показывать значок рядом с именем элемента JuшpTask в списке
переходов, присвойте свойству IconResourcelndex значение -1. Этот способ работает
вне зависимости от того, задано свойство IconResourcePath или нет.

280

Глава 8.Особенности Windows 7

СОВЕТ
Чтобы поставить между элементами JumpTask горизонтальную линию, достаточно
добавить в нужное место элемент JumpTask, не задавая для него никаких свойств. На
рис. 8.7. показано, что получается, если добавить элементы  между
первыми двумя и последними двумя элементами в листинге 8.2.

Рис. 8.7. Добавление горизонтальных разделителей с помощью пустых элементов
JumpTask

Нестандартные категории
Рассматриваемое в этом разделе свойство CustomCategory определено не в классе
JumpTask, а в его базовом классе JumpItem. Присвоив ему непустую строку, вы
сможете поместить элемент в отдельную секцию с заголовком, отличным от
стандартного Tasks (Задачи).
В листинге 8.3 мы поместили один элемент в категорию One и два элемента - в
категорию Two. Результат изображен на рис. 8.8.
Листинг 8.3.Арр.хат1 - использование свойства CustomCategory











Puс. 8.8. Задание нестандартных категорий в списке переходов

ПРЕДУПРЕЖДЕНИЕ
Закрепление JumpTask не работает, если не задано свойство Arguments!
Из-за ошибки в Windows 7 задачи без аргументов закрепить невозможно. Кнопка
закрепления присутствует, но при ее нажатии ничего не происходит. К счастыо, у
большинства задач есть хотя бы один аргумент. Если необходимо запустить
программу, которой аргументы не нужны, а фиктивный аргумент передать
невозможно, то можно написать промежуточную программу запуска, которая
принимает и игнорирует аргумент.
Элементы, находящиеся в нестандартных категориях, автоматически поддерживают
закрепление и удаление пользователем (последняя возможность доступна с помощью
контекстного меню). Закрепленные элементы перемещаются в категорию Pinned
(Закреплено). Впоследствии пользователь может открепить элемент, как показано на
рис. 8.9.

282

Глава 8.Особенности Windows 7

Рис. 8.9. Закрепление элемента JumpTask из нестандартной категории

ПРЕДУПРЕЖДЕНИЕ
Нестандартные категории отображаются в порядке снизу вверх!
И элементы JumpTask, и нестандартные категории отображаются в том порядке, в
котором они хранятся в коллекции JumpItems. Но если список JumpTask растет
сверху вниз, то список категорий - снизу вверх! Именно поэтому категория Two на
рис. 8.8 и 8.9 находится над категорией One.

Элемент JumpPath
Если элемент JumpTask представляет программу, то JumpPath представляет файл,
который открывается приложением-владельцем списка. В действитедаиости
приложение может использовать элементы JumpPath, только если оно
зарегистрировано в Windows для обработки файлов с соответствующим расширением.
Чтобы выполнить примеры из этого раздела, необходимо временно зарегистрировать
приложение как обработчик JPG-файлов (в ходе экспериментов это можно сделать не
программно, а в Проводнике Windows, выбрав из контекстного меню файла пункты
Открыть с помощью->Вьібратьпрограмму(Ореп With->СhооseDefault Program).
В листинге 8.4 разметка из листинга 8.3 модифицирована - в коллекцию элементов
JumpTask добавлены элементы JumpPath (и те и другие элементы можно смешивать,
потому что они наследуют общему базовому классу JumpItem). Поскольку файл
существует на диске С: и приложение зарегистрировано для обработки JPG-файлов, то
список переходов теперь выглядит, как показано на рис 8.10. Если бы хотя бы одно из
вышеперечисленных условий не выполнялось, то список переходов выглядел бы, как
на рис. 8.8.

Списки переходов

283

Листинг 8.4. App.xaml - добавление JumpPath в разметку из листинга 8.3








Рис. 8.10. В категорию Photos списка переходов добавлен элемент JumpPath
По умолчанию элементы JumpPath помещаются в категорию Tasks, что выглядит
странновато. Однако их можно поместить и в другие категории, задав свойство
CustomCategory (унаследованное от JumpItem). Достоинство такого подхода в том, что
каждый элемент автоматически становится доступным для закрепления.
Когда пользователь щелкает по элементу DSC06397.jpg, запускается новый экземпляр
приложения-владельца, которому в качестве единственного аргумента командной
строки передается путь Path. Поэтому, если не считать значка и контекстного меню, то
элемент JumpPath в листинге 8.4 аналогичен следующему элементу JumpTask:

284

Глава 8.Особенности Windows 7



Обязанность учитывать переданный в командной строке аргумент н выполнять
«открытие» файла, что бы это ни означало, возлагается на само приложение - точно так
же, как в случае прочих элементов JumpTask.

ПРЕДУПРЕЖДЕНИЕ
В свойстве Path элемента JumpPath не поддерживаются переменные окружения!
Именно поэтому в листинге 8.4 путь к JPG-файлу зашит жестко. На практике, однако,
это не должно составлять серьезную проблему. Обычно приложения добавляют пути
динамически в процедурном коде, а в этом случае логика формирования пути может
быть произвольной (в том числе с применением переменных окружения).

Недавние и часто посещаемые пути JumpPath
В большинстве приложений - даже в зарегистрированных обработчиках определенных
типов файлов - нет причин явно манипулировать элементами JumpPath. Дело в том, что
списки переходов автоматически поддерживают две наиболее распространенных
категории: недавние элементы и часто посещаемые элементы.
Чтобы та или другая категория появилась в списке переходов, достаточно установить
для свойства ShowRecentCategory и/или ShowFrequentCategory элемента JumpList
значение true. Тогда соответствующая категория появится и будет заполняться
автоматически. Windows учитывает открытие файла, если оно было произведено
посредством диалогового окна Открытие файла (File Open) или в результате
использования зарегистрированной ассоциации с типом (например, после двойного
щелчка по файлу в Проводнике Windows или щелчка по элементу JumpPath).
Если вы хотите принудительно поместить элемент в какой-либо из этих списков
(например, потому, что приложение открывает файлы в обход вышеупомянутых
механизмов), то можете вызвать метод JumpList.AddToRecentCategory. У него есть
перегруженные варианты, принимающие путь в виде строки, объекта JumpPath и даже
объекта JumpTask. Не существует метода AddToFrequentCategory; для того чтобы файл
появился в списке часто посещаемых, необходимо достаточно много раз поместить его
в категорию недавних.
После добавления обеих категорий в список переходов из листинга 8.4 мы получим
результат, изображенный на рис. 8.11.



288

Риє. 8.11. Использование категорий Recent и Frequent
Разумеется, использовать обе категории одновременно не имеет особого смысла из-за
того, что оба списка сильно перекрываются. Как показано на рис. 8.1, в программе
Internet Explorer используется список часто посещаемых файлов (Frequent), тогда как в
большинстве других программ - список недавних файлов (Recent). (Windows 7
автоматически предоставляет категорию Recent приложениям, которые не работают со
списками переходов явно.)

Реакция на отказ от добавления или на удаление элемента из списка
переходов
Если приложение не зарегистрировано как обработчик файлов определенного типа или
файл не существует, то Windows отказывается добавлять путь JumpPath в коллекцию
JumpItems списка переходов, а может и удалить уже присутствующий в списке путь.
Чтобы предотвратить такое автоматическое удаление, следует обработать событие
JumpItemsRejected объекте JumpList.

286

Глава 8.Особенности Windows 7

Событие JumpItemsRejected генерируется один pas при удалении одного или нескольких элементов, но не раньше очередного обновления объекта JumpList, например
при следующем запуске приложения. Чтобы обработать это событие для списка
JumpList, определенного в XAML-разметке, следует присоединить обработчик к
XAML. Если объект JumpList создан в процедурном коде не забудьте присоединить
обработчик до вызова метода Apply.
Объект типа JumpItemsRejectedEventArgs, передаваемый
обработчику
события
содержит список всех отвергнутых элементов Jumpltem, а также список значений
перечисления JumpItemRejectionReason, а именно:

NoRegisteredHandler - приложение не зарегистрировано как обработчик файлов
данного типа.

InvalidItem - файл не существует (при работе в версии Windows, предшествующей Windows 7).

RemovedByUser - элемент удален пользователем.

None - причина отказа неизвестна.
Если вас интересуют только элементы, удаленные пользователем, то можно
ограничиться обработкой события JumpItemsRemovedByUser, вместе с которым
передается список удаленных элементов JumpItem. Это имеет смысл, например, для
того, чтобы узнать, когда пользователь удалил какие-то из добавленных вами задач
JumpTask. В этом случае следует прекратить добавлять такую задачу в список
переходов при последующих запусках программы.

КОПНЕМ ГЛУБЖЕ
О
моменте
возникновения
событий
JumpItemsRejected
и
JumpItemsRemovedByUser
Тот факт, что эти события генерируются лишь при следующем вызове метода
JumpList. Apply, вносит некоторую путаницу, но в этом плане WPF ограничена поведением используемых Shell Win32 API. Оболочка Windows Shell не предоставляет
средств для опроса текущего содержимого списка переходов и не позволяет заранее
узнать, будет ли добавление элемента в список принято или отвергнуто. Клиенты (в
частности, WPF) должны атомарно обновлять всю категорию. Windows либо примет,
либо отвергнет операцию; иногда при этом возвращается осмысленный код ошибки, а
иногда - нет. Кроме того, Windows применяет эвристическое правило - отвергает
элемент, если пользователь удалял его ранее, но только в случае, когда удаление было
произведено в интервале между текущей и предыдущей попыткой обновить список.
Метод Apply существует в классе JumpList для того, чтобы воспрепятствовать
добавлению объекта JumpTask или JumpPath, в котором установлена лишь часть
обязательных свойств. Может оказаться, что объект с частично заданными свойствами
недопустим или допустим, но перестает быть таковым после того, как заданы
недостающие свойства. После вызова метода Apply содержимое объекта WPF JumpList
соответствует тому, что оболочка считает принятым списком. События (одно или два)
генерируются внутри Apply, потому что только там WPF может узнать, что сделал
пользователь с момента последнего обновления списка переходов программой.

Настройка элементов на панели задач

287

Настройка элементов на панели задач
Начиная с версии WPF 4 в классе Window имеется свойство TaskbarItemInfo (типа
System.Windows.Shell.TaskbarItemInfо), которое позволяет настраивать значок
приложения на
панели задач или его эскиз. Например, чтобы добавить всплывающую
290
подсказку для эскиза приложения на панели задач, достаточно установить свойство
Description элемента TaskbarItemInfo следующим образом:





Или в программе на С#:
public MainWindow()
{
…
this.TaskbarItemInfo = new TaskbarItemInfo();
this.TaskbarItemInfo.Description = ‚Custom tooltip‛;
}

На рис. 8.12 показан результат этой операции. Разумеется, TaskbarItemInfo позволяет
задавать не только всплывающие подсказки.

Рис. 8.12. Всплывающая подсказка, заданная с помощью TaskbarItemInfo.Description
Индикатор выполнения для элемента на панели задач
Элементы на панели задач поддерживают встроенный индикатор выполнения. Он
бывает полезен, когда нужно отобразить состояние долго работающей задачи, не
отвлекая внимание пользователя. Эта возможность используется и в Проводнике
Windows, и в Internet Explorer. Очень удобно - работаешь в одной программе и время от
времени поглядываешь, что делается в другой.

288

Глава 8.Особенности Windows 7

Чтобы показать индикатор выполнения, достаточно задать два свойства объекта
TaskbarItemInfo: ProgressValue и ProgressState. ProgressValue может принимать значение
типа double от 0 (0%) до 1 (100%), показывающее процент заполненности полосы
индикатора. ProgressState может принимать следующие значения, принадлежащие
перечислению TaskbarItemProgressState:

Normal - показывать зеленый индикатор.

Paused - показывать желтый индикатор.

Error - показывать красный индикатор.

Indeterminate - показывать зеленый анимированный индикатор, а не стандартную
частично заполненную полосу, отражающую значение ProgressValue.

None - не показывать индикатор. Это значение по умолчанию.
Первые три значения приводят к отображению «обычной» полосы индикатора,
различия только в цвете. Желтый означает, что программа приостановлена, а красный
свидетельствует об ошибке. Впрочем, интерпретация цветов целиком в ваших руках.
Так, ничто не мешает сообщать о продолжении работы, даже если свойство
ProgressState равно Paused.
Значение Indeterminate свойства ProgressState подходит для случая, когда вы не знаете,
как далеко продвинулось выполнение. В этом состоянии значение ProgressValue
игнорируется, а вместо него показывается стандартная анимация.
Изменять свойства ProgressState и ProgressValue можно в любой момент, и результат
сразу же отражается на индикаторе выполнения. На рис. 8.13 показаны все пять
значений ProgressState в предположении, что ProgressValue равно .85.

Рис. 8.13. Пять значений ProgressState, которые поддерживает индикатор
выполнения, связанный с элементом на панели задач

Наложения для элементов на панели задач
Помимо индикаторов выполнения, элементы на панели задач поддерживают наложение
маленьких изображений поверх основного значка для передачи дополнительной
информации о состоянии. В классе TaskbarItemInfo эта возможность реализуется с
помощью свойства Overlay типа ImageSource (этот класс рассматривается в
последующих главах).
На рис. 8.14 показано, что происходит после следующего задания наложения.

Настройка элементов на панели задач

289





292

overlay.png

Результат наложения

Рис. 8..14.. Наложенное изображение и его представление на панели задач
Если установлен режим показа мелких значков на панели задач, то наложение не
поддерживается, то есть установка этого свойства ничего не дает. Кроме того, попытка
воспользоваться любой функциональностью класса TaskbarItemInfo ни к чему не
приводит, если приложение работает в версии Windows, предшествующей Windows 7.
Наложенное изображение постепенно появляется в правом нижнем углу. А когда
свойству Overlay присваивается значение null, наложение столь же постепенно
пропадает.

СОВЕТ
При смене изображений в свойстве Overlay эффект плавного затухания не
используется. Поэтому быстрое изменение значения этого свойства может создать
эффект анимации!

Настройка содержимого эскиза
По умолчанию эскиз, отображаемый при задержке указателя мыши над элементом на
панели задачи, - это просто уменьшенное изображение текущего окна приложения.
Класс Taskbarltemlnfo позволяет чуть изменить это поведение. Установив свойство
ThumbnailClipMargin (типа Thickness), можно обрезать эскиз по умолчанию.
На рис. 8.15 демонстрируется одно из возможных применений этой функции.
Программа Photo Gallery могла бы установить значение ThumbnailClipMargin (и
корректировать его при изменении размера окна) при просмотре одной фотографии —
чтобы убрать обрамление и привлечь внимание к основному содержимому.

290

Глава 8.Особенности Windows 7

Рис. 8.15. Обрезка эскиза по размеру самой фотографии, а не всего окна

Добавление кнопок управления к эскизу на панели задач
И последнее, что позволяет сделать класс TaskbarItemInfo, - поместить под эскизом
кнопки, имитирующие интерфейс Windows Media Player: Воспроизведение/Пауза,
Предыдущая, Следующая. Для этой цели предназначено свойство ThumbButtonInfos коллекция объектов типа ThumbButtonInfo.
Хотя ThumbButtonInfo не наследует классу WPF UIElement, он обладает основными
свойствами, ожидаемыми от кнопки. Единственное ограничение состоит в том, что
содержимое может быть только объектом типа ImageSource. В классе ThumbButtonInfo
имеется свойство ImageSource, определяющее содержимое, свойство Description,
задающее всплывающую подсказку, и событие Click. (Однако, в отличие от класса
Button, событие Click не маршрутизируется.) Кроме того, в классе ThumbButtonInfo
имеется свойство Command и дополняющие его свойства CommandTarget и Command
Parameter, поэтому кнопки могут использоваться в сочетании с применяемыми в
приложении командами.
В классе ThumbButtonInfo есть также стандартное свойство Visibility, способное
принимать любое из трех обычных значений. (Приятная неожиданность, если учесть,
что компоновка WPF здесь не применяется.) И ряд булевских свойств: IsEnabled,
IsInteractive, IsBackgroundVisible и DismissWhenClicked; все они, кроме последнего, по
умолчанию равны true. Под «фоном* (background), упоминаемым в названии свойства
IsBackgroundVisible, понимается обрамление кнопки, никакого настраиваемого фона у
этих кнопок не существует.
На рис. 8.16 показан результат применения следующей разметки в программе Photo
Gallery:



Настройка элементов на панели задач

291





294








Рис. 8.16. В состав эскиза можно включить кнопки управления

ПРЕДУПРЕЖДЕНИЕ
Во внимание принимаются лишь первые семь элементов ThumbButtonlnfo!
Поскольку в окне эскиза есть место только для семи кнопок управления, все
последующие элементы коллекции ThumbButtonInfos игнорируются. Но тут есть
тонкий момент - дополнительные кнопки игнорируются даже в случае, когда для
некоторых из первых семи свойство Visibility равно Collapsed (то есть теоретически
есть место для других кнопок). Поэтому» если вы хотите динамически изменять
состав кнопок, общее число которых превышает семь, то должны будете добавлять и
удалять элементы коллекции, а не просто манипулировать свойством Visibility.

292

Глава 8.Особенности Windows 7

FAQ
Как настроить изменение цвета элемента на панели задач при наведении
указателя мыши?
Единственный способ — изменить цвета самого значка. Windows выбирает
доминирующий цвет для значка и на его основе определяет цвет подсветки.

Функция Aero Glass
Aero Glass - это размытое, прозрачное обрамление окна, которое можно распространить
на клиентскую область. Впервые эта функция появилась в Windows Vista. Чтобы
воспользоваться ею в WPF-приложении, проще всего вызывать функцию
DwmExtendFrameIntoClientArea из Win32 API. (Префикс Dwm означает Desktop
Window Manager.) Это позволяет сделать все окно Window похожим на стеклянный
лист (как показано на рис. 8.17) или распространить «стекловидность» на заданную
часть клиентской области, примыкающую к любой из четырех сторон окна (как
показано на рис. 8.18). В любом случае поверх стекла можно располагать WPFсодержимое, как если бы окно было закрашено сплошным цветом.

Рис. 8.17. Стеклянный фон для окна Windows

Функция Aero Glass

293

296

Рис. 8.18. Распространение стекла только на нижнюю часть окна
В программе на Visual C++ вызвать функцию DwmExtendFramelntoClientArea можно
непосредственно. Но в языках типа C# или Visual Basic необходимо использовать
технологию PInvoke (конкретно - задать атрибут DllImport). PInvoke - ключ к вызову из
C# любой функции API из группы Desktop Window Manager. В листинге 8.5 приведены
сигнатуры PInvoke и простой служебный метод, обертывающий обращения к PInvoke.
Листинг 8.5. Использование функции Aero Glass из программы на C#
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
public MARGINS(Thickness t)
{
Left = (int)t.Left;
Right = (int)t.Right;
Top = (int)t.Top;
Bottom = (int)t.Bottom;
}
public int Left;
public int Right;
public int Top;
public int Bottom;
}
public class GlassHelper
{

294

Глава 8.Особенности Windows 7

[DllImport(‚dwmapi.dll‛, PreserveSig=false)]
static extern void DwmExtendFrameIntoClientArea(
IntPtr hWnd, ref MARGINS pMarInset);
[DllImport(‚dwmapi.dll‛, PreserveSig=false)]
static extern bool DwmIsCompositionEnabled();
public static bool ExtendGlassFrame(Window window, Thickness margin)
{
if (!DwmIsCompositionEnabled())
return false;
IntPtr hwnd = new WindowInteropHelper(window).Handle;
if (hwnd == IntPtr.Zero)
throw new InvalidOperationException(
‚The Window must be shown before extending glass.‛);
// Устанавливаем прозрачный фон - как с точки зрения WPF, так и в Win32
window.Background = Brushes.Transparent;
HwndSource.FromHwnd(hwnd).CompositionTarget.BackgroundColor =
Colors.Transparent;
MARGINS margins = new MARGINS(margin);
DwmExtendFrameIntoClientArea(hwnd, ref margins);
return true;
}

Метод GlassHelper.ExtendGlassFrame принимает объект Window и уже знакомый нам
объект Thickness; последний определяет части клиентской области, примыкающие к
каждой из четырех сторон, на которые следует распространить стекло. (Чтобы
получить эффект стеклянного листа, нужно для всех четырех сторон указать значение 1.) Проверив, что включен режим композиции рабочего стола (необходимое условие для
применения Aero Glass), программа создает по объекту Thickness структуру MARGINS,
получения которой ожидает функция DwmExtendFrameIntoClientArea, и вызывает эту
функцию, передавая ей соответствующий описатель HWND. Свойству Background
объекта Window присваивается значение Transparent, чтобы стекло было прозрачным.
Дополнительные сведения об использованной здесь технике см. в главе 19
«Интероперабельность с другими технологиями».
Любое
WPF-окно
Window
может
воспользоваться
методом
GlassHelperExtendGlassFrame следующим образом:
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
// Это нельзя делать до возникновения события Sourcelnitialized:
GlassHelper.ExtendGlassFrame(this, new Thickness(-1));
// Присоединяем оконную процедуру, чтобы впоследствии
// можно было понять, включен ли режим композиции рабочего стола

Функция Aero Glass

295

IntPtr hwnd = new WindowInteropHelper(this).Handle;
HwndSource.FromHwnd(hwnd).AddHook(new HwndSourceHook(WndProc));
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool
handled)
{
298
if (msg == WM_DWMCOMPOSITIONCHANGED)
{
// Повторно включаем эффект стекла:
GlassHelper.ExtendGlassFrame(this, new Thickness(-1));
handled = true;
}
return IntPtr.Zero;
}
private const int WM_DWMCOMPOSITIONCHANGED = 0x031E;

Этот метод следует вызывать не только на этапе инициализации, но и в случае, когда
режим композиции стола был сначала выключен, а потом снова включен. Это может
произойти как вследствие явного действия пользователя, так и в результате работы
программ типа Remote Desktop (Удаленный рабочий стол). Чтобы получать
уведомления об изменениях режима композиции рабочего стола, необходимо
перехватить сообщение Win32 WM_DWMC0MP0SITI0NCHANGED. Если хотите
лучше разобраться в работе этого кода, обратитесь к главе 19.
На рис. 8.19 показан результат использования этого кода в программе Photo Gallery.

Рис. 8.19. Программа Photo Gallery с эффектом стекла

296

Глава 8.Особенности Windows 7

Функция TaskDialog
Часто у разработчика возникает искушение использовать MessageBox там, где уместнее
выглядело бы нестандартное диалоговое окно. Но человеку свойственно лениться,
поэтому в Windows Vista появился новый, улучшенный вариант класса MessageBox TaskDialog, - который обладает большей гибкостью. Он соответствует облику
современных версий Windows и даже позволяет выполнять глубокую настройку с
применением дополнительных элементов управления.
Чтобы воспользоваться этой функциональностью, следует вызвать функцию TaskDialog
из Win32 API. Как и при работе с Aero Glass, ключом к вызову этой функции является
технология PInvoke. В листинге 8.6 показаны сигнатура PInvoke для самой функции
TaskDialog и связанных с ней типов.
Листинг 8.6. Сигнатуры TaskDialog и относящихся к ней типов на языке C#
[DllImport(‚comctl32.dll‛, PreserveSig=false, CharSet=CharSet.Unicode)]
static extern TaskDialogResult TaskDialog(IntPtr hwndParent, IntPtr hInstance,
string title, string mainInstruction, string content,
TaskDialogButtons buttons, TaskDialogIcon icon);
enum TaskDialogResult
{
Ok=1,
Cancel=2,
Retry=4,
Yes=6,
No=7,
Close=8
}
[Flags]
enum TaskDialogButtons
{
Ok = 0x0001,
Yes = 0x0002,
No = 0x0004,
Cancel = 0x0008,
Retry = 0x0010,
Close = 0x0020
}
enum TaskDialogIcon
{
Warning = 65535,
Error = 65534,
Information = 65533,
Shield = 65532
}

Функция TaskDialog

287

В отличие от MessageBox, функция TaskDialog позволяет задать текст основного
сообщения, визуально отделенный от прочего содержимого. Кроме того, можно
задавать произвольную комбинацию кнопок. На рис. 8.20 и 8.21 показаны различия
между окнами MessageBox и TaskDialog, формируемыми следующим кодом:
300

// Используем MessageBox
result = MessageBox.Show("Are you sure you want to delete " + filename + "?",
"Delete Picture", MessageBoxButton.YesNo, MessageBoxImage.Warning);
// Используем TaskDialog
result = TaskDialog(new System.Windows.Interop.WindowInteropHelper(this).Handle,
IntPtr.Zero, "Delete Picture",
"Are you sure you want to delete " + filename + "?",
"This will delete the picture permanently, rather than sending it
➥to the Recycle Bin.",
TaskDialogButtons.Yes | TaskDialogButtons.No, TaskDialogIcon.Warning);

Рис. 8.20. Окно MessageBox Windows 7 выглядит старомодным и недостаточно
проработанным

Puc. 8.21. Аналогичное окно TaskDialog куда дружелюбнее

298

Глава 8.Особенности Windows 7

ПРЕДУПРЕЖДЕНИЕ
Для работы с TaskDialog необходима версия 6 библиотеки Windows Common
Controls DLL (ComCtl32.dll)!
Из соображений совместимости эта версия не подключается по умолчанию. Один из
способов подключить версию 6 к своему приложению состоит в том, чтобы
поместить в один каталог с исполняемым файлом файл манифеста (с именем
[YourAppNameJ.exe.manifest), который содержит следующую разметку:



Your description







Манифест можно также внедрить в сам исполняемый файл в виде ресурса Win32 (с
именем RT_MANIFEST и ID, равным 1), если вы не хотите включать в дистрибутив
еще один файл. Эту работу можно поручить Visual Studio, сославшись на файл
манифеста в свойствах проекта.
Если с приложением не связана шестая версия библиотеки стандартных диалоговых
окон, то обращение к функции TaskDialog приведет к возбуждению исключения с
сообщением "Unable to find an entry point named ‗TaskDialog‘ in DLL ―comctl32.dll‘. Я
рекомендую связывать приложение с этой версией, даже если вы не собираетесь
использовать TaskDialog. В противном случае любой стандартный элемент
управления Win32, например MessageBox, отображается в старом стиле и в новых
версиях ОС выглядит плохо.

СОВЕТ
Для проведения глубокой настройки TaskDialog можно также воспользоваться более
сложной функцией TaskDialogIndirect. В Windows SDK имеются примеры
использования этой и других функций Win32 в приложениях .NET. Сигнатуры
PInvoke для наиболее популярных функций Win32 API можете посмотреть также на
сайте http://pinvoke.net.

Резюме

299

Резюме
В этой главе рассмотрены новейшие достижения в области пользовательского
интерфейса, включенные в ОС Windows 7, и некоторые интересные усовершенствования,302появившиеся еще в Windows Vista. К счастью, WPF предоставляет
полноценную поддержку для использования этих средств в XAML и процедурном
.NET-совместимом языке. Для использования возможностей, добавленных в Windows
Vista, необходимо применять технологию PInvoke, которая позволяет обращаться к
неуправляемым функциям из Win32 API. Впрочем, доступ к базовой функциональности
из управляемого кода все равно несложен.
В этой главе мы рассмотрели все средства Windows 7, к которым есть простой
интерфейс из WPF. Однако это лишь малая толика того, что было включено в Win32
API в версии Windows 7 (и Windows Vista). Мы не будем пытаться поведать обо всех
хитростях интероперабельности с неуправляемым кодом, необходимых для доступа к
некоторым из этих средств, а вместо этого порекомендуем загрузить пакет Windows
API Code Pack с сайта http://code.msdn.microsoft.com/WindowsAPICodePack. Этот пакет
содержит классы и примеры, упрощающие работу со многими новыми функциями
Windows 7 и Windows Vista из управляемого кода. Рассматриваются самые разные
области — от настройки оболочки и панели задач до датчиков, лингвистических служб
и управления питанием.

СОВЕТ
Даже если вы пока не готовы к переносу своего приложения на WPF 4, то все равно
можете воспользоваться описанными в этой главе функциями Windows 7, прибегнув
к помощи библиотеки WPF Shell Integration Library, которая находится по адресу
http://code.msdn.microsoft.com/WPFShell.
Эта библиотека содержит совместимую с .NET Framework 3.5 версию классов из
пространства имен System.Windows.Shell в WPF 4. Между двумя API есть некоторые
мелкие несовместимости (например, в библиотеке для версии 3.5 Taskbar- ItemInfо это присоединенное свойство, а не обычное свойство зависимости), но в целом
библиотека удачно прокладывает путь для последующего перехода на новую версию
WPF.

СОВЕТ
Планируя воспользоваться средствами, имеющимися в конкретной версии Windows,
всегда нужно думать о том, что делать, если программа будет запущена в более
ранней версии (если, конечно, вы собираетесь эти более ранние версии
поддерживать).

300

Глава 8.Особенности Windows 7

В части списков переходов и средств работы с панелью задач в пространстве имен
System.Windows.Shell WPF сама заботится о поддержке более старых версий Windows.
Если запустить рассмотренные в этой главе примеры в Windows Vista, то код,
работающий с классами JumpList, TaskbarItemInfo и пр., будет выполняться без
ошибок, но и без видимого эффекта.
Что же касается прямых обращений к неуправляемым функциям через PInvoke, то вы
должны явно проверять версию Windows и соответственно адаптировать поведение
программы. В .NET для проверки версии операционной системы можно использовать
свойство System.Environment.OSVersion. Например:
if (System.Environment.OSVersion.Version.Major >= 6)
// Windows Vista или более поздняя, используем TaskDialog
else
// Младше Windows Vista, используем MessageBox
Основной и дополнительный номер версии Windows 7 равен 6.1, a Windows Vista –
6.0.

304

306

III
Элементы управления

Глава 9 «Однодетные элементы управления»
Глава 10 «Многодетные элементы управления»
Глава 11 «Изображения, текст и другие элементы управления

9
Однодетные элементы управления




Кнопки
Простые контейнеры
Контейнеры с заголовками

Ни один современный презентационный каркас нельзя считать полным без
стандартного набора элементов управления, из которых можно быстро собрать
традиционный пользовательский интерфейс. И в дистрибутиве Windows Presentation
Foundation тоже есть множество таких элементов. С некоторыми из них мы уже
встречались в предыдущих главах. А в этой части книги мы проведем обзор
большинства встроенных элементов управления, уделив пристальное внимание их
уникальным особенностям.
На приведенных в этой книге рисунках показано, как выглядят элементы управления
WPF в теме Aero, имеющейся в Windows 7 и Windows Vista. Но у большинства
элементов WPF есть и другое обличье, подразумеваемое по умолчанию. Дело в том, что
WPF поставляется с DLL-библиотекой тем, которая содержит шаблоны элементов для
следующих тем Windows:
Aero (подразумевается по умолчанию в Windows 7 и Windows Vista)
Luna (подразумевается по умолчанию в Windows ХР)
 Royale (малоизвестная тема из Windows ХР Media Center Edition 2005 и Windows ХР
Tablet PC Edition 2005)
 Classic (имеется в Windows 2000 и более поздних версиях)
На рис. 9.1 показан внешний вид кнопки WPF во всех поддерживаемых темах Windows.
Если WPF встречает неподдерживаемую тему, например тему Zune, выпущенную
Microsoft в 2006 году, то вместо нее использует тему Classic.
Тема Aero
Тема Luna
ТемаRoyale
Тема Classic



Рис. 9.1. Внешний вид кнопки WPF в различных темах

304

Глава 9.Однодетные элементы управления

В большинстве случаев различия во внешнем виде малозаметны. Конечно, любому
элементу управления можно придать радикально отличающийся облик (основанный на
текущей теме или вообще не зависящий от темы) с помощью шаблонов, которые
обсуждаются в главе 14 «Стили, шаблоны, обложки и темы».
Встроенные в WPF элементы управления можно грубо разбить на следующие
категории, основанные на иерархии наследования:
Однодетные элементы управления (эта глава)
Многодетные элементы управления (глава 10 «Многодетные элементы
управления»)

Диапазонные элементы управления (глава 11 «Изображения, текст и другие
элементы управления»)

Все остальное (глава 11)
В этой главе мы рассмотрим однодетные элементы управления, то есть элементы,
которые могут содержать всего один объект. Все они являются классами,
производными от System.Windows.Controls.ContentControl, и имеют свойство Content
типа Object, которое может содержать только один объект (одним из таких элементов
является кнопка Button, продемонстрированная в главе 2 «Все тайны XAML»).
Поскольку содержимое однодетного элемента управления может быть произвольным
объектом, то такой элемент, в принципе, может содержать большое дерево объектов.
Но непосредственный потомок может быть только один. Помимо Content, в классе
ContentControl есть еще один интересный член - булевское свойство HasContent. Оно
возвращает false, если Content равно null, и true в противном случае.
Есть три основных разновидности однодетных элементов управления:

Кнопки

Простые контейнеры

Контейнеры с заголовками



FAQ
Почему в классе ContentControl определено свойство HasContent? Ведь
сравнение Content==null ничуть не сложнее, чем HasContent==false!
Добро пожаловать в мир WPF API, который не всегда выглядит, как привычные .NET
API! С точки зрения языка C# свойство HasContent избыточно. Но с точки зрения
XAML оно очень полезно. Например, с его помощью гораздо проще реализовать
триггер свойства, который устанавливает различные значения свойства, когда
HasContent становится равным true.

Кнопки

305

Класс Window, который мы рассматривали в главе 7 «Структурирование и
развѐртывание приложения», также является однодетным элементом управления. Его
свойству Content обычно присваивается ссылка на какую-нибудь панель Panel,
например Grid, так что внутри может находиться сколь угодно сложный
пользовательский интерфейс.

КОПНЕМ ГЛУБЖЕ
Свойство Content и произвольные объекты
Раз значением свойства Content может быть любой управляемый объект, то, естественно, возникает вопрос, что произойдет, если в роли содержимого выступает
невизуальный объект, например Hashtable или TimeZone. Ответ довольно прост: если
содержимое является объектом класса, производного от UIElement, то оно
визуализируется методом OnRender этого класса. Иначе, если к элементу применим
шаблон данных (см. главу 13 «Привязка к данным»), этот шаблон может определять
визуализацию от имени данного объекта. Или же вызывается метод ToString объектасодержимого и возвращенный им текст рисуется внутри элемента TextBlock.

Кнопки
Кнопки, - пожалуй, самый знакомый и неотъемлемый элемент пользовательского
интерфейса. Элемент WPF Button, изображенный на рис. 9.1, уже неоднократно
встречался на страницах этой книги.
Хотя интуитивно каждый понимает, что такое кнопка, точное определение (по крайней
мере, в WPF) может показаться неочевидным. Базовая кнопка - это однодетный элемент
управления, по которому можно щелкнуть, но нельзя щелкнуть дважды. Это поведение
инкапсулировано в абстрактном классе ButtonBase, которому наследуют несколько
разных элементов управления.
В классе ButtonBase имеется событие Click и определено, что понимается под
«щелчком». Как и для обычных кнопок Windows, щелчок может возникать в результате
нажатия и последующего отпускания левой кнопки мыши или нажатия клавиши Enter
либо пробела, если кнопка владеет фокусом.
В классе ButtonBase определено также булевское свойство IsPressed на случай, если вам
вдруг понадобится выполнить какие-то действия, когда кнопка нажата (то есть левая
кнопка мыши или клавиша пробела нажата, но еще не отпущена).
Однако самым интересным свойством ButtonBase является ClickMode. Оно может
принимать значения, определенные в перечислении ClickMode, и управляет тем, при
каких условиях генерируется событие Click. Возможные значения: Release (по
умолчанию), Press и Hover. Хотя изменение ClickMode для стандартной кнопки может

306

Глава 9.Однодетные элементы управления

лишь вызвать у пользователя недоумение, эта возможность весьма полезна для кнопок
с измененным стилем, которые и на кнопку-то совсем не похожи. В таких случаях
принято ожидать, что нажатие объекта дает тот же результат, что и щелчок по нему.

КОПНЕМ ГЛУБЖЕ
Click и другие события
Чтобы сгенерировать событие Click, класс ButtonBase анализирует такие более
примитивные события, как MouseLeftButtonDown и MouseLeftButtonUp. Если
ClickMode равно Release или Press, то ни одно из этих примитивных событий не
всплывает дальше элемента, производного от ButtonBase, потому что ButtonBase присваивает свойству MouseButtonEventArgs.Handled значение true. В режиме Hover по
той же причине не всплывают события MouseEnter и MouseLeave. Если вы все же
хотите обрабатывать примитивные события мыши для элемента, производного от
ButtonBase,
то
должны
либо
обрабатывать
Preview-версию
события
(PreviewMouseLeftButtonDown, PreviewMouseLeftButtonUp и т.д.), либо присоединить
свой обработчик в процедурном коде с помощью перегруженного варианта метода
AddHandler, который игнорирует пометку события как обработанного.
Классу ButtonBase прямо или опосредованно наследуют несколько элементов
управления, которые мы рассмотрим ниже.

Button

RepeatButton

ToggleButton

CheckBox

RadioButton
Существуют и другие производные от ButtonBase элементы, но они предназначены для
использования внутри конкретных составных элементов управления, таких как
Calendar или DataGrid.

Класс Button
Класс Button в WPF добавляет к тому, что уже дает ButtonBase, два простых понятия:
кнопка отмены и кнопка по умолчанию. Оба удобны для применения в диалоговых
окнах. Если для некоторой кнопки, находящейся внутри диалогового окна (то есть окна
Window, показанного методом ShowDialog), свойство Button.IsCancel равно true, то при
нажатии этой кнопки окно автоматически закрывается и свойство DialogResult
принимает значение false. Если свойство Button.IsDefault равно true, то нажатие клавиши

Кнопки

307

Enter приводит к активизации этой кнопки, если только у нее явно не отобран фокус

FAQ
В чем разница между свойствами IsDefault и IsDefaulted класса Button?
Свойство IsDefault доступно для чтения и записи и определяет, будет ли данная
кнопка считаться кнопкой по умолчанию. С другой стороны, свойство IsDefaulted (с
неудачно выбранным именем) предназначено только для чтения. Оно показывает,
что кнопка по умолчанию в данный момент находится в таком состоянии, что
нажатие клавиши Enter приведет к ее активизации. Другими словами, IsDefaulted
равно true только при выполнении следующих условий: IsDefault равно true и
фокусом владеет либо сама кнопка по умолчанию, либо элемент TextBox (для
которого AcceptsReturn равно false). Последнее условие позволяет клавише Enter
нажать кнопку по умолчанию, не покидая элемент TextBox.

Класс RepeatButton
Класс RepeatButton ведет себя так же, как Button, но продолжает генерировать событие

FAQ
Как нажать кнопку из программы?
Классу Button, как и многим другим элементам управления WPF, соответствует
класс в пространстве имен System.Windows.Automation.Peers, предназначенный для
поддержки автоматизации пользовательского интерфейса: ButtonAutomationPeer. Для
кнопки с именем myButton его можно использовать следующим образом:
ButtonAutomationPeer bap = new ButtonAutomationPeer(myButton);
IInvokeProvider iip = bap.GetPattern(PatternInterface.Invoke)
as IInvokeProvider;
iip.Invoke(); // Это обращение приводит к нажатию кнопки

В подобных классах автоматизации есть несколько членов, которые чрезвычайно
полезны для автоматизации тестирования и предоставления дополнительных
возможностей.
Click до тех пор, пока кнопка нажата. (Кроме того, в нем нет понятия кнопки по
умолчанию и кнопки отмены, так как он наследует непосредственно ButtonBase.)
Частота генерации событий Click зависит от значений свойств Delay и Interval, которые
по
умолчанию
равны
SystemParameters.KeyboardDelay
и
SystemParameters.KeyboardSpeed соответственно. Внешне элемент RepeatButton
выглядит точно так же, как Button (см. рис. 9.1).
Поведение RepeatButton на первый взгляд может показаться странным, но оно полезно
(и является стандартным) для кнопок, которые при каждом нажатии увеличивают или
уменьшают некоторое значение. Например, кнопки на концах полосы прокрутки
демонстрируют именно такое поведение, когда вы щелкаете по ним и не отпускаете

308

Глава 9.Однодетные элементы управления

кнопку мыши. Другой пример: если бы вы захотели написать элемент «числовой
счетчик» (пока не включенный в WPF), то, наверное, использовали бы пару элементов
RepeatButton для управления числовым значением. Класс RepeatButton находится в
пространстве имен System.Windows.Controls.Primitives, поскольку, скорее всего, он
найдет применение только в составных элементах управления, а не сам по себе.

Класс ToggleButton
ToggleButton - это «залипающая» кнопка, которая сохраняет свое состояние после
нажатия (понятия кнопки по умолчанию и кнопки отмены для нее тоже не определены).
При первом щелчке свойство IsChecked становится равным true, при следующем возвращается в false. По умолчанию ToggleButton выглядит точно так же, как Button и
RepeatButton.
В классе ToggleButton имеется также свойство IsThreeState; если оно равно true то
IsChecked может принимать три разных значения: true, false или null. На самом деле
свойство IsChecked принадлежит типу Nullable (bool? в языке С#). В
трехпозиционном режиме первый щелчок устанавливает для IsChecked значение true,
второй - null, третий - false и т. д. Чтобы изменить порядок перехода состояний, можно
либо перехватывать щелчки, обрабатывая Preview-версии событий мыши, и вручную
присваивать свойству IsChecked требуемое значение, либо создать свой подкласс и
переопределить в нем метод OnToggle класса ToggleButton, реализовав в нем нужную
вам логику.
Помимо свойства IsChecked, в классе ToggleButton определено по одному событию для
каждого значения IsChecked: Checked для true, Unchecked для false и Indeterminate для
null. Может показаться странным, что не определено единственное событие
IsCheckedChanged, но, как выясняется, наличие трех отдельных событий удобно в
случае декларативной разметки.
Как и RepeatButton, класс ToggleButton находится в пространстве имен System.
Windows.Controls.Primitives, то есть разработчики WPF полагали, что такими кнопками
не будут пользоваться без дополнительной настройки. Однако же они вполне
естественно выглядят на панели инструментов ToolBar, о чем будет рассказано в главе
10.

Класс CheckBox
Элемент управления CheckBox, изображенный на рис. 9.2, всем хорошо знаком. Но
постойте... ведь этот раздел вроде бы посвящен кнопкам, разве не так? Так-то оно так,
но давайте приглядимся к характерным особенностям элемента CheckBox в WPF:

У него имеется единственный вложенный элемент, задаваемый в разметке (в
отличие от стандартного флажка).

Для него определено понятие «нажатия» с помощью мыши или клавиатуры.

После нажатия он сохраняет состояние: отмечен или сброшен.

Кнопки

309

Он поддерживает трехпозиционный режим, в котором состояние циклически
переключается между вариантами «отмечен», «неизвестно» и «сброшен».
Ничего не напоминает? А должно бы, поскольку СhескВох — не что иное, как элемент
ToggleButton с измененным внешним видом! Класс СhескВох наследует ToggleButton и
переопределяет лишь стиль по умолчанию, используя визуальные элементы,
показанные на рис. 9.2.


Рис. 9.2. Все три состояния элемента управления WPF CheckBox

КОПНЕМ ГЛУБЖЕ
Поддержка клавиатуры в классе CheckBox
Класс CheckBox поддерживает одно дополнительное поведение, отсутствующее в
ToggleButton, - в целях совместимости с малоизвестной особенностью флажков в
Win32. Если флажок CheckBox владеет фокусом, то нажатие клавиши плюс (+)
отмечает его, а клавиши минус (-) - сбрасывает! Отметим, что это работает, только
если свойство IsThreeState равно false.

Класс RadioButton
RadioButton - еще один элемент управления, производный от ToggleButton. Его
уникальная особенность заключается в поддержке взаимного исключения. Если
несколько элементов RadioButton помещены в одну группу, то в любой момент
времени отмеченным может быть только один из них. Включение какого-то одного
переключателя RadioButton — даже из программы - автоматически выключает все
остальные переключатели в той же группе. На самом деле у пользователя даже нет
возможности выключить какой-то элемент RadioButton, щелкнув по нему; выключение
возможно только из программы. Таким образом, элемент RadioButton предназначен для
формулирования вопроса, имеющего несколько вариантов ответа. На рис. 9.3
изображен стандартный внешний вид RadioButton.
Редко используемое неопределенное состояние элемента RadioButton (IsThreeState=true и IsChecked=null) аналогично выключенному в том смысле, что пользователь
не может установить такое состояние щелчком по элементу; это можно сделать только
из программы. Щелчок по RadioButton переводит его во включенное состояние, но если
какой-то переключатель в той же группе находится в неопределенном состоянии, то он
в таком состоянии и остается.

310

Глава 9.Однодетные элементы управления

Рис. 9.3. Все три состояния элемента управления WPF RadioButton
Поместить несколько переключателей RadioButton в одну группу очень просто. По
умолчанию все переключатели, имеющие одного и того же непосредственного
логического родителя, автоматически попадают в одну группу. Например, если задана
такая разметка:

Option 1
Option 2
Option 3


то в любой момент времени включенным может быть только один переключатель.
Но если требуется сгруппировать переключатели каким-то особым образом, то можно
воспользоваться свойством GroupName, значением которого является строка. Все
переключатели с одним и тем же значением GroupName помещаются в одну группу
(при условии, что у них общий логический корень). Это позволяет группировать
переключатели, принадлежащие разным родителям, например:


Option 1
Option 2


Option 3



Можно даже создавать подгруппы в пределах одного родителя:



GroupName="A">Option 1
GroupName="A">Option 2
GroupName="B">A Different Option 1
GroupName="B">A Different Option 2

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

Простые контейнеры

311

Простые контейнеры
В имеется несколько встроенных однодетных элементов управления, для которых, в
отличие от кнопки, не определено понятие щелчка. У каждого из них есть свои
уникальные особенности. Перечислим эти элементы:

Label

ToolTip

Frame

Класс Label
Label (метка) - классический элемент управления, который, как и в предшествующих
технологиях, может содержать текст. Но, поскольку это однодетный элемент
управления в смысле WPF, то в свойстве Content может находиться произвольное
содержимое - Button, Menu и т. д., - хотя на практике метка все-таки используется в
основном для представления текста.
WPF позволяет размещать текст на экране разными способами, например в элементе
TextBlock. Уникальная особенность метки - поддержка клавиш доступа. В тексте метки
можно выделить одну букву, так что нажатие соответствующей ей комбинации клавиш
доступа - эта буква в сочетании с клавишей Alt - будет обрабатываться особым
образом. Точнее, можно назначить произвольный элемент, который получит фокус при
нажатии этой комбинации. Чтобы выделить букву (которая в зависимости от настроек
Windows может быть подчеркнута), достаточно поместить перед ней знак
подчеркивания. А чтобы назначить соответствующий целевой элемент, нужно задать
свойство метки Target (типа UIElement).

СОВЕТ
Такие элементы управления, как Label и Button, поддерживают клавиши доступа путем
специальной обработки знака подчеркивания перед буквой, как, например, в названиях _0реп
или Save_As. (В Win32 и Windows Forms для этой цели применяется знак амперсанда [&], но
подчеркивание значительно удобнее в контексте XML.) Если требуется, чтобы в тексте
присутствовал сам знак подчеркивания, то надо записать его два раза подряд, например
__Open или Save __As.

Классическое применение клавиш доступа для метки - сопоставление ей поля ввода
TextBox. Например, следующая XAML-разметка передает фокус элементу TextBox при
нажатии сочетания клавиш Alt+U:



Присваивание значения свойству Target неявно приводит к вызову конвертера типа
NameReferenceConverter, который был описан в главе 2. В C# можно просто записать в
свойство ссылку на объект TextBox (в предположении, что метка называется
userNameLabel):

312

Глава 9.Однодетные элементы управления

userNameLabel.Target = userNameBox;

Класс ToolTip
Элемент управления ToolTip (всплывающая подсказка) представляет свое содержимое
в плавающем прямоугольнике, который появляется, когда пользователь наводит
указатель мыши на ассоциированный элемент, и исчезает когда указатель покидает
пределы этого элемента. На рис. 9.4 показано типичное употребление элемента ToolTip,
созданного в соответствии со следующей XAML-разметкой:


Элемент ToolTip нельзя помещать в дерево элементов UIElement непосредственно. Он
должен быть присвоен свойству ToolTip отдельного элемента (это свойство определено
в классах FrameworkElement и FrameworkContentElement).
Clicking this will submit your request.

Puc. 9.4. Элемент WPF ToolTip

СОВЕТ
Если задается свойство ToolTip некоторого элемента, то элемент ToolTip можно вообще опустить! Это свойство имеет тип Object, и если присвоить ему ссылку на любой объект, отличный от ToolTip, то система автоматически создаст объект ToolTip и
в качестве его содержимого будет использовать значение свойства. Поэтому XAMLкод, соответствующий рис. 9.4, можно упростить следующим образом:


и даже так:


FAQ
Как сделать, чтобы всплывающая подсказка появлялась при задержке
указателя мыши над неактивным элементом?
Просто воспользуйтесь присоединенным свойством ShowOnDisabled из класса
ToolTipService.
В XAML это будет выглядеть следующим образом:


А в C# следует вызвать статический метод, соответствующий присоединенному
свойству:
ToolTipService.SetShowOnDisabled(myButton, true);

FAQ
Как принудительно закрыть всплывающую подсказку?
Присвоить false ее свойству IsOpen.

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

Простые контейнеры

307

Например, распространение свойств вниз по дереву элементов (обычный механизм
наследования свойств в WPF) прекращается по достижении фрейма. Во многих
отношениях элемент Frame в WPF ведет себя как HTML-фрейм.
Раз уж речь зашла о HTML, то претензии Frame на всеобщее признание связаны с тем,
что он может визуализировать не только WPF-, но и HTML-содержимое. В классе
Frame имеется свойство Source типа System. Uri, которому можно присвоить ссылку на
любую страницу HTML (или XAML). Например:


СОВЕТ
При использовании Frame для переходов между веб-страницами не забывайте обрабатывать событие NavigationFailed, которое происходит при возникновении
ошибки, и устанавливать для свойства NavigationFailedEventArgs.Handled значение
true. В противном случае необработанное исключение (например, WebException)
будет возбуждаться в другом потоке. Объект NavigationFailedEventArgs, который
передается обработчику, среди прочего предоставляет доступ к объекту исключения.
В главе 7 было отмечено, что Frame - это навигационный контейнер со встроенным
механизмом запоминания истории, пригодный для отображения как HTML-, так и
XAML-содержимого. Поэтому можно считать элемент Frame более гибкой версией
элемента управления ActiveX Microsoft Web Browser или обертывающего его элемента
WPF WebBrowser.

СОВЕТ
По сравнению с фреймами Frame элемент управления WPF WebBrowser (появившийся в WPF 3.5 SP1) предлагает более развитые средства для работы с HTML. Он
поддерживает визуализацию HTML-разметки, представленной строкой в памяти или
потоком Stream, а также интерактивное взаимодействие с HTML DOM и сценарии.
Кроме того, он дает возможность включать Silverlight-содержимое в WPFприложение: достаточно задать URL, указывающий на ХАР-файл Silverlight.
Отметим, что WebBrowser не является однодетным элементом управления; он не
может содержать другие элементы WPF в качестве непосредственных потомков.
К сожалению, когда Frame содержит HTML-разметку, на него распространяется
несколько ограничений, не применяемых к другим элементам управления WPF (из-за
того, что для визуализации HTML используются механизмы Win32). Например, HTMLсодержимое всегда рисуется поверх WPF-содержимого, к нему нельзя применять
эффекты, невозможно изменить его свойство Opacity и т.д. Элемент Frame также не
поддерживает визуализацию произвольной строки или потока HTML; содержимое
должно быть задано в виде пути или URL-адреса автономного файла. Если необходимо
отображать строки держащие НТМL-код, то лучше воспользоваться элементом
управления WPF WebBгowseг.

316

Глава 9.Однодетные элементы управления

КОПНЕМ ГЛУБЖЕ
Свойство Content элемента Frame
Хотя Frame - однодетный элемент управления и имеет свойство Content, он не рассматривает его как свойство содержимого в смысле XAML. Иными словами, элемент
Frame в XAML не поддерживает наличие дочернего элемента. Свойство Content
необходимо задавать явно следующим образом:




В классе Frame это достигается за счет пометки пустым атрибутом
ContentPropertyAttribute,
который
отменяет
пометку
атрибутом
[ContentPropertyC'Content")] базового класса ContentControl. Но зачем это сделано?
По словам разработчиков WPF, для того, чтобы предотвратить некорректное использование свойства Content класса Frame, потому что типичным и ожидаемым
способом работы с фреймом является задание свойства Source, указывающего на
внешний файл. А единственная причина, по которой Frame сделан однодетным
элементом управления, - необходимость сохранить совместимость с классом
NavigationWindow, который рассматривался в главе 7. Отметим, что если заданы оба
свойства Source и Content, то предпочтение отдается Content.

Контейнеры с заголовками
Все рассмотренные выше однодетные элементы управления добавляют к своему
содержимому очень простые визуальные элементы по умолчанию (обрамление кнопки,
квадратик флажка и т. д.). Следующие два элемента управления в этом отношении
слегка отличаются, потому что добавляют к основному содержимому настраиваемый
пользователем заголовок. Они наследуют подклассу ContentControl, который
называется HeaderedContentControl и добавляет свойство Header типа Object.

Класс GroupBox
GroupBox - хорошо знакомый элемент для организации групп элементов управления.
На рис. 9.6 показан элемент GroupBox, охватывающий несколько флажков CheckBox.
Он создан из следующей XAML-разметки:


Check grammar as you type
Hide grammatical errors in this document
Check grammar with spelling



Контейнеры с заголовками

317

Puc. 9.6. Элемент WPF GroupBox
Как правило, GroupBox применяется для охвата нескольких элементов, но поскольку
это однодетный элемент управления, то у него может быть всего один
непосредственный потомок. Поэтому обычно дочерним элементом GroupBox делают
какой-то промежуточный элемент, способный содержать несколько потомков. Для этой
цели идеально подходит панель, например StackPanel.
Как и Content, свойство Header может ссылаться на произвольный объект, и если это
объект класса, производного от UIElement, то отображается он естественным образом.
Например, если сделать Header кнопкой Button (см. ниже), то получится результат,
показанный на рис. 9.7:





Check grammar as you type
Hide grammatical errors in this document
Check grammar with spelling



Puc. 9.7. Элемент GroupBox с кнопкой Button в заголовке - еще одна демонстрация
гибкости модели содержимого в WPF
На рис. 9.7 находящаяся в заголовке кнопка вполне функциональна. Она может
получать фокус, ее можно нажимать и т. д.

318

Глава 9.Однодетные элементы управления

Класс Expander
Элемент Expander может заинтриговать, потому что это единственный из
318
Глава 9.управления,
Однодетные элементы
управлении
рассматриваемых в данной
главе элементов
не имеющий
аналогов в
предшествующих технологиях конструирования пользовательских интерфейсов, в том
числе в Windows Forms! Expander очень похож на GroupBox, но содержит кнопку,
которая позволяет сворачивать и разворачивать внутреннее содержимое (по умолчанию
в начальный момент содержимое свернуто!.
На рис. 9.8 показаны оба состояния элемента Expander. Этот элемент создав с помощью
той же разметки, что и показанный на рис. 9.6, только открывающий и закрывающий
теги GroupBox заменены тегами Expander:


Check grammar as you type
Hide grammatical errors in this document
Check grammar with spelling



Свернуто

Развернуто

Puc. 9.8. Элемент WPF Expander
В классе Expander определено свойство IsExpanded и события Expanded/Collapsed.
Кроме того, он позволяет задать направление развертывания (Up, Down, Left, Right) с
помощью свойства ExpandDirection.
Кнопка внутри Expander в действительности представляет собой элемент ToggleButton
с измененным стилем. Помимо Expander, еще в нескольких составных элементах
используются примитивные элементы управления, такие как ToggleButton и
RepeatButton.

Резюме
Никогда еще кнопка не была такой гибкой! В WPF элемент Button, как и все остальные
однодетные элементы управления, может содержать все что угодно - при условии, что
есть только один непосредственный потомок. Ну а теперь, завершив обзор элементов
управления содержимым, перейдем к элементам, которые могут содержать несколько
потомков, - многодетным элементам (items controls).

10





Многодетные элементы управления
Общая функциональность
Селекторы
Меню
Другие многодетные элементы управления

Помимо элементов управления содержимым, в WPF есть еще одна крупная категория
элементов управления — многодетные элементы, которые могут содержать в качестве
непосредственных потомков коллекции объектов, а не один-единственный объект. Все
многодетные элементы управления наследуют абстрактному классу ItemsControl,
который, как и ContentControl, является прямым подклассом Control.
В классе ItemsControl содержимое хранится в свойстве Items (типа ItemCollection).
Коллекция может состоять из объектов произвольного типа, по умолчанию они
визуализируются так же, как если бы находились внутри однодетного элемента. Иными
словами, объекты типа UIElement визуализируются естественным образом, а все
остальные типы (без учета шаблонов данных) - в виде текстового блока Text Blocк,
содержащего строку, которую возвращает метод ToString.
Встречавшийся ранее элемент ListBox — пример многодетного элемента управления. В
предыдущих главах мы добавляли в список только объекты типы ListBoxItem, а вот в
следующем примере добавим произвольные объекты:



1/1/2012
1/2/2012
1/3/2012


(Поскольку мы использовали конструкцию sys:DateTime вместо х:DateTime, то эта
разметка будет работать в качестве как автономного, так и откомпилированного
ХАМL-кода.)

320

Глава 10.Многодетные элементы управления

Дочерние элементы неявно добавляются в коллекцию Items, потому что Items свойство содержимого. Этот список визуализируется, как показано на рис. 10.1. Оба
элемента UIElement (Button и Expander) отображаются нормально и полностью
интерактивны. Три объекта DateTime представляются в соответствии с тем, что
возвращает их метод ToString.

Рис. 10.1. Список ListBox, содержащий произвольные объекты
В главе 2 «Все тайны XAML» отмечалось, что свойство Items доступно только для
чтения. Это означает, что в первоначально пустую коллекцию можно добавлять
объекты, из нее можно удалять объекты, но нельзя записать в Items ссылку на
совершенно другую коллекцию. В классе ItemsControl имеется еще одно свойство ItemsSouгcе, - которое поддерживает заполнение элемента объектами из уже
существующей произвольной коллекции. Использование свойства ItemsSource более
подробно рассматривается в главе 13 «Привязка к данным».

СОВЕТ
Чтобы не усложнять изложение, мы в этой главе заполняем многодетные элементы
управления только визуальными элементами. Однако предпочтительным является другой
подход: поместить в многодетный элемент невизуальные элементы (например,
специализированные бизнес-объекты) и с помощью шаблонов данных определить способ
их визуализации. Подробнее шаблоны данных обсуждаются в главе 13.

Общая функциональность
Помимо свойств Items и ItemsSource, в классе ItemsControl есть еще несколько
интересных свойств, а именно:

HasItems - доступное только для чтения булевское свойство, упрощающее
анализ наличия элементов в коллекции из декларативного ХАМL-кода. В
программе на C# можно использовать это свойство или просто проверить
значение Items.Count.

IsGrouping — еще одно булевское свойство, доступное только для чтения.
Информирует о том, разбиты ли объекты, входящие в элементы управления на
группы верхнего уровня. Группировка производится прямо в классе
ItemsCollection, который включает несколько свойств для управления группами
и присвоения им имен. Подробнее о группировке вы узнаете в главе 13.

Общая функциональность

321



AlternationCount и Alternationlndex — эти два свойства позволяют задать чередующиеся стили объектов-потомков в зависимости от индекса в коллекции.
Например, если AlternationCount равно 2, то элементам с четным индексом будет
назначен один стиль, а элементам с нечетным индексом- другой. Пример
использования этих свойств приведен в главе 14 «Стили, шаблоны, обложки и
темы».

DisplayMemberPath — строковое свойство; в него можно занести имя свойства
каждого объекта (или более сложное выражение), которое изменяет порядок его
визуализации.

ItemsPanel - свойство, позволяющее изменить способ организации объектов
внутри многодетного элемента управления, не заменяя полностью его шаблон.
В следующих двух разделах последние два свойства описываются более подробно.

DisplayMemberPath
На рис. 10.2 показано, что происходит, если в рассмотренном выше списке ListBox
задать свойство DisplayMemberPath:



1/1/2012
1/2/2012
1/3/2012


Рис. 10.2. Тот же список ListBox, что на рис. 10.1, но добавлено свойство
DisplayMemberPath, равное DayOfWeek
Если DisplayMemberPath равно DayOfWeek, то WPF отображает не сам объект, а
значение его свойства DayOfWeek (день недели). Поэтому-то на рис. 10.2 объекты типа
DateTime представлены как Sunday, Monday и Tuesday. (Это основанная на методе
ToString визуализация значений перечисления DayOfWeek - типа свойства
DayOfWeek.) Поскольку в классах Button и Expander нет свойства DayOfWeek. то они
отображаются в виде пустых текстовых блоков TextBlock.

322

Глава 10.Многодетные элементы управления

КОПНЕМ ГЛУБЖЕ
Путь к свойству в WPF
Свойство DisplayMemberPath поддерживает синтаксис так называемого пути к
свойству, который используется в WPF в нескольких местах, в частности для
привязки к данным и анимации. Основная идея состоит в том, чтобы записать
последовательность из одного или нескольких свойств, которую можно было бы
использовать в процедурном коде для получения требуемого значения. Простейший
пример пути - имя одного свойства, но если значением свойства является составной
объект, то можно обратиться к его свойствам (и т. д.), разделяя отдельные имена
свойств точками, как принято в С#. Этот синтаксис поддерживает даже индексаторы
и массивы.
Представьте, к примеру, объект, в котором определено свойство FirstButton типа
Button, причем в текущий момент значением свойства Content кнопки является
строка ‖0К". Тогда значение этой строки ("ОК") можно представить таким путем к
свойству:
FirstButton.Content

А следующий путь к свойству представляет длину этой строки (2):
FirstButton.Content.Length

А такой путь - первый символ строки ('О'):
FirstButton.Content[0]

Эти выражения вполне соответствуют синтаксису С#, разве что приводить типы не
требуется.

ItemsPanel
Как и в случае всех остальных элементов управления WPF, смыслом многодетных
элементов является не их внешний вид, а способ хранения нескольких объектов, а часто
также способ логического выбора объектов. Внешний облик всех элементов
управления WPF можно изменить, применив другой шаблон, но для многодетных
элементов есть и более короткий способ - заменить лишь часть шаблона, отвечающую
за организацию хранящихся в нем объектов. Этот мини-шаблон, который еще
называют внутренней панелью (items panel), позволяет подменить панель,
применяемую для организации объектов, оставив все прочие аспекты элемента
управления неизменными.
В качестве внутренней панели разрешается использовать любую из панелей,
рассмотренных в главе 5 «Компоновка с помощью панелей» (и вообще любой класс,
производный от Panel). Например, список ListBox по умолчанию располагает
хранящиеся в нем объекты вертикально, но следующий XAML-код подставляет вместо
этой панели WrapPanel, как в примере программы Photo Gallery из главы 7
«Структурирование и развертывание приложения»:

Общая функциональность

323








Перевод этого XAML-кода на процедурный язык нетривиален, поэтому покажем, как
решить ту же задачу в С#:
FrameworkElementFactory panelFactory = new
FrameworkElementFactory(typeof(WrapPanel));
myListBox.ItemsPanel = new ItemsPanelTemplate(panelFactory);

А вот пример подстановки нестандартной панели FanCanvas, которую мы реализуем в
главе 21 «Компоновка с помощью нестандартных панелей»:







На рис. 10.3 показан результат применения этой разметки к приложению Photo Gallery
(попутно список ListBox обертывается элементом Viewbox) в случае, когда выбран
один объект. Несмотря на нестандартную внутреннюю компоновку, элемент ListBox
сохраняет все особенности поведения, касающиеся выбора объектов.

Рис.10.3. Список ListBox с нестандартной панелью FanCanvas в качестве ItemsPanel

324

Глава 10.Многодетные элементы управления

FAQ
Как заставить ListВох располагать свои объекты по горизонтали. а не по
вертикали?
По умолчанию в списке ListBox используется панель VirtualizingStackPanel, которая
располагает находящиеся в ней объекты по вертикали. Следующий код подменяет ее
другой панелью, VirtualizingStackPanel, в которой свойству Orientation явно
присвоено значение Horizontal:







СОВЕТ
В нескольких многодетных элементах управления для повышения производительности в качестве ItemsPanel по умолчанию используется панель VirtualizingStackPanel. В WPF 4 эта панель поддерживает новый режим, еще больше повышающий производительность прокрутки, но устанавливать его нужно явно. Для
этого
следует
присвоить
присоединенному
свойству
VirtuializingStackPanel.VirtualizationMode значение Recycling. В таком случае панель
повторно использует («рециклирует») контейнеры, в которых хранятся видимые на
экране объекты, а не создает новый контейнер для каждого объекта.
Если взглянуть на подразумеваемый по умолчанию шаблон для такого многодетного
элемента управления, как ListBox, то мы увидим элемент ItemsPresenter, задача
которого — выбрать подходящую панель ItemsPanel:








!

Присутствие ScrollViewer в шаблоне элемента по умолчанию объясняет, откуда берется
поведение прокрутки. Управлять тем, как осуществляется прокрутка в многодетном
элементе управления, позволяют различные присоединенные свойства элемента
ScrollViewer.

Селекторы

325

Управление поведением прокрутки
На примере списка ListBox приведем подразумеваемые по умолчанию значения
следующих свойств:
• ScrollViewer.HorizontalScrollBarVisibility - Auto
• ScrollViewer.VerticalScrollBarVisibility - Auto
• ScrollViewer.CanContentScroll - true
• ScrollViewer.IsDeferredScrollingEnabled - false
Если CanContentScroll равно true, то прокрутка производится пообъектно, если же false
- то попиксельно. Последний режим обеспечивает более плавную прокрутку, но не
гарантирует совмещения первого объекта с краем списка.
Если свойство IsDeferredScrollingEnabled равно false, то прокрутка производится
синхронно с перетаскиванием ползунка. Если же оно равно true, то содержимое
ScrollViewer обновляется только после прекращения перетаскивания, когда будет
отпущена кнопка мыши. Если в многодетном элементе управления используется
виртуализирующая панель и он содержит много сложных объектов, то установка для
IsDeferredScrollingEnabled значения true может дать существенный прирост
производительности за счет отказа от визуализации промежуточных состояний.
Например, Microsoft Outlook именно так прокручивает длинные списки.
Ниже приведен пример списка ListBox, в котором установлены все четыре
присоединенных свойства, чтобы изменить поведение ScrollViewer в подразумеваемом
по умолчанию шаблоне:


ListBox - разумеется, не единственный многодетный элемент управления. Многодетные
элементы можно разделить на три основных группы, которые обсуждаются в
следующих разделах: селекторы, меню и все остальные.

Селекторы
Селекторами называются многодетные элементы управления, объекты которых можно
индексировать и - что более важно - выбирать. Абстрактный класс Selector,
производный от ItemsControl, добавляет несколько свойств, необходимых для
поддержки выбора. Например, следующие три похожих свойства предназначены для
получения и установки текущего выбранного объекта:

326

Глава 10.Многодетные элементы управления

SelectedIndex — отсчитываемое от нуля целое число, равное индексу бранного
объекта, или -1» если ничего не выбрано. Объекты нумеруются в порядке добавления в
коллекцию.
• SelectedItem - сам выбранный объект.
• SelectedValue - значение выбранного объекта. По умолчанию оно совпадает с самим
объектом, то есть SelectedValue - то же самое, что и SelectedItem. Однако с помощью
свойства SelectedValuePath можно задать имя произвольного свойства или даже
выражение, которое будет представлять значение объекта (SelectedValuePath работает
аналогично DisplayMemberPath).
Все три свойства допускают чтение и запись, поэтому с их помощью можно не только
получать текущий выбранный объект, но и устанавливать его.
В классе Selector определены также два присоединенных свойства, применяемые к
отдельным объектам:
• IsSelected - булевское свойство, позволяющее выбрать или отменить выбор объекта
(либо узнать, в каком состоянии он сейчас находится).
• IsSelectionActive - доступное только для чтения булевское свойство, которое
сообщает, владеет ли выбранный объект фокусом.
В классе Selector имеется также событие SelectionChanged, которое позволяет получать
уведомления об изменении выбранного объекта. В главе 6 «События ввода: клавиатура,
мышь, стилус и мультисенсорные устройства» мы пользовались им для списка ListBox,
когда демонстрировали работу с присоединенными событиями.
В состав WPF входит пять элементов управления, производных от Selector:
• ComboBox
• ListBox
• ListView
• TabControl
• DataGrid
Мы опишем их в следующих разделах.
•

Элемент ComboBox
Изображенный на рис. 10.4 элемент ComboBox позволяет выбрать из списка один
объект. Он очень популярен, потому что занимает мало места на экране. В поле выбора
отображается только объект, выбранный в данный момент, а весь остальной список
раскрывается по требованию. Чтобы раскрыть или закрыть список, можно щелкнуть
мышью, а также нажать сочетание клавиш Alt+стрелка вверх, Alt+стрелка вниз либо
клавишу F4.
В классе ComboBox определены два события - DropDownOpened и DropDownClosed - и
свойство IsDropDownOpen. Все вместе они позволяют реагировать на раскрытие или
закрытие списка. Например, можно отложить заполнение ComboBox до момента
раскрытия списка - обработав событие DropDownOpened. Отметим, что свойство
IsDropDownOpen допускает чтение и запись, то есть с его помощью можно напрямую
управлять состоянием раскрытия.

Селекторы

327

Puс. 10.4. Элемент WPF ComboBox в раскрытом виде

Режимы работы поля выбора
Элемент ComboBox поддерживает режим, в котором пользователь может вводить в
поле выбора произвольный текст. Если текст совпадает с каким-то из присутствующих
в списке элементов, то этот элемент автоматически становится выбранным. В
противном случае ни один элемент не будет выбран, но введенный текст сохраняется в
свойстве Text элемента ComboBox, так что программа может получить к нему доступ.
Этот режим контролируется двумя неудачно названными свойствами - IsEditable и
IsReadOnly, - по умолчанию равными false. Кроме того, имеется свойство
StaysOpenOnEdit; если оно равно true, то список остается раскрытым, когда
пользователь щелкает по полю выбора (так ведут себя раскрывающиеся списки в
Microsoft Office в противоположность стандартным спискам Win32).
Если поле выбора является полем ввода, то выбранный объект можно отображать
только в виде простой строки. Это не страшно, если в списке ComboBox и так хранятся
строки (или однодетные элементы управления, содержащие строки). Но если в списке
находятся более сложные объекты, то необходимо сообщить ComboBox, как получить
их строковое представление.
В листинге 10.1 показана XAML-разметка элемента ComboBox, содержащего составные объекты. В каждом объекте отображается кадр презентации PowerPoint, так
что все вместе напоминает галерею в духе Microsoft Office, где имеется эскиз и краткое
описание объекта. Однако в типичной галерее Office поле выбора может содержать
только простой текст, а не выбранный элемент во всей полноте. На рис. 10.5 показан
результат визуализации разметки из листинга 10.1, а также то, как он будет выглядеть,
если установить для свойства IsEditable значение true.
Листинг 10.1. Список ComboBox с составными элементами, напоминающий галерею
Microsoft Office





Curtain Call

Whimsical, with a red curtain background that represents a stage.







Fireworks

Sleek, with a black sky containing fireworks. When you need to
celebrate PowerPoint-style, this design is for you!



…другие объекты…


IsEditable=False (по умолчанию)

IsEttitable=True

Puc. 10.5. По умолчанию установка для IsEditable значения true приводит к
отображению в поле выбора строки, возвращаемой методом ToString
Понятно, что выводить в поле выбора имя типа "System.Windows.Controls.StackPanel"
никуда не годится, и тут приходит на помощь класс TextSearch. В нем определены два
присоединенных свойства, позволяющих управлять тем, что отображается в
редактируемом поле выбора.

Селекторы

329

FAQ
В чем разница между свойствами IsEditable и IsReadOnly элемента ComboBox?
Если IsEditable равно true, то поле выбора ComboBox превращается в поле ввода.
Свойство IsReadOnly управляет тем, можно ли изменять текст в этом поле, - точно
так же, как свойство IsReadOnly элемента TextBox. Таким образом, поле IsReadOnly
не имеет смысла, если IsEditable не равно true, а тот факт, что IsEditable равно true,
еще не означает, что текст в поле выбора можно редактировать. В табл. 10.1 сведены
особенности поведения ComboBox при различных комбинациях этих свойств.
Таблица 10.1. Поведение ComboBox при всех возможных комбинациях свойств
IsEditable и IsReadOnly
IsEditable IsReadOnly
false
false

Описание
В поле выбора отображается копия выбранного объекта, и вводить туда произвольный текст запрещено
(это поведение по умолчанию)

false
true

true
false

true

true

То же, что и выше
В поле выбора отображается текстовое представление выбранного объекта, и разрешено вводить произвольный текст
В поле выбора отображается текстовое представление выбранного объекта, но вводить произвольный
текст запрещено

К элементу ComboBox можно присоединить свойство TextSearch.TextPath и тем самым
указать, какое свойство (или субсвойство) объекта отображать в поле выбора.
Механизм работы такой же, как у свойств DisplayMemberPath и Selected- ValuePath;
единственное различие заключается в способе использования конечного значения.
Для объектов в листинге 10.1 напрашивается решение отображать в поле выбора
содержимое первого текстового блока TextBlock, потому что оно содержит заголовок
("Curtain Call" или "Fireworks"). Поскольку этот элемент TextBlock вложен в две панели
StackPanel, то в пути к требуемому свойству нужно сначала упомянуть внутренний
элемент StackPanel (второй потомок каждого объекта), а уже потом сам TextBlock
(первый потомок внутренней панели StackPanel). Таким образом, присоединенное
свойство TextPath в случае листинга 10.1 будет выглядеть следующим образом:


330

Глава 10.Многодетные элементы управления

Однако такое решение слишком хрупко - путь к свойству перестанет работать, если
изменить структуру объекта. Не обрабатывается также случай разнородных объектов;
объекты, не соответствующие структуре TextPath, представляются в поле выбора
пустыми строками.
В классе TextSearch есть еще одно присоединенное свойство Text; оно более гибкое, но
применяться должно к индивидуальным объектам списка ComboBox Значением
свойства Text может быть литеральный текст, отображаемый в поле выбора для
данного элемента. В листинге 10.1 им можно воспользоваться следующим образом:



…
IsEditable=False (default) IsEditable=True



…другие объекты…


Можно одновременно задавать и свойство TextSearch.TextPath для ComboBox в целом,
и свойство TextSearch.Text для отдельных объектов. В таком случае TextPath дает
представление в поле выбора по умолчанию, a Text переопределяет его для тех
объектов, где присутствует.
На рис. 10.6 показан результат описанного выше задания свойств TextSearch. TextPath и
TextSearch.Text.

Рис. 10.6. Благодаря присоединенному свойству TextSearch получается список,
похожий на галерею Office

Селекторы

331

СОВЕТ
Можно
подавить
применение
свойства
TextSearch,
задав
свойство
IsTextSearchEnabled
элемента
ItemsControl
равным
false.
Свойство
IsTextSearchCaseSensitive, также определенное в классе ItemsControl (по умолчанию
равно false), указывает, надо ли при сравнении введенного текста с текстами
присутствующих в списке объектов принимать во внимание регистр букв.

СОВЕТ
Как получить новый выбранный объект в обработчике события
SelectionChanged?
Событие SelectionChanged предназначено для элементов управления, допускающих
выбор нескольких объектов, поэтому для селектора типа СоmboВох, позволяющего
выбрать только один объект, работать с ним не очень удобно. Передаваемый
обработчику события объект типа SelectionChangedEventArgs имеет два свойства
типа IList: Addedltems и Removedltems. Свойство Addedltems содержит множество
вновь выбранных объектов, а свойство Removedltems — множество ранее выбранных
объектов. Если разрешено выбирать только один объект, то получить его можно
следующим образом:
void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
object newSelection = e.AddedItems[0];
}

Но не следует заранее предполагать, что какие-то элементы вообще выбраны (в
приведенном выше коде это проверяется)! Мало того что выбор элемента в списке
ComboBox можно отменить программно, так еще это может сделать и пользователь,
если IsEditable равно true, a IsReadOnly - false. Если в этом случае пользователь
введет в поле выбора значение, не совпадающее ни с одним из присутствующих в
списке объектов, то событие SelectionChanged произойдет, но коллекция Addedltems
будет пустой.

Класс ComboBoxItem
Класс ComboBox неявно обертывает каждый хранящийся в нем объект объектом
ComboBoxItem. (В этом можно убедиться, написав программу, которая пройдет вверх
по визуальному дереву, начиная с любого из объектов списка.) Но это можно сделать и
явно (кстати, ComboBoxItem - однодетный элемент управления). В листинге 10.1 это
будет выглядеть следующим образом:




332

Глава 10.Многодетные элементы управления






…другие объекты…

Отметим, что свойство TextSearch.Text теперь нужно присоединять к элементу
ComboBoxItem, поскольку StackPanel больше не является самым внешним элементом
хранимого объекта. Придется также модифицировать значение свойства
TextSearch.TextPath, записав в него путь Content.Children[1].Children[0].Text.

FAQ
А зачем мне самому обертывать объекты в ComboBoxItem?
В классе ComboBoxItem есть полезные свойства — IsSelected и IsHighlighted - и полезные события - Selected и Unselected. Применение ComboBoxItem позволяет также
избежать странного поведения при показе однодетных элементов управления в поле
выбора (когда IsEditable равно false): если объектом в списке ComboBox является
однодетный элемент управления, то в поле выбора показывается не весь элемент, а
только его свойство Content. Если же содержательный объект обернут в
ComboBoxItem (тоже однодетный элемент), то содержимым последнего будет как раз
исходный объект, который и требовалось показать.
Так как ComboBoxItem - однодетный элемент управления, то его удобно использовать для добавления в список ComboBox простых строк (вместо того, чтобы обертывать их в TextBlock или Label). Например:

Item 1
Item 2


Элемент ListBox
Уже знакомый нам элемент управления ListBox аналогичен ComboBox, только все
объекты отображаются прямо в области, занятой элементом (если они не помещаются,
то появится полоса прокрутки). На рис. 10.7 показан элемент ListBox, содержащий те
же объекты, что были определены в листинге 10.1.
Пожалуй, самая важная особенность ListBox состоит в том, что он поддерживает выбор
нескольких объектов. Этим режимом управляет свойство SelecttionMode, которое
может принимать три значения (определенных в перечислении SelectionMode):.

Single - одновременно может быть выбран только один объект, как ComЬоВох.
Это значение по умолчанию.

Селекторы

333

Рис. 10.7. Элемент WPF ListBox


Multiple - одновременно может быть выбрано несколько объектов. Щелчок по
невыбранному объекту добавляет его в коллекцию SelectedItems, а щелчок по
выбранному объекту удаляет его из этой коллекции.

Extended - одновременно может быть выбрано несколько объектов, но поведение
оптимизировано для выбора одного объекта. Чтобы в этом режиме выбрать
несколько объектов, следует во время щелчка мышью удерживать нажатой
клавишу Shift (чтобы выбирать соседние элементы) или Ctrl (чтобы выбирать
произвольные, необязательно соседние элементы). Точно так же ведет себя
элемент управления ListBox в Win32.
Как у ComboBox имеется компаньон ComboBoxItem, так и у ListBox есть компаньон
ListBoxItem. С этим классом мы уже встречались в предыдущих главах. На самом деле
ComboBoxItem наследует классу ListBoxItem, в котором и определены свойство
IsSelected и события Selected и Unselected.

КОПНЕМ ГЛУБЖЕ
Свойства ListBox и множественный выбор
Хотя в классе ListBox имеется свойство Selectedltems, которым можно пользоваться
вне зависимости от режима SelectionMode, он также наследует от класса Selector
свойства SelectedIndex, SelectedItem и SelectedValue, не укладывающиеся в модель
множественного выбора.
Если выбрано несколько объектов, то свойство SelectedItem просто указывает на
первый элемент в коллекции SelectedItems (то есть тот, которым был выбран первым), а свойства SelectedIndex и SelectedValue возвращают индекс и значение этого
объекта. Впрочем, для элементов управления, поддерживающих множественный
выбор, этими свойствами лучше не пользоваться. Отметим, что в классе ListBox не
определены свойства SelectedIndices и SelectedValues.

334

Глава 10.Многодетные элементы управления

СОВЕТ
Прием с использованием свойства TextSearch, продемонстрированный ранее для
элемента ComboBox, сохраняет актуальность и для ListBox. Например, если
объекты на рис. 10.7 аннотировать соответствующими значениями TextSearch.Text,
то при нажатии клавиши F в момент, когда ListBox владеет фокусом, выбранным
станет объект Fireworks. Если бы TextSearch не было задано, то нажатие клавиши S
привело бы к передаче фокуса списку, потому что S - первая буква в строке
System.Windows.Controls.StackPanel. (И такое поведение показалось бы
пользователю странным!)

FAQ
Как добиться плавной прокрутки ListBox?
По умолчанию ListBox прокручивается пообъектно. Поскольку шаг прокрутки
рассчитывается на основе высоты объекта, то в случае больших объектов прокрутка
может происходить рывками. Чтобы список прокручивался плавно, с шагом в
несколько пикселов, не зависящим от высоты объектов, проще всего присвоить
значение false свойству ScrollViewer.CanContentScroll, присоединенному к элементу
ListBox, как было показано в предыдущей главе.
Однако имейте в виду, что в таком режиме теряется возможность виртуализации
списка. Под виртуализацией понимается оптимизация создания дочерних элементов они создаются только в момент, когда оказываются видны на экране. Виртуализация
возможна только в случае, когда для создания объектов, хранящихся в списке,
применяется привязка к данным, поэтому установка для свойства CanContentScroll
значения false может негативно сказаться на производительности работы списка,
привязанного к данным.

СОВЕТ
Как отсортировать объекты в списке ListBox (да и в любом другом элементе
типа ItemsControl)?
В основе сортировки лежит механизм, реализованный в классе ItemsCollection,
поэтому он равным образом применим ко всем элементам, производным от
ItemsControl. В классе ItemsCollection имеется свойство SortDescriptions. Это кол
лекция,
которая
может
содержать
сколько
угодно
объектов
типа
System.ComponentModel.SortDescription. Каждый такой объект описывает одно
свойство, по которому производится сортировка, а также направление сортировки по возрастанию или по убыванию. Например, следующий код сортирует
последовательность объектов ContentControl по их свойству Content:
// Сначала очищаем имеющиеся описания сортировки
myItemsControl.Items.SortDescriptions.Clear();
// Затем сортируем no свойству Content
myItemsControl.Items.SortDescriptions.Add(
new SortDescription(‚Content‛, ListSortDirection.Ascending));

Селекторы

335

СОВЕТ
Как снабдить объекты в элементе ItemsControl идентификаторами
автоматизации, видными в инструментальных средствах, например в
программе UI Spy?
Самый простой способ снабдить любой элемент, производный от FrameworkElement,
идентификатором автоматизации — установить его свойство Name, поскольку именно оно по
умолчанию применяется для целей автоматизации. Но если вы хотите назначить элементу
идентификатор, отличный от его имени, то просто запишите желаемое значение в присоединенное
свойство AutomationProperties. AutomationlD (из пространства имен System.Windows. Automation).

Элемент ListView
Элемент управления ListView, производный от ListBox, выглядит и ведет себя, как
ListBox, с тем отличием, что по умолчанию установлен режим Extended SelectionMode.
Однако класс ListView добавляет также свойство View, которое расширяет
возможности настройки внешнего вида, не ограничиваясь одним лишь выбором
нестандартной панели ItemsPanel.
Свойство View принадлежит типу ViewBase, абстрактному классу. В состав WPF
входит один конкретный подкласс этого класса, GridView. По умолчанию он очень
похож на вид Таблица (Details) в Проводнике Windows. (На самом деле в бета-версиях
WPF класс GridView даже назывался DetailsView.)
На рис. 10.8 представлен простой элемент ListView, созданный из следующей XAMLразметки, в которой предполагается, что префикс sys соответствует пространству имен
System из сборки mscorlib.dll:








1/1/2012
1/2/2012
1/3/2012


В классе GridView имеется свойство содержимого Columns, в котором хранится
коллекция объектов GridViewColumn, а также другие свойства, управляющие
поведением заголовков столбцов. В WPF определен элемент ListViewItem, производный от ListBoxItem. В данном случае объекты DateTime неявно обернуты
элементами ListViewItem, поскольку явно это не указано.

336

Глава 10.Многодетные элементы управления

Puc. 10.8. Элемент управления WPF ListView с видом GridView
Объекты, хранящиеся в списке ListView, описываются в виде простого списка, как и в
случае ListBox, поэтому ключом к отображению разных данных в различных столбцах
служит свойство DisplayMemberBinding класса GridView.Column. Идея в том, что в
каждой строке ListView может находиться составной объект, а в столбцах
отображаются свойства или субсвойства этого объекта. Но, в отличие от свойства
DisplayMemberPath, определенного в классе ItemsContlrol, для работы со свойством
DisplayMemberBinding необходима привязка к данным (см. главу 13).
Интересно, что GridView автоматически поддерживает кое-какие специальные
возможности табличного вида Проводника Windows, а именно:

Разрешается менять порядок столбцов путем перетаскивания их заголовков.

Разрешается изменять размеры столбцов путем перетаскивавших разделителей.

Двойной щелчок по разделителю столбцов приводит к автоматической подгонке
их размера под размер содержимого столбца.
Однако GridView не поддерживает автоматическую сортировку щелчком по заголовку
столбца, что, безусловно, является досадным упущением. Код сортировки объектов в
результате щелчка по заголовку столбца совсем не сложен (достаточно воспользоваться
вышеупомянутым свойством SortDescriptions), но вот рисовать внутри заголовка
стрелочку, индицирующую факт и направление сортировки, придется самостоятельно.
В общем и целом, ListView с видом GridView - сильно урезанный вариант элемента
DataGrid. Но теперь, когда в WPF 4 появился настоящий элемент DataGrid, нужда в
GridView сильно поуменьшилась.

Элемент TabControl
Следующий селектор, TabControl, полезен для переключения между страницами
содержимого. На рис. 10.9 показано, как выглядит элемент TabControl в простейшем
случае. Обычно вкладки располагаются вдоль верхнего края, но свойство
TabStripPlacment (типа Dock) позволяет разместить их слева (Left), справа (Right) или
снизу (Bottom).
Работать с TabControl просто. Нужно лишь поместить внутрь него какие-нибудь
объекты - и каждый объект автоматически окажется на отдельной вкладке. Например:

Селекторы

337


Content for Tab 1.
Content for Tab 2.
Content for Tab 3.


Puc. 10.9. Элемент управления WPF TabControl
Как ComboBox со своим ComboBoxItem, ListBox со своим ListBoxItem и т. д., элемент
TabControl неявно обертывает каждый объект элементом типа Tabltem. Впрочем,
маловероятно, что вы когда-нибудь захотите добавлять объекты типа, отличного от
Tabltem, непосредственно в TabControl, потому что без Tabltem у соответствующей
вкладки не будет метки. Например, на рис. 10.9 представлена визуализация следующей
XAML-разметки:

Content for Tab 1.
Content for Tab 2.
Content for Tab 3.


TabItem -однодетный элемент управления с заголовком, поэтому Header может быть
произвольным объектом — так же как в случае GroupBox или Expander.
В отличие от других селекторов, первый элемент Tabltem по умолчанию оказывается
выбранным. Однако в программе можно сделать все вкладки невыбранными, записав
значение null в свойство SelectedItem или значение -1 в свойство SelectedIndex.

Элемент DataGrid
DataGrid - весьма гибкий элемент управления для отображения данных в виде таблицы
с несколькими столбцами, допускающей сортировку, редактирование и многое другое.
Он оптимизирован для связывания с таблицей базы данных в памяти (например, типа
System. Data. DataTable из ADO.NET). Мастера Visual Studio и такие технологии, как
LINQ to SQL, предельно упрощают такое связывание.
В листинге 10.2 показана XAML-разметка элемента DataGrid, который содержит
коллекцию из двух объектов следующего типа Record:

338

Глава 10.Многодетные элементы управления

public class Record
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Uri Website { get; set; }
public bool IsBillionaire { get; set; }
public Gender Gender { get; set; }
}

где перечисление Gender определено следующим образом:
public enum Gender
{
Male,
Female
}

Пять столбцов данных, показанные на рис. 10.10 (по одному для каждого свойства
объекта Record), определены в коллекции Columns.
Листинг 10.2. Элемент DataGrid со статически определенными данными и столбцами
разных типов








292 CHAPTER 10 Items Controls
From the Library of Wow! eBook














Селекторы

339

Puc. 10.10. Элемент WPF DataGrid, описанный в листинге 10.2
Элемент DataGrid автоматически поддерживает изменение порядка и размера столбцов
и сортировку по столбцам, но любую возможность можно отключить, установив
значение false для некоторых из следующих свойств: CanUserReorderColumns,
CanUserResizeColumns, CanllserResizeRows и CanUserSortColumns. Свойства
GridLinesVisibility и HeadersVisibility позволяют отключить показ линий сетки и
заголовков соответственно.
В листинге 10.2 демонстрируются основные типы столбцов, поддерживаемые
элементом DataGrid:

DataGridTextColumn - идеален для представления строк, поскольку в обычном
режиме используется элемент TextBlock, а в режиме редактирования - элемент TextBox.

DataGridHyperlinkColumn - представляет обычный текст в виде гиперссылки, по
которой можно щелкнуть. Отметим, однако, что со щелчком не ассоциируется никакое
поведение по умолчанию (например, открытие браузера). Эти действия вы должны
обрабатывать явно.

DataGridCheckBoxColumn - идеален для представления булевских значений,
поскольку используется элемент CheckBox, который в отмеченном состоянии
соответствует значению true, а в сброшенном — значению false.

DataGridComboBoxColumn - идеален для представления перечислений, поскольку в обычном режиме используется элемент TextBlock, а в режиме редактирования — элемент ComboBox, содержащий все возможные значения.
В WPF встроен еще один тип столбца:

DataGridTemplateColumn - позволяет задать произвольные шаблоны для представления значения в обычном режиме и в режиме редактирования. Делается это с
помощью свойств CellTemplate и CellEditingTemplate.

Автоматически генерируемые столбцы
Если объекты, отображаемые в элементе DataGrid, задаются с помощью свойства
ItemsSource, то элемент пытается автоматически сгенерировать соответствующие
столбцы. В таком случае для представления строк выбирается столбец типа
DataGridTextColumn, для представления URI - столбец типа DataGridHyperlinkColumn,
для представления булевских величин - столбец типа DataGridCheckBoxColumn, а для
представления перечислений — столбец типа DataGridComboBoxColumn (при этом
источник данных для значений перечисления присоединяется автоматически).

340

Глава 10.Многодетные элементы управления

Таким образом, пустой элемент DataGrid:


порождает почти такой же результат, как на рис. 10.10, если установить его свойство
ItemsSource, как в следующем застраничном коде:
dataGrid.ItemsSource = new Record[]
{
new Record { FirstName=‛Adam‛, LastName=‛Nathan‛, Website=
new Uri(‚http://adamnathan.net‛), Gender=Gender.Male },
new Record { FirstName=‛Bill‛, LastName=‛Gates‛, Website=
new Uri(‚http://twitter.com/billgates‛), Gender=Gender.Male,
IsBillionaire=true }
};

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

Рис. 10.11. Элемент WPF DataGrid с автоматически сгенерированными столбцами, в
которых заголовками служат имена свойств класса Record
Мало того что конструирование элемента оказалось гораздо проще, так еще DataGrid на
рис. 10.11 автоматически поддерживает редактирование всех полей каждого элемента.
При включении элементов непосредственно в коллекцию Items элемента DataGrid этого
не было. Стоит щелкнуть по любой ячейке в первых трех столбцах, как она
автоматически превращается в редактируемое поле ввода TextBox, по флажкам
CheckBox тоже можно щелкать, а ячейка в столбце Gender (Пол) при щелчке по ней
автоматически превращается в список ComboBox с правильным набором значений. Для
ячеек, владеющих фокусом, особым образом интерпретируются некоторые клавиши,
например пробел или F2. Результат любого завершенного редактирования отражается в
коллекции ItemsSource. (К сожалению, отметка флажка IsBillionaire (Миллиардер)
против моего имени никак не отразилась на состоянии моего банковского счета.
Наверное, в этом примере какая-то ошибка.)
Если в элементе DataGrid уже были явно определены какие-то столбцы, то автоматически сгенерированные добавляются после них. Отдельные автоматически
сгенерированные столбцы можно настроить или удалить, обработав событие
AutoGeneratingGolumn, которое возникает один раз для каждого столбца. После
генерации всех столбцов один раз возникает событие AutoGeneratedColumns.

Селекторы

341

Чтобы вообще отменить автоматическую генерацию столбцов, достаточно присвоить
свойству AutoGenerateColumns значение false.

Выбор строк и ячеек

Элемент DataGrid поддерживает несколько моделей выбора с помощью двух свойств —
SelectionMode и SelectionUnit. Свойству SelectionMode можно присвоить значение
Single — тогда разрешено выбирать только один объект, или значение Extended — в
этом случае можно выбирать несколько объектов (это режим по умолчанию).
Определение слова «объект» зависит от значения свойства SelectionUnit:
• Cell - разрешено выбирать только отдельные ячейки.
• FullRow - разрешено выбирать только строки целиком.
• CellOrRowHeader - разрешено выбирать и то и другое (для выбора всей строки
следует щелкнуть по ее заголовку).
В режиме выбора нескольких объектов щелчок с нажатой клавишей Shift позволяет
выбирать соседние объекты, а с нажатой клавишей Ctrl — произвольно расположенные
объекты.
При выборе строк генерируется событие Selected, а свойство SelectedItems содержит
коллекцию выбранных объектов. Для элемента DataGrid в листинге 10.2 это была бы
коллекция объектов типа Record. При выборе отдельных ячеек генерируется событие
SelectedCellChanged, а свойство SelectedCells содержит список структур
DataGridCellInfo, в которых хранится информация
о соответствующих строках и столбцах. Выбираемые объекты DataGridRow и
DataGridCell также генерируют свои события Selected, а их свойство IsSelected
принимает значение true.
Даже если выбрано несколько ячеек или строк, в каждый момент времени фокус может
принадлежать только одной ячейке. Получить или установить эту ячейку позволяет
свойство CurrentCell. Кроме того, свойство CurrentColumn позволяет определить, в
каком столбце находится ячейка CurrentCell, а свойство CurrentItem возвращает объект
данных, соответствующий строке, которая содержит ячейку CurrentCell.
Развитая поддержка множественного выбора и операций над выделенными объектами
реализована в базовом классе MultiSelector, который наследует классу Selector и был
впервые введен в версии WPF 3.5. Другие элементы управления WPF также
поддерживают множественный выбор, но только DataGrid наследует классу
MultiSelector.

Дополнительные настройки
Класс DataGrid поддерживает и другие способы настройки, например взаимодействие с
буфером обмена, виртуализацию, возможность выводить дополнительную
информацию для строк и «замораживать» столбцы.
Взаимодействие с буфером обмена. Настроить, какие именно данные копируются из
DataGrid в буфер обмена (например, при нажатии Ctrl+C после выбора объектов),
позволяет свойство ClipboardCopyMode. Оно может принимать следующие значения:

342

Глава 10.Многодетные элементы управления


Exclude Header - не включать заголовки столбцов в копируемый текст. Это
режим по умолчанию.

IncludeHeader - включать заголовки столбцов в копируемый текст.

None - ничего не копировать в буфер обмена.
Виртуализация. По умолчанию строки DataGrid виртуализируются (объекты UIElement
не создаются для строк, невидимых на экране, причем в зависимости от источника
данных даже выборка данных для этих строк может откладываться), а столбцы - нет.
Для изменения этого поведения предназначены свойства EnableRowVirtualization (если
оно равно false, то строки не виртуализируются) и EnableColumnVirtualization (если оно
равно true, то столбцы виртуализируются). Свойство EnableColumnVirtualization по
умолчанию не равно true, потому что в этом режиме может замедляться обновление
изображения при горизонтальной прокрутке.
Дополнительная информация для строк. Элемент DataGrid поддерживает показ
дополнительной информации в строках за счет установки свойства RowDetailsTemplate.
Например:



Details go here.



Обычно элементы внутри шаблона RowDetailsTemplate пользуются механизмов
привязки к данным, чтобы изменить содержимое текущей строки, но в данном случае
мы задали простой элемент TextBlock. На рис. 10.12 показано, что происходит при
выборе строки.

Рис. 10.12. Показ дополнительной информации для выбранной строки в элементе
DataGrid
По умолчанию дополнительная информация показывается только для выбранной
строки (или строк), но это поведение можно изменить с помощью свойства
RowDetailsVisibilityMode, принимающего следующие значения:

Селекторы

343


VisibleWhenSelected — дополнительная информация показывается только для
выбранных строк. Это режим по умолчанию.

Visible - дополнительная информация показывается для всех строк.

Collapsed - дополнительная информация вообще не показывается.
Замораживание столбцов. Элемент DataGrid позволяет заморозить любое число
столбцов. Это означает, что они не будут выдвинуты за пределы области элемента при
горизонтальной прокрутке. Примерно так же заморозка столбцов работает в Microsoft
Excel. Но имеется несколько ограничений: замораживать можно только самые левые
столбцы и замороженные столбцы нельзя менять местами с незамороженными.
Чтобы заморозить один или несколько столбцов, достаточно присвоить свойству
FrozenColumnCount любое значение, большее 0. На рис. 10.13 показано, как выглядит
элемент DataGrid из листинга 10.2, когда FrozenColumnCount равно 2. Столбцы,
начиная с третьего, можно прокручивать, поэтому-то и не видно заголовка третьего
столбца.
заморожены

незаморожены

Рис. 10.13. Элемент DataGrid из листинга 10.2, в котором FrozenColumnCount="2"

FAQ
Можно ли в DataGrid замораживать строки?
Нет, такая возможность не предусмотрена. Автоматически заморозить можно только
дополнительную информацию для строк. Если свойство AreRowDetailsFrozen равно
true, то вся показанная дополнительная информация не смещается при
горизонтальной прокрутке.р

Редактирование, добавление и удаление данных
Мы уже видели, что при использовании свойства ItemSource автоматически
поддерживается редактирование данных в отдельных объектах. Поскольку коллекция
ItemSource допускает также добавление и удаление объектов, то автоматически
поддерживает и эти операции. В предыдущем примере для получения этой
функциональности достаточно обернуть массив списком List (при этом
статический массив служит только для инициализации динамического списка):

344

Глава 10.Многодетные элементы управления

dataGrid.ItemsSource = new List(
new Record[]
{
new Record { FirstName="Adam", LastName="Nathanv, Website=
new Uri("http://adamnathan.net"), Gender=Gender.Male },
new Record { FirstName="Bill", LastName="Gates", Website=
new Uri("http://twitter.com/billgates"), Gender=Gender.Male,
IsBillionaire=true }
}

Теперь в сетке DataGrid внизу присутствует пустая строка, в которую в любой момент
можно добавить данные. В классе DataGrid определены методы и команды для таких
типичных действий, как начало редактирования (клавиша F2), отмена редактирования
(клавиша Esc), сохранение результатов редактирования (клавиша Enter) и удаление
строки (клавиша Delete).
Для предотвращения редактирования следует присвоить свойству IsReadOnly значение
true, а чтобы запретить добавление или удаление строк, нужно присвоить значение false
свойству CanUserAddRows или CanUserDeleteRows соответственно. В листинге 10.2
для свойства IsReadOnly установлено значение true, чтобы избежать исключений,
поскольку описанная в разметке коллекция объектов Record не поддерживает
редактирование. Хотя вход в режим редактирования (и переключение ячейки в такой
режим) производится автоматически, по ходу дела возникает несколько событий,
позволяющих вмешаться в этот процесс: PreparingCellForEdit, BeginningEdit,
CellEditEnding/RowEditEnding и InitialzeNewItem.

СОВЕТ
Свойства CanUserAddRows и CanUserDeleteRows могут быть автоматически
сброшены в false!
В зависимости от значений прочих свойств, свойства CanUserAddRows и
CanUserDeleteRows могут оказаться равными false, хотя для них было явно
установлено значение true! Например, если свойство IsReadOnly или IsEnabled
элемента DataGrid равно false, то будут равны false и оба вышеупомянутых свойства.
Есть и менее очевидный случай: если источник данных не поддерживает добавление
или удаление (на что указывают свойства CanAddNew и CanRemove, определенные в
интерфейсе IEditableCollectionView), то и CanUserAddRows, и CanUserDeleteRows
будут равны false. Дополнительные сведения о видах коллекций и, в частности об
интерфейсе IEditableCollectionView см. в главе 13.

Меню

345

Меню
В WPF имеются оба элемента, относящихся к меню: Menu и ContextMenu. Но, в
отличие от технологий на базе Win32, меню WPF не являются каким-то особым
случаем элементов управления со специальными ограничениями. Это просто еще один
вид многодетных элементов, предназначенный для иерархического отображения
объектов в виде каскадных выпадающих меню.

Элемент Menu
Элемент Menu располагает хранящиеся в нем объекты по горизонтали в строке
характерного серого цвета (по умолчанию). К своему базовому классу ItemsControl
класс Menu добавляет только свойство IsMainMenu. Если оно равно true (случай по
умолчанию), то меню Menu получает фокус при нажатии клавиши Alt или F10, что
соответствует ожиданиям пользователей, привыкших к меню в Win32.
Поскольку Menu - обычный многодетный элемент управления, то в качестве объектов в
нем может храниться все что угодно, хотя предполагается, что это будут объекты типа
MenuItem или Separator. На рис. 10.14 показано типичное меню, созданное из разметки
в листинге 10.3.
Листинг 10.3. Типичное меню с дочерними элементами Menuitem и Separator 














Класс MenuItem относится к многодетным элементам управления с заголовком
(наследует классу HeaderedItemsControl) и во многом напоминает однодетные элементы
управления с заголовком. В случае MenuItem свойство Header представляет собой
основной объект (как правило, текст, см. рис. 10.14). В коллекции Items, если она
непустая, хранятся дочерние элементы, отображаемые в виде подменю. Так же как
Button и Label, класс MenuItem поддерживает клавиши доступа, обозначаемые
предшествующим знаком подчеркивания.

346

Глава 10.Многодетные элементы управления

Рис. 10.14. Меню WPF
Separator - это простой элемент управления, который, будучи помещен в MenuItem,
визуализируется в виде горизонтальной черты, как показано на рис. 10.14. Этот класс
может использоваться и в двух других многодетных элементах: ToolBar и StatusBar.
Хотя Menu - простой элемент управления, класс MenuItem содержит много свойств для
настройки своего поведения. Приведем наиболее интересные.
•
Icon - позволяет добавлять произвольный объект, отображаемый рядом с
заголовком Header. Объект Icon визуализируется так же, как Header, хотя обычно
представляет собой небольшое изображение.
•
IsCheckable - наделяет MenuItem поведением флажка CheckBox.
•
InputGestureText - позволяет аннотировать элемент меню ассоциированным с
ним жестом (чаще всего какой-нибудь комбинацией клавиш, например Ctrl+0).
В классе Menultem определено также пять событий: Checked, Unchecked,
SubmenuOpened, SubmenuClosed и Click. Обычно для наделения пункта меню
поведением применяется обработчик события Click, но можно также записать команду
в свойство Command объекта MenuItem.

ПРЕДУПРЕЖДЕНИЕ
Задание свойства InputGestureText не ассоциирует с Menultem соответствующую
комбинацию клавиш!
Это досадное отличие WPF от таких систем, как Windows Forms и Visual Basic 6,запись в свойство InputGestureText элемента Menultem строки вида ―Ctrl+0‖ еще не
означает, что при нажатии комбинации клавиш Ctrl+0 будет автоматически вызван
данный пункт меню! Эта строка - не более чем документация.
Чтобы связать с Menultem комбинацию клавиш, необходимо ассоциировать ее с
командой с помощью свойства Command. Если с командой ассоциирован некий жест
ввода, то в свойство InputGestureText объекта Menultem автоматически записывается
соответствующая ему строка, то есть текстовое представление комбинации клавиш
отображается без каких-либо дополнительных действий.

Меню

347

СОВЕТ
Когда свойству Command объекта Menultem присваивается ссылка на объект типа
RoutedUICommand, в его свойство Header автоматически записывается значение
свойства Text команды. Это поведение можно переопределить, установив заголовок
Header явно.

FAQ
Как расположить пункты Menu по вертикали, а не по горизонтали?
Поскольку Menu - обычный многодетный элемент управления, можно воспользоваться описанным выше при рассмотрении ListBox приемом - подменой панели
ItemsPanel, только подразумеваемую по умолчанию панель следует заменить на
StackPanel:







По умолчанию StackPanel ориентирована вертикально, поэтому в данном случае
явно задавать свойство Orientation необязательно. Результат показан на рис. 10.15.

Рис. 10.15. Вертикальное меню

Если вы хотите, чтобы пункты меню были еще и повернуты на 90° (как в старых
программах из пакета Microsoft Office в случае, когда меню перетаскивается и
пристыковывается к левой или правой стороне окна), то воспользуйтесь преобразованием RotateTransform.

Элемент ContextMenu
Элемент ContextMenu работает так же, как Menu; это простой контейнер, предназначенный для хранения пунктов меню MenuItem и разделителей Separator. Однако же
включать ContextMenu непосредственно в дерево элементов нельзя. Следует связывать
его с элементом управления посредством подходящего присоединенного свойства,
например
свойства
ContextMenu
в
классах
FrameworkElement
и
FrameworkContentElement. Контекстное меню элемента отображается когда
пользователь щелкает по элементу правой кнопкой мыши (или нажимает комбинацию
клавиш Shift+F10).

348

Глава 10.Многодетные элементы управления

На рис. 10.16 изображено контекстное меню, следующим образом ассоциированное со
списком ListBox (предполагается, что пункты меню такие же, как в листинге 10.3):



…Три элемента MenuItems из листинга 10.3…



Помимо ожидаемого свойства IsOpen и событий Opened/Closed, в классе ContextMenu
определено еще много свойств для настройки местоположения меню. По умолчанию
левый верхний угол меню совпадает с позицией указателя мыши. Но свойство
Placement может принимать и другие значения, кроме MousePoint (например, Absolute).
К тому же с помощью свойств HorizontalOffset и VerticalOffset можно задавать
смещение от указателя по горизонтали и вертикали.

Рис. 10.16. Контекстное меню WPF

FAQ
Как сделать, чтобы контекстное меню появлялось при щелчке правой кнопкой
мыши по неактивному элементу?
Как и ToolTipServise, класс ContextMenuService содержит присоединенное свойство
ShowOnDisabled специально для этой цели. Используется оно следующим образом:



Другие многодетные элементы управления

349

Напомним, что с классом ToolTip связан статический класс ToolTipService, позволяющий управлять свойствами всплывающей подсказки из элемента, с которым она
ассоциирована. Точно так же с классом ContextMenu связан статический класс
ContextMenuService, предназначенный для той же цели. В нем имеется несколько
присоединенных свойств, соответствующих свойствам, определенным в самом классе
ContextMenu.

Другие многодетные элементы управления
Оставшиеся многодетные элементы управления - TreeView, ToolBar и StatusBar - не
являются ни селекторами, ни меню, но тем не менее могут содержать неограниченное
число произвольных объектов.

Элемент TreeView
TreeView - популярный элемент управления, предназначенный для отображения
иерархически организованных данных с возможностью раскрывать и сворачивать узлы
дерева, как показано на рис. 10.17. В теме Aero состояние узлов обозначается
треугольничками, в других темах, например Luna, - привычными знаками плюс и
минус.
Тема Aero

Тема Luna

Puc. 10.17. Элемент WPF TreeView
TreeView, как и Menu, - очень простой элемент управления. Он может содержать
любые объекты и располагает их по вертикали. Однако от TreeView мало пользы, если
в нем хранится что-то, кроме объектов типа TreeViewItem.
TreeViewItem, как и MenuItem, — многодетный элемент управления с заголовком. В
его свойстве Header хранится сам элемент, а в коллекции Items - его подэлементы
(предполагается, что они также являются объектами типа TreeViewItem).
Элемент TreeView, изображенный на рис. 10.17, соответствует следующей XAMLразметке:




350

Глава 10.Многодетные элементы управления

…











R классе TreeViewItem имеются удобные свойства IsExpanded и IsSelecteil, а также
четыре события, соответствующие четырем возможным состояниям этих свойств Expanded, Collapsed, Selected и Unselected. Кроме того, класс TreeViewItem
поддерживает навигацию с помощью клавиатуры: клавиши плюс (+) и минус (-)
сответственно раскрывают и сворачивают узел, а клавиши со стрелками, Page Up, Page
Down, Home и End дают разные способы передачи фокуса от одного узла другому. О
ПНЕМ ГЛУБЖЕ

КОПНЕМ ГЛУБЖЕ
Сравнение классов TreeView и Selector
С точки зрения API класс TreeView очень похож на класс Selector, но не является
производным от него, потому что для иерархически организованных объектов не
существует естественного понятия целочисленного индекса. Поэтому в TreeView
определены собственные свойства SelectedItem и SelectedValue (но не SelectedIndex).
Также определено событие SelectedItemChanged, вместе с которым обработчику
передаются не коллекции, а просто объекты OldValue и NewValue, gjскольку
TreeView поддерживает выбор только одного объекта.
Отсутствие поддержки для выбора нескольких объектов - досадное ограничение,
сохранившееся и в версии WPF 4. Если вам это необходимо, то можете пользоваться
каким-нибудь сторонним элементом управления, например RadTreeView компании
Telerik (http://telerik.com/products/wpf/treeview.aspx) . Можете также попробовать
самостоятельно написать класс TreeView с поддержкой множественного выбора,
унаследовав классу ListBox, но это нелегко.

СОВЕТ
В версии WPF 4клас TreeView начал поддерживать виртуализацию, но включать ее
нужно
явно
путем
установки
для
присоединенного
свойства
VirtualizingStackPanel.IsVirtualizing объекта TreeView значения true. Этот режим
позволяет заметно сэкономить память и повысить производительность прокрутки,
когда количество узлов очень велико.

Другие многодетные элементы управления

351

ПРЕДУПРЕЖДЕНИЕ
Всегда явно обертывайте узлы TreeView элементами TreeViewItem!
Очень заманчиво использовать в качестве листовых узлов простые элементы
TextBlock, но если вы так поступите, то можете столкнуться с одной тонкостью
механизма наследования значений свойств, из-за которой текст в таких элементах,
как TextBlock, как бы пропадает. По умолчанию выбор родительского элемента
меняет его цвет Foreground на белый, и если текстовые блоки TextBlock являются
прямыми логическими потомками этого элемента, то и к ним будет применен белый
цвет. (Хотя визуальным родителем каждого текстового блока является неявный
элемент TreeViewItem, в механизме наследования свойств приоритет отдается
логическому родителю.) Понятно, что на подразумеваемом по умолчанию белом
фоне такой текст не виден. Если же сделать TreeViewItem явным (логическим)
родителем каждого блока TextBlock, то нежелательный эффект наследования не
проявляется.

Элемент ToolBar
Элемент управления ToolBar (панель инструментов) обычно применяется для
группировки небольших кнопок (или других элементов управления) и служит
дополнением к традиционной системе меню. На рис. 10.18 показана панель
инструментов, полученная визуализацией следующей XAML-разметки:



























352

Глава 10.Многодетные элементы управления

10.18. Элемент WPF ToolBar
Отметим, что элементы Button и ComboBox на панели инструментов выглядят иначе,
чем обычно. Кроме того, разделитель Separator теперь представляется вертикальной
линией, а не горизонтальной, как в меню Menu. Элемент ToolBar переопределяет
подразумеваемые по умолчанию стили своих дочерних элементов так, чтобы они
выглядели привычно для большинства пользователей
Элементы ToolBar могут находиться в любом месте дерева элементов, но обычно их
помещают в специальный контейнер ToolBarTray, производный от FraimworkElement.
Объект ToolBarTray содержит коллекцию элементов ToolBar (в своем свойстве
содержимого ToolBars) и, если его свойство IsLocked не равно true, позволяет
перетаскивать панели инструментов и располагать их в другом месте. (В классе
ToolBarTray определено также присоединенное свойство IsLocked, которое можно
задавать для отдельных панелей ToolBar.) В классе ToolBarTray имеется свойство
Orientation; если присвоить ему значение Vertical, то все содержащиеся в нем панели
ToolBar будут ориентированы вертикально.
Если панель инструментов ToolBar содержит больше элементов, чем помещается в
занимаемой ею области, то лишние попадают в область переполнения. Это
всплывающее окно, для открытия которого нужно щелкнуть по стрелочке в конце
панели, как показано на рис. 10.19. По умолчанию первым в область переполнения
попадает последний элемент панели, но для отдельных: элементов этим поведением
можно управлять с помощью присоединенного свойства OverflowMode из класса
ToolBar. С его помощью можно определить, что элемент должен перемещаться в
область переполнения по мере необходимости (AsNeeded - по умолчанию), всегда
(Always) или никогда (Never).

Рис. 10.19. У панели инструментов имеется область переполнения, в которую
попадают не поместившиеся элементы

СОВЕТ
Чтобы создать настраиваемую панель инструментов, как в Visual Studio, присвойте
свойству ToolBar.OverflowMode значение Never для каждого элемента, затем
добавьте элемент Menu с заголовком "_Add or Remove Buttons" (Добавить или удалить кнопки), для которого свойство ToolBar.OverflowMode должно быть равно
Always (чтобы он всегда оставался в области переполнения). Далее в это меню можно
добавить пункты MenuItem и сделать так, чтобы отметка флажка в таком пункте
приводила к добавлению соответствующего пункта на панель инструментов, а сброс
флажка - к убиранию элемента с панели.

Другие многодетные элементы управления

353

КОПНЕМ ГЛУБЖЕ
Настройка навигации с помощью клавиатуры
Следующий элемент ToolBar демонстрирует несколько странное поведение в части
навигации с помощью клавиш:









Если передать фокус панели ToolBar, а затем несколько раз нажать клавишу Tab, то
фокус «застрянет» - будет передаваться от А к В, затем к С, к D и снова к А и далее
по кругу. А если нажимать клавишу со стрелкой влево или вправо, то фокус будет
попеременно передаваться В и С.
В классе KeyboardNavigation из пространства имен System.Windows.Input определено
несколько полезных присоединенных свойств для настройки этого и других аспектов
поведения клавиатуры. Например, чтобы избежать зацикливания при нажатии
клавиши Tab на панели инструментов, можно присвоить свойству
КеуboardNavigation.TabNavigation для элемента ToolBar значение Continue (вместо
Cycle). А чтобы не попасть в цикл при навигации по меню с помощью клавиш со
стрелками, задайте для элемента Menu свойство KeyboardNavigation.
DirectionalNavigation, равное Continue.

354

Глава 10.Многодетные элементы управления

КОПНЕМ ГЛУБЖЕ
Неиспользуемое свойство Header элемента ToolBar
На самом деле ToolBar - многодетный элемент управления с заголовком (как
MenuItem и TreeViewltem). Его свойство Header никогда не отображается, но может
быть полезно для реализации дополнительных возможностей ToolBarТгау. Например, можно добавить контекстное меню, в котором перечислены все панели
инструментов ToolBar (представленные своими заголовками Header), дав пользователям возможность добавлять или удалять панели. Или реализовать перемещаемые
панели инструментов и показывать заголовок, когда панель «плавает».

Элемент StatusBar
Элемент StatusBar ведет себя, как Menu, но располагает своих потомков по горизонтали,
как показано на рис. 10.20. Обычно его помещают вдоль нижнего края окна Window и
используют для отображения информации о состоянии

Рис. 10.20. Элемент WPF StatusBar
Строка состояния StatusBar, изображенная на рис. 10.20, получена визуализацией
следующей XAML-разметки:









По умолчанию StatusBar переопределяет шаблон элемента управления Separator так,
что он отображается в виде вертикальной линии, как на панели инструментов ToolBar.
Дочерние элементы StatusBar (кроме Separator) неявно обертываются объектами
StatusBarItem, но можно включить их и явно. Тогда можно будет настраивать их
позиции с помощью относящихся к компоновке присоединенных свойств, которые мы
рассматривали в главе 5.

Резюме

355

FAQ
Как сделать так, чтобы секции строки состояния пропорционально
растягивались?
Очень часто желательно, чтобы отдельные секции строки состояния сохраняли
пропорции. Например, левая секция должна занимать 25% ширины StatusBar, а
правая — 75%. Добиться этого эффекта можно, заменив внутреннюю панель
ItemsPanel сеткой Grid и сконфигурировав ее столбцы следующим образом:

















Отметим, что к элементам внутри StatusBar необходимо явно присоединять свойство
Grid.Column (которое имеет смысл, только если в качестве ItemsPanel используется
Grid), иначе все они окажутся в столбце с индексом 0. Кроме того, имейте в виду, что
такие свойства компоновки работают только для потомков типа StatusBarItem и
Separator. Дело в том, что остальные элементы (Label, ComboBox и Button в
рассматриваемом примере StatusBar) неявно обернуты объектами StatusBarItem, к
которым нужные свойства не присоединены. Поэтому, чтобы добиться требуемого
эффекта, необходимо обернуть их в StatusBarItem явно.

Резюме
Понимать, что такое многодетные элементы управления, необходимо практически в
любом проекте на основе WPF. Трудно представить себе WPF-приложение, в котором
не используются ни однодетные, ни многодетные элементы. Но, в отличие от
однодетных,
многодетные
элементы
обладают
гораздо
более
развитой
функциональностью! В этой главе неоднократно упоминалось о важности привязки к
данным при работе с динамическими списками объектов. Но перед тем как вплотную
заняться привязкой к данным, мы должны рассмотреть еще несколько областей WPF. В
следующей главе мы поговорим об изображениях, тексте и других элементах
управления.

11
Изображения, текст и другие элементы управления
•
•
•
•
•

Элемент управления Image
Элементы управления Text и Ink
Документы
Диапазонные элементы управления
Календарные элементы управления

В этой главе мы будем рассматривать элементы управления, не являющиеся ни
однодетными, ни многодетными. Некоторые из них, например Image, кое-какие
текстовые элементы, ProgressBar и Slider, наверное, вам знакомы, но в WPF они
обладают более развитой функциональностью, чем вы могли бы предположить.
Элементы Calendar и DatePicker появились только в WPF 4. Мы рассмотрим также ряд
элементов, производных от класса FrameworkContentElement (а не Control), с помощью
которых можно создавать потоковые документы. Это мощная, но не слишком часто
используемая возможность WPF.

Элемент управления Image
Класс System.Windows.Controls.Image позволяет включать в пользовательский
интерфейс изображения (в формате BMP, PNG, GIF, JPG и др.). В нем имеется свойство
Source типа System.Windows.Media.ImageSource, но благодаря конвертеру типа
System.Windows.Media.ImageSourceConverter его можно задавать в XAML в виде
простой строки, например:


Свойство ImageSource может указывать на изображения, представленные URLадресом, хранящиеся в файловой системе и даже внедренные в сборку. (Извлечение и
рисование изображений, внедренных в сборку, рассматриваете в следующей главе.) В
классе Image определены те же самые свойства Stretch и StretchDirection, с которыми
мы встречались в главе 5 «Компоновка с помощью панелей», - они позволяют
управлять масштабированием.
В целом работать с классом Image несложно, если не считать ряд менее очевидных
средств для визуализации изображения, которые он поддерживает. К элементу Image
можно присоединить свойство RenderOptions. BitmapScalingMode, задающее
компромисс между скоростью и качеством визуализации.

Элемент управления Image

357

Из всех принимаемых им значений наиболее важным является NearestNeighbor — это
режим масштабирования растрового изображения по ближайшей соседней точке, при
котором изображение становится более четким. Этот режим мы устанавливали в
предыдущей главе при обсуждении элементов ToolBar и StatusBar, а также в
приложении Photo Gallery из главы 7 «Структурирование и развертывание
приложения». Пример:


В печатном тексте различие неотчетливо, но на экране компьютера улучшение качества
сразу заметно. На рис. 11.1 показаны изображения из программы Photo Gallery в
режиме NearestNeighbor и без него.
Отображение по умолчанию RenderOptions.BitmapScalingMode="NearestNeighbor"

Рис. 11.1. Если свойство BitmapScalingMode равно NearestNeighbor, то края выглядят
четче

СОВЕТ
Вместо того чтобы пользоваться конвертером типа для преобразования строкового
имени файла в объект ImageSource, можно явно присвоить свойству Source объекта
Image ссылку на объект одного из подклассов ImageSource, что открывает дополнительные возможности. Например, в подклассе BitmapImage есть ряд свойств,
таких как DecodePixelWidth и DecodePixelHeight, с помощью которых можно задать
размер изображения, меньший естественного, и тем самым сэкономить память—
иногда довольно ощутимо. Подкласс FormatConvertedBitmap позволяет изменять
формат пикселов Image, создавая различные эффекты, например переход к
полутоновому изображению. В следующей XAML-разметке возможности класса
FormatConvertedBitmap применяются для получения результата, показанного на рис.
11.2:

















358

Глава 11.Изображения, текст и другие элементы управления

Длинный перечень возможных форматов определен в перечислении;
System.Windows.Media.PixelFormats.

Pbgra32
(по умолчанию)

Gray32Float

BlackWhite

Рис. 11.2. Отображение Image с тремя разными форматами пикселов (см. также
цветную вклейку)

Элементы управления Text и Ink
Помимо элементов TextBlock и Label WPF содержит еще ряд элементов ддя
отображения и редактирования текста - посредством как клавиатурного набора, так и
рукописного ввода с помощью стилуса. В этом разделе мы чуть подробнее рассмотрим
элемент TextBlock, а также поговорим о следующих элементах:
•
TextBox
•
RichTextFormat
•
PasswordBox
•
InkCanvas
Но сначала упомянем о важном усовершенствовании в WPF 4, которое распространяется на все способы визуализации текста. С самого начала пользователи WPF
жаловались на размытость текста. (Я сам неоднократно заявлял, что смогу мгновенно
распознать созданный на WPF интерфейс по нечеткому тексту!) Механизм
визуализации текста в WPF был оптимизирован для крупного кегля и/или экрана со
сверхвысоким разрешением с учетом точности масштабирования и отличного качества
передачи при печати. Но при работе с кеглями шрифтов, характерными для
большинства приложений, и с разрешениями современных мониторов выявились
недостатки такого подхода. Когда хотят вежливо описать эту ситуацию, говорят, что
визуализация текста в WPF опередила время.

Элементы управления Text и Ink

359

Рад сообщить вам, что в WPF 4 эти проблемы разрешены. Как и во многих областях,
где была повышена производительность, кое-какие усовершенствования в части
визуализации текста вы получаете задаром. (Например, WPF теперь автоматически
использует растровые изображения, внедренные в некоторые восточноазиатские
шрифты, чтобы получать четкий текст при мелких кеглях.) Другие же
усовершенствования необходимо активировать явно - в целях сохранения
совместимости с существующими приложениями.
Главное,
о
чем
нужно
знать,
это
присоединенное
свойство
TextOptions.TextFormattingMode. Его можно задавать как для отдельных текстовых
элементов, так и - что практикуется чаще - для родительского элемента, например Window; в последнем случае оно распространяется на визуализацию текста во всем дереве
элементов-потомков. Присвоив свойству TextFormattingMode значение Display, вы
включите новый механизм визуализации текста в WPF 4, в котором применяются
метрики текста, совместимые с GDI. С точки зрения четкости текста основная
особенность этого механизма состоит в том, что каждый глиф позиционируется на
границе пикселов (а его ширина кратна ширине пиксела).
Подразумеваемое по умолчанию значение TextFormattingMode - то самое, которое
причиняло столько неприятностей разработчикам и пользователям, - по иронии судьбы
названо Ideal. В этом случае метрики текста обеспечивают максимально точное
следование определению шрифта, даже если это означает, что глифы не совмещаются с
границами пикселов. В будущем идеальном мире, где плотность размещения пикселов
на экране будет куда выше нынешней, это действительно даст оптимальный результат
(как и сегодня при отображении текста крупным кеглем).
Присоединенному свойству TextOptions.TextRenderingMode можно присвоить значение
ClearType, Grayscale, Aliased или Auto - для управления режимом сглаживания текста
(antialiasing). При заданном значении Auto (по умолчанию) будет действовать режим
ClearType, если эта технология не отключена на данном компьютере, в противном
случае — режим Grayscale.
На рис. 11.3 показана разница между двумя значениями TextFormattingMode и тремя
значениями TextRenderingMode, отличными от Auto, хотя на печатной странице
заметить разницу сложно.

Рис. 11.3. Настройка визуализации текстовых блоков при кегле FontSize=11

360

Глава 11.Изображения, текст и другие элементы управления

Далее свойству TextOptions.TextHintingMode можно присвоить значение Fixed,
Animated или Auto - для оптимизации отображения в зависимости от того, является
текст стационарным или анимированным.

FAQ
Не следует ли всегда задавать значение Display свойства TextFormattingMode,
чтобы оптимизировать визуализацию текста?
Нет. Если текст отображается достаточно крупным кеглем (FontSize порядка 15 или
больше), то режим Ideal дает такое же четкое изображение, как режим Display, а
глифы располагаются лучше. Но еще важнее то, что в случае применения к тексту
геометрического преобразования режим Display оказывается хуже, поскольку
выравнивание на границы пикселов больше не применяется. Хуже всего выглядит
текст в режиме Display после увеличения в результате применения ScaleTransform,
потому что WPF просто масштабирует растровое изображение текста, а не
перерисовывает его более крупным шрифтом. (Так делается для того, чтобы
гарантировать точное масштабирование с заданным коэффициентом, чего
невозможно было бы достичь, если бы при большем кегле применялось
выравнивание на границы пикселов.) Но для типичных меток, отображаемых мелким
шрифтом, у режима Display нет конкурентов.

Элемент TextBlock
У элемента TextBlock есть ряд простых свойств, модифицирующих его внешний вид,
например FontFamyly, FontSize, FontStyle, FontWeight и FontStretch. Но главный
сорприз TextBlock заключается в том, что его свойством содержимого является не Text,
а коллекция объектов Inlines. Хотя показанная ниже разметка дает тот же самый
результат, что и установка свойства Text, в действительности мы устанавливаем другое
свойство:

Text in a TextBlock

Конвертер типа создает иллюзию, будто значением является простая строка, хотя на
самом деле это коллекция, состоящая из одного элемента Run. Поэтому следующая
XAML-разметка в точности эквивалентна предыдущей:


и, в свою очередь, эквивалентна такой (поскольку Text — это свойство содержимого в
классе Run):
Text in a TextBlock

Объект Run - это просто фрагмент текста с одним и тем же форматированием. Явное
использование одного элемента Run не дает никаких преимуществ, но, когда в одном
блоке TextBlock встречается несколько элементов Run, картина становится интереснее.
Например, показанный выше TextBlock можно было бы записать и так:

Элементы управления Text и Ink

361


Text
 in
 a
 TextBlock


Результат визуализации при этом не изменяется. Однако в классе Run имеется
несколько свойств форматирования, позволяющих переопределить соответствующие
свойства, установленные в родительском элементе TextBlock, а именно: FontFamily,
FontSize, FontStretch, FontStyle, FontWeight, Foreground и TextDecorations. Они
используются в следующей XAML-разметке, результат визуализации которой показан
на рис. 11.4:

Rich
 Text 
in
 a 
TextBlock


Puc. 11.4. Несколько фрагментов Run с разным форматированием внутри одного
блока TextBlock
Это, конечно, крайность, но аналогичную технику можно применить, например, для
курсивного начертания или подчеркивания одного слова в абзаце. Это гораздо проще,
чем пытаться правильно позиционировать несколько элементов TextBlock. Кроме того,
при использовании одного TextBlock вы получаете корректное отсечение и перенос на
другую строку, даже если начертание текста неоднородно. Помимо Run есть много
других объектов типа Inline; в разделе «Документы» ниже они рассматриваются более
подробно.

КОПНЕМ ГЛУБЖЕ
TextBlock и пустое пространство
Если содержимое TextBlock устанавливается с помощью свойства Text, то все символы пробела сохраняются. Если же оно устанавливается с помощью свойства Inlines в
XAML, то пустое пространство не сохраняется. Начальные и конечные пробелы при
этом игнорируются, а соседние пробелы заменяются одним (как в HTML).

362

Глава 11.Изображения, текст и другие элементы управления

СОВЕТ
При добавлении содержимого в свойство Inlines элемента TextBlock его
неформатированное представление дописывается в конец свойства Text. Поэтому
программа по-прежнему может пользоваться свойством Text, даже если явно
устанавливается только Inlines. Например, для блока TextBlock на рис. 11.4
значением Text является строка "Rich Text in a TextBlock", как и следовало ожидать!

КОПНЕМ ГЛУБЖЕ
Явно и неявно заданные фрагменты Run
Следующий элемент TextBlock:
Text in a TextBlock

эквивалентен такому:
Text in a TextBlock

но не всегда поведение конвертера типа настолько очевидно. Например, такое
использование элемента LineBreak (еще одной разновидности Inline) допустимое!
Text ina TextBlock

а такое - нет:
Text ina TextBlock

Последний вариант недопустим, потому что свойством содержимого класса М (Text)
является простая строка, а включить элемент LineBreak внутрь строки нельзя. Однако
конвертер типа преобразует свойство содержимого класса TextBlock (Inlines) в один
или несколько объектов Run, корректно обрабатывая объекты LineBreak. В
результате следующая XAML-разметка:
Text ina TextBlock

оказывается эквивалентной блоку TextBlock, содержащему два объекта Run, по
одному с каждой стороны LineBreak:
Text ina TextBlock

Элемент TextBox
Элемент управления TextBox, изображенный на рис. 11.5, позволяет вводить одну или
несколько строк текста. В отличие от большинства других элементов управления в
WPF, содержимое TextBox хранится не в виде объекта типа System.Object, а в
строковом свойстве Text.

Рис. 11.5. Элемент WPF TextBox

Элементы управления Text и Ink

363

Хотя на первый взгляд TextBox выглядит очень просто, в него встроена весьма
развитая функциональность, привязки для команд Cut, Copy, Paste, Undo и Redo (см.
главу 6 «События ввода: клавиатура, мышь, стилус и мультисенсорные устройства») и
даже проверка правописания!
В классе TextBox определено несколько методов и свойств для выбора различных
частей текста (выделенного фрагмента, по номеру строку и т. д.), а также методы для
поиска физической точки в тексте по номеру строки и символа и наоборот. Определены
также события TextChanged и SelectionChanged.
Если на размер элемента TextBox не налагает ограничений окружение (и он не задан
явно), то элемент растет по мере добавления в него текста. Если же ширина TextBox
ограничена, то можно установить режим переноса строк, присвоив свойству
TextWrapping значение Wrap или WrapWithOverflow. В режиме Wrap содержимое ни
при каких условиях не может выйти за пределы области, занятой элементом, даже если
придется разорвать строку в середине слова. В режиме WrapWithOveгflow строка
разрывается, только если есть такая возможность, так что длинные слова могут выйти
за границы области. (В классе TextBlock также есть свойство TextWrapping.)

FAQ
Как сделать, чтобы элемент TextBox поддерживал ввод нескольких строк
текста?
Если присвоить свойству AcceptsReturn значение true, то при нажатии клавиши Enter
будет создаваться новая строка. Отметим, что TextBox в любом случае поддерживает
создание многострочных текстов из программы. Если записать в свойство Text текст,
содержащий символы NewLine, то он отобразится в виде нескольких строк вне
зависимости от значения AcceptsReturn. Кроме того, поддержка многострочных
текстов никак не связана с переносом строк. Перенос применяется только к
отдельным строкам, длина которых превышает ширину TextBox.

КОПНЕМ ГЛУБЖЕ
Проверка правописания
Чтобы включить проверку правописания в TextBox (или RichTextBox), необходимо
присвоить присоединенному свойству SpellCheck.IsEnabled значение true. Выглядит
это примерно так же, как в Microsoft Word: неправильно написанные слова
подчеркиваются красным цветом, а если щелкнуть по такому слову правой кнопкой
мыши, будут предложены варианты исправления. WPF пользуется словарем, который
сопоставим с применяемым в Microsoft Office и имеется для разных языков (входит в
состав соответствующего языкового пакета). Однако пользовательские словари WPF
не поддерживает.

364

Глава 11.Изображения, текст и другие элементы управления

Элемент RichTextBox
Элемент RichTextBox предоставляет больше возможностей, чем ТехtВох, поскольку
может содержать форматированный текст (и допускает наличие в тексте произвольных
объектов). На рис. 11.6 показан элемент RichTextBox с простым форматированным
текстом.

Рис. 11.6. Элемент WPF RichTextBox
У RichTextBox и TextBox общий базовый класс (TextBoxBase), поэтому многие возможности, описанные выше для TextBox, применимы и к RichTextBox. Но некоторые
средства TextBox реализованы в RichTextBox более полно. Там, где TextBox
предоставляет лишь простые целочисленные свойства Caretlndex, SelectionStart и
SelectionEnd, RichTextBox предлагает свойство CaretPosition типа TextPointer и
свойство Selection типа TextSelection. Кроме того, содержимое RichTextBox хранится в
свойстве Document типа FlowDocument, а не в простом строковом свойстве Text.
Содержимое может даже включать объекты типа UIElement, с которыми можно
взаимодействовать и которые генерируют события, если свойство IsDocumentEnabled
элемента RichTextBox имеет значение true. Класс FlowDocument обсуждается ниже в
разделе «Документы».

Элемент PasswordBox
Элемент PasswordBox - это упрощенный вариант TextBox, предназначенный для ввода
пароля. Вместо вводимых символов в нем отображаются кружочки, как показано на
рис. 11.7.

Рис. 11.7. Элемент WPF PasswordBox
Класс PasswordBox не наследует TextBoxBase, как два предыдущих элемента
управления, поэтому не поддерживает ни команды Cut, Copy, Undo и Redo (хотя
поддерживает команду Paste), ни проверку правописания. И это вполне разумное
поведение элемента, предназначенного для хранения паролей!
Если вам не нравятся кружочки, можете выбрать другой символ с помощью свойства
PasswordChar. (По умолчанию предполагается звездочка, которая отображается
специальным шрифтом и выглядит, как кружочек.)
Текст элемента PasswordBox хранится в строковом свойстве Password. В действительности для более надежной защиты применяется специальный класс
System.Security.SecureString. Содержимое объекта типа SecureString шифруется и
намеренно стирается, тогда как объекты System.String не шифруются и могут
оставаться в куче неопределенно долгое время, пока не будут убраны сборщиком
мусора.

Элементы управления Text и Ink

365

При изменении пароля генерируется событие TextBoxPasswordChanged. Его обработчик имеет тип RoutedEventHandler, то есть вместе с событием не передается
информация о старом и новом паролях. Если нужно узнать текущий пароль, можно
просто опросить внутри обработчика свойство Password.

Элемент InkCanvas
Основная задача поразительно гибкого элемента InkCanvas - предоставить средства для
рукописного ввода (с помощью мыши или стилуса, но не мульсенсорного устройства).
Его внешний вид показан на рис. 11.8. Строго говоря, InkCanvas - не элемент
управления, поскольку наследует непосредственно классу FrameworkElement, но ведет
он себя почти так же, как элементы управления (за исключением того, что его стиль
нельзя изменить с помощью шаблона).

Рис. 11.8. Элемент WPF InkCanvas
В режиме по умолчанию InkCanvas позволяет просто писать или рисовать на своей
поверхности. При работе со стилусом заостренный конец рисует, а обратный конец
стирает.
Каждый
нанесенный
штрих
запоминается
в
виде
объекта
System.Windows.Ink.Stroke, а все такие объекты сохраняются в коллекции Strokes. Но
InkCanvas позволяет также хранить любое число произвольных объектов типа
UIElement в коллекции Children (это его свойство содержимого). В результате очень
легко пометить все что угодно рукописной надписью, как показано на рис. 11.9.

Рис.11.9. Нанесение рукописных пометок поверх изображения

366

Глава 11.Изображения, текст и другие элементы управления

Чтобы получить это изображение, я разрисовал стилусом следующее окно Window:








Здесь я воспользовался очень интересным свойством SizeToContent - если в этом
режиме вы начнете рисовать за пределами окна, то объект Window автоматически
изменит размер так, чтобы поместились все штрихи!
Свойство DefauItDгawingAttributes позволяет изменить внешний вид будущих штрихов
(толщину, цвет и т.д.). В классе Stroke также есть свойство DrawingAttributes, так что
внешний вид можно задавать для каждого штриха в отдельности.
Элемент InkCanvas поддерживает несколько режимов, которые можно независимо
применять к заостренному концу стилуса (или к мыши) - посредством свойства
EditingMode - и к обратному его концу - с помощью свойства EditingModeInverted.
Предназначенное только для чтения свойство ActiveEditMode сообщает, какой режим
действует в данный момент. Все три эти свойства имеют тип перечисления
InkCanvasEditingMode, в котором определены следующие значения:
• Ink - рисование штрихов мышью или стилусом. Это подразумеваемое по умолчанию
значение свойства EditingMode.
• InkAndGesture - аналогично Ink, но распознает также жесты пользователя. Список
поддерживаемых жестов (Up, Down, Circle, ScratchOut, Tap и др.) определен в
перечислении System.Windows.Ink. ApplicationGesture.
• GestureOnly - только распознает жесты, никакие штрихи не рисуются.
• EraseByStroke - стирает весь штрих, которого коснулся стилус. Это подразумеваемое
по умолчанию значение свойства EditingModeInverted. I
• EraseByPoint - стирает только часть штриха, находящуюся непосредственно под
стилусом (как обычный ластик).
• Select - выделяет штрихи или другие элементы UIElement при касании, так чтобы
впоследствии сразу ко всем можно было применить операцию удаления, перемещения
или изменения размеров в границах InkCanvas/
• None - никак не реагирует на попытки ввода данных мышью или стилусом.
Применение режима Select к обычным элементам, не имеющим ничего общего с
рукописным вводом, — довольно интересная возможность, поскольку она позволяет
организовать примитивную область конструирования для размещения элементов. В
классе InkCanvas определено также 15 событий, генерируемых при выполнении таких
операций, как смена режима редактирования, корректировка, перемещение и изменение
размера выделенных элементов, запоминание и стирание штрихов и распознавание
жестов.

Документы

367

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

Документы
Элементы управления TextBlock и Label предназначены для отображения статического
текста, а элементы TextBox и RichTextBox - для показа редактируемого текста. Но в
части работы с текстами WPF может предложить и куда более развитую
функциональность!
В WPF имеется обширный набор классов для создания, просмотра, модификации,
организации и хранения высококачественных документов. В настоящем разделе мы
будем говорить о так называемых потоковых документах. Такой документ
(представленный объектом типа FlowDocument) содержит текст и другие данные,
которые требуется расположить так, чтобы оптимально использовалось отведенное под
документ место. Например, на мониторах с широким экраном можно автоматически
добавлять дополнительные колонки.
Создание потоковых документов
Класс FlowDocument наследует классу FrameworkContentElement, аналогу
FrameWorkElement, ориентированному на работу с содержимым. Все элементы типа
FrameworkContentElement, как и элементы типа FrameworkElement, поддерживают
привязку к данным, анимацию и другие механизмы WPF, но не участвуют в механизме
компоновки. При отображении на экране элементы FrameworkContentElement
располагаются внутри какого-то элемента FrameworkElement.

СОВЕТ
Как поддержка потоковых документов в WPF соотносится со спецификацией
XML Paper Specification (XPS)?
В отличие от документов с динамической компоновкой, описываемых в этом разделе,
XPS-документы имеют фиксированную компоновку и одинаково выглядят как на
экране, так и на бумаге. В каркас .NET Framework включены классы для создания и
просмотра
XPS-документов
(они
находятся
в
пространствах
имен
System.Windows.Xps и System.Windows.Documents), но для этих целей можно
пользоваться также инструментальными средствами, например Microsoft Word. В
WPF-приложениях XPS-документы обычно представляются объектами типа
FixedDocument
и
просматриваются с
помощью
элемента
управления
DocumentViewer.

368

Глава 11.Изображения, текст и другие элементы управления

XPS-документы во многом напоминают документы в формате Adobe PDF; для
других есть автономные программы для просмотра (на разных платформах), и их
можно просматривать в браузере (при наличии подходящего расширения). Одной из
уникальных особенностей XPS является тот факт, что это одновременно и «родной»
формат спулингового файла Windows (начиная с Windows Vista). Это означает, что
XPS-документы можно печатать без потери качества и точности воспроизведения, не
требуя никакой дополнительной работы от приложения, отправляющего документ на
печать.
Спецификацию XPS и положенную в ее основу спецификацию Open Packaging
Conventions (поддерживающие ее классы находятся в пространстве имен
System10.Packaging) можно найти по адресу http://microsoft.com/xps.
Еще одним классом, производным от FrameworkContentElement, является Element абстрактный класс, представляющий содержимое, которое можно поместить в
документ FlowDocument. В этом разделе мы рассмотрим различные подклассы
TextElement (все они находятся в пространстве имен System.Windows Documents) и
покажем, как с помощью их композиции можно создавать гибкие документы с
разнородным наполнением.
Простой потоковый документ
В следующей XAML-разметке показан простой объект FlowDocument, представляющий собой коллекцию абзацев Paragraph (типа TextElement) из черновика
первой главы этой книги.

Chapter 1
Why WPF?

In movies and on TV, the …




На рис. 11.10 мы видим результат визуализации этого XAML-кода. Если сделать
подобный элемент FlowDocument корнем XAML-файла, то его можно будет отобразить
в подходящей программе просмотра.
Есть два основных типа элементов TextElement — Block и Inline (оба класса являются
абстрактными, производными от TextElement). Block занимает прямоугольную область,
которая может разрываться только при переходе на другую страницу, a Inline заполняемую текстом область, которая, в принципе, может оказаться и
непрямоугольной (перетекать из конца одной строки в начало следующей). Элемент
FlowDocument может содержать только блоки Block в качестве дочерних элементов.
(Его свойство содержимого называется Block и имеет тип BlocksCollection.) Роль
элементов Inline мы рассмотрим после ТОго, как поближе ознакомимся с блоками.

Документы

369

Puc. 11.10. Простой документ типа FlowDocument
Класс Block
В WPF есть пять типов блоков:
•
Paragraph - содержит коллекцию Inlines, которая обычно и составляет содержание документа. В XAML содержимым элемента Paragraph часто бывает простой
текст, но внутри система обертывает этот текст объектом класса Run, производного от
Inline, который и добавляется в коллекцию Inlines - так же, как в случае TextBlock.
•
Section - группирует один или несколько блоков, не вводя никакой дополнительной структуры. Это удобно, когда нужно применить к нескольким блокам одно и
то же значение некоторого свойства, например Background или Foreground.
•
List - представляет коллекцию объектов типа ListItem в виде маркированного,
нумерованного или простого списка. Каждый ListItem может содержать коллекцию
блоков Block, так что создание типичного списка List подразумевает помещение
объекта Paragraph внутрь каждого ListItem. Свойство MarkerStyle (типа
TextMarkerStyle) позволяет задать различные стили форматирования маркеров - Box,
Circle, Disc (подразумевается по умолчанию) и Square - и номеров - Decimal,
LowerLatin, UpperLatin, LowerRoman и UpperRoman. Чтобы получить простой список,
нужно присвоить свойству MarkerStyle значение None.
•
Table - располагает содержимое в виде таблицы из строк и столбцов, наподобие
Grid, но все же ближе к HTML-таблице. Элементы Table, в отличие от Grid, могут
содержать только блоки Block (и элементы, описывающие структуру таблицы).
•
BlockUIContainer - содержит единственный элемент UIElement. Поэтому
BlockUlContainer - ключ к размещению разнообразного WPF-содержимого внутри
FlowDocument: изображений Image, видео, содержащегося внутри MediaElement,
кнопок Button, трехмерной графики внутри элемента Viewport 3D и т.д.

370

Глава 11.Изображения, текст и другие элементы управления

В листинге 11.1 демонстрируется использование всех пяти видов блоков в документе
FlowDocument. Результат визуализации этой разметки показан на рис. 11.11.
Листинг 11.1. Разметка документа FlowDocument, изображенного на рис. 11.11

WPF 4 Unleashed Notes from Chapter 1
Here are some highlights of WPF: Broad integration Resolution independence Hardware acceleration Declarative programming Rich composition and customization The technologies in the .NET Framework. Документы .NET Framework WPF WCF WF WCS ADO.NET ASP.NET Windows Forms ... 371 372 Глава 11.Изображения, текст и другие элементы управления
Рис. 11.11.Документ FlowDocument, в котором встречаются все пять видов блоков Элементы Paragraph используются в этом документе повсеместно, а элемент Section только в начале, чтобы задать для двух элементов Paragraph особые значения свойств Foreground, Background и LineHeight. Далее идет элемент List с настройками по умолчанию, соответствующими маркированному списку. Элемент BlockUIContainer содержит не только изображение Image, но и подпись к нему в виде элемента TextBlock. Оба эти элемента скомпонованы на панели StackPanel и помещены в элемент Viewbox, в результате чего они будут красиво масштабироваться при изменении ширины документа. Наконец, просто для демонстрации мы имитировали изображение с помощью таблицы Table. Отметим, что API класса Table (а значит, и структуре вложенных в Table элементов в XAML-коде) существенно отличается от Grid. Для определения столбцов мы помещаем элементы TableColumn в коллекцию Columns (аналог коллекции ColumnDefinitions в элементе Grid), но строки определяются непосредственно своим содержимым. Следовательно, Table содержит элемент TableRowGroup, внутри которого строки таблицы TableRow располагаются в том порядке, в котором должны присутствовать в документе, - сверху вниз. Ячейки TableCell внутри каждой строки TableRow заполняют столбцы последовательно, если только с помощью свойства ColumnSpan не задано другое поведение. TableCell - единственный элемент, который может содержать блоки Block, составляющие содержимое таблицы, в данном случае это абзацы Paragraph. Документы 373 Элемент Table может содержать несколько групп строки TableRowGroup! Строки, входящие в каждую группу, располагаются сразу под предыдущей группой. На рис. 11.11 видно, что получившаяся таблица внешне очень похожа на включенное в документ изображение Image. Разумеется, их поведение сильно различается. Текст в таблице Table можно выделять, и он масштабируется при увеличении документа. Но если изображение никогда не разрывается при переходе на другую страницу, то для таблицы это допустимо. Кроме того, если места не хватает, то содержимое отдельных ячеек может переноситься на новую строку. На рис. 11.12 показаны и разрыв, и перенос. Рис. 11.12. Другое представление документа на рис. 11.11 - часть таблицы оказалась на странице 2, а часть — на странице 3 Класс Inline Элементы Inline могут находиться внутри Paragraph, позволяя добавлять к тексту эффектное форматирование. В предыдущем разделе было сказано, что на самом деле объект Paragraph содержит не просто строку, а коллекцию объектов Inline. И, хотя абзац Paragraph, представленный в XAML, вроде бы содержит чистый текст, в действительности это одиночный объект класса Run, производного от Inline. В классе Run имеется строковое свойство Text, а его конструктор принимает строку. Таким образом, элемент Paragraph, определенный в XAML следующим образом: Here are some highlights of WPF: эквивалентен такому коду на С#: Paragraph p = new Paragraph(new Run(‚Here are some highlights of WPF:‛)); Остальные встраиваемые в абзацы элементы Inline можно разбить на три категории: отрезки (span), заякоренные блоки и все остальное. 374 Глава 11.Изображения, текст и другие элементы управления Отрезки. Наиболее распространенными отрезками являются элементы Bold, Italic, Underline и уже знакомый Hyperlink из главы 7. Все они наследуют классу Span, который можно использовать внутри Paragraph и напрямую для применения к тексту дополнительных эффектов. Хотя класс Paragraph и сам поддерживает возможность изменять начертание своего текста (делать его полужирным, курсивным и т.д.) путем установки таких свойств, как FontWeight и FontStyle, отрезки позволяют применять нужный эффект к более мелким участкам текста, чем целый абзац. В следующем элементе Paragraph, который показан на рис. 11.13, демонстрируются все виды отрезков: bold italic underline hyperlink superscript subscript strikethrough Puc. 11.13. Отрезки с различным форматированием внутри абзаца СОВЕТ Так как TextBlock хранит свое содержимое в виде коллекции Inline, то можно было бы заменить теги Paragraph в показанной выше разметке фрагментами с тегами TextBlock, и все работало бы по-прежнему. С другой стороны, элемент Label такую разметку не поддерживает. Свойства BaselineAlignment и TextDecorations, примененные к элементу Span, на самом деле являются общими для всех подклассов Inline, так что их можно спокойно сочетать с Bold, Italic и другими эффектами. Кроме того, как и в случае Paragraph, содержимое любого отрезка на самом деле представляет собой коллекцию объектов Inline, а не простую строку. Документы 375 В показанной выше ХАМL-разметке это означает, что каждый потомок Paragraph неявно обернут объектом Run. А заодно и то, что одни отрезки можно вкладывать в другие, как показано на примере следующего элемента Paragraph, визуализированного на рис. 11.14: a b c d e f g h i Рис. 11.14. Элемент Hyperlink, вложенный в Underline, который вложен в Italic, а тот, в свою очередь, вложен в Bold Заякоренные блоки. WPF содержит два подкласса Inline, необычных тем, что они используются как контейнеры для элементов Block. Это классы Figure и Floater - оба наследующие абстрактному классу AnchoredBlock. Figure - это в каком-то смысле мини-FlowDocument, который можно вкладывать в объемлющий FlowDocument. Внутреннее содержимое изолировано от внешнего, которое обтекает Figure. Например, в документе FlowDocument с текстом главы 1 я мог бы сделать так, чтобы текст абзацев обтекал изображения (рисунки в этой книге именно так и размещены). Этого можно достичь следующим образом: Chapter 1 Why WPF?
In movies and on TV, the …
Поскольку Figure может содержать элементы Block, значит, внутрь него можно помещать Table, Paragraph и другие элементы. Но в данном случае нам достаточно поместить один элемент BlockUIContainer, который содержит изображение. Результат показан на рис. 11.15. 376 Глава 11.Изображения, текст и другие элементы управления Местоположением Figuге можно управлять с помощью свойств HorizontalAnchor и VerticalAnchor (типа FigureHorizontalAnchor и FigureVerticalAnchor соответственно). По умолчанию HorizontalAnchor равно ColumnRight, a VerticalAnchor -ParagraphTop. Оба свойства позволяют задавать различные режимы размещения относительно текущего столбца либо абзаца или даже страницы в целом. На рис. 11.16 показаны некоторые альтернативные способы размещения элемента Figure, изображенного на рис. 11.15, при различных значениях HorizontalAnchor и/или VerticalAnchor. Рис. 11.15. В третьем абзаце документа FlowDocument находится элемент Figure, содержащий изображение Image Элемент Floater - упрощенный вариант Figure. Он может содержать произвольные блоки Block, но не поддерживает ни позиционирование относительно границ страницы, ни даже распространение на несколько столбцов. Вместо двух свойств HorizontalAnchor и VerticalAnchor в нем есть только одно простое свойство HorizontalAlignment (типа HorizontalAlignment), которое может принимать значения Left, Center, Right и Stretch. Если вам ни к чему полная функциональность Figure, то можете использовать вместо него более легкий элемент Floater. Прочие элементы Inline. Два оставшихся элемента Inline не имеют ничего общего за исключением того факта, что они не наследуют ни одному из классов Span или AnchoredBlock. Один из них - LineBreak, который играет роль символа новой строки. Если поместить пустой элемент LineBreak между двумя символами в абзаце, то второй из них окажется в начале новой строки. СОВЕТ Чтобы вставить в FlowDocument разрыв не строки, а страницы, задайте свойство BreakPageBefore для того элемента Paragraph, перед которым нужно перейти на новую страницу. Свойство BreakPageBefore определено в классе Block, так что применимо также к Section, List, BlockUIContainer и Table. Документы 377 HorizontalAnchor=‖ColumnLeft‖ HorizontalAnchor=‖PageCenter‖ HorizontalAnchor=‖PageRight‖ и VerticalAnchor=‖PageTop‖ Рис. 11.16. Управление местоположением Firure с помощыо свойств HorizontalAnchor и VerticalAnchor 378 Глава 11.Изображения, текст и другие элементы управления И последний класс, производный от Inline, — это InlineUIContainer, который отличается от BlockUIContainer лишь тем, что может быть вложен внутрь Paragraph и будет размещаться в одном потоке с остальным текстом. Как и BlockUlContainer, он может содержать видео в элементе MediaElement, кнопки Button трехмерную графику в элементе Viewport3D и т. д., но чаще всего его применяют для того, чтобы включить в поток небольшое изображение. Следующий абзац, показанный на рис. 11.17, демонстрирует, как можно вставить значок RSS-ленты рядом с гиперссылкой Hyperlink на эту ленту: You can read more about this on my blog ( subscribe ), which I try to update once a month. Puc. 11.17. Элемент Paragraph с изображением Image в общем потоке - благодаря InlineUIContainer Отображение потоковых документов Выше уже упоминалось, что элемент FlowDocument можно отображать (и редактировать) внутри RichTextBox. Хотя редактирование можно запретить, установив для элемента RichTextBox свойство IsReadOnly равным true, RichTextBox не задумывался как основной элемент для чтения документов. Вместо этого WPF предлагает три дополнительных элемента для отображения потоковых документов. Поначалу разобраться в них, возможно, и нелегко, но различия достаточно понятны: • FlowDocumentScrollViewer - отображает документ как один непрерывный файл с полосой прокрутки, как в режиме веб-документа в Microsoft Word (аналогично доступному только для чтения RichTextBox, помещенному в ScrollViewer). • FlowDocumentPageViewer - отображает документ в виде набора отдельных страниц, как в режиме полноэкранного чтения в Microsoft Word. • FlowDocumentReader объединяет FlowDocumentScrollViewer и FlowDocumentPageViewer в один элемент управления и предлагает дополнительную функциональность, в частности встроенный текстовый поиск. (Такой элемент управления вы получаете по умолчанию, если сделаете FlowDocument корневым элементом XAML-файла.) Документы 379 FlowDocumentScrollViewer FlowDocumentPageViewer FlowDocumentReader Рис. 11.18. Текст главы 1 в каждом из контейнеров документов FlowDocument 380 Глава 11.Изображения, текст и другие элементы управления На рис. 11.18 показаны различия между этими элементами управления на примере документа FlowDocument, содержащего черновик первой главы. Элемент FlowDocumentReader наделен весьма развитой функциональностью (наподобие стандартных программ просмотра XPS- и PDF-файлов), но если вам не нужна возможность переключаться между режимами прокрутки и постраничного просмотра, то, пожалуй, лучше ограничиться более простыми средствами просмотра. И FlowDocumentPageViewer, и FlowDocumentReader (в режиме постраничного просмотра) автоматически добавляют или убирают колонки при уменьшении либо увеличении масштаба, чтобы оптимально использовать имеющееся место. Отметим, что FlowDocumentScrollViewer не содержит инструментов для управления масштабом, как остальные два элемента, но их можно добавить, присвоив свойству IsToolBarVisible значение true. Добавление комментариев Все три элемента для просмотра документов типа FlowDocument (а также DocumentViewer, предназначенный для просмотра документов типа FixedDocument) поддерживают добавление комментариев, то есть позволяют пользователю подсвечивать часть содержимого или присоединять заметки в виде печатного либо рукописного текста. Странно то, что для реализации этой возможностям вы должны самостоятельно организовать пользовательский интерфейс - никаких элементов управления по умолчанию не предусмотрено. Конструировать свой интерфейс для ввода комментариев утомительно, но не слишком трудно. На помощь приходит класс AnnotationService в пространстве имен System.Windows.Annotations, в котором имеются команды для всех нужных функций: • CreateTextStickyNoteCommand присоединяет новый текстовый элемент StickyNoteControl в качестве комментария к выделенному тексту. • CreateInkStickyNoteCommand присоединяет новый рукописный элемент StickyNoteControl в качестве комментария к выделенному тексту. • DeleteStickyNotesCommand удаляет выделенные в данный момент элементы StickyNoteControl. • CreateHighlightCommand подсвечивает выделенный текст цветом, переданным команде в качестве параметра. • ClearHighiightsCommand удаляет подсветку с выделенного в данный момент текста. В листинге 11.2 определено окно Window, в котором над элементом FlowDocumentReader размещено несколько простых кнопок. С каждой кнопкой ассоциирована одна из описанных выше команд. Листинг 11.2. Windowl.xaml - пользовательский интерфейс для FlowDocumentReader с возможностью добавления комментариев Пространству имен .NET System.Windows.Annotations сопоставлен префикс пространства имен XMLа, с помощью которого мы ссылаемся на команды в классе AnnotationService. Хотя AnnotationService - часть PresentationFramework, это пространство имен почему-то не включено в состав стандартного для WPF пространства имен XML.Чтобы команды заработали, в каждой кнопке в качестве цели команды указан элемент FlowDocumentReader. Кнопки становятся активными и неактивными автоматически в зависимости от контекста, в котором допустима соответствующая команда. Осталось только определить методы Uninitialized и OnClosed, на которые есть ссылки в XAML-файле. В листинге 11.3 приведен застраничный файл для разметки, предоставленной в листинге 11.2. 382 Глава 11. Изображения, текст и другие элементы управления Листинг 11.3. Windows.xaml.cs - процедурный код для элемента FlowDocumentReader с возможностью добавления комментариев using using using using using System; System.IO; System.Windows; System.windows.Annotations; System.windows.Annotations.Storage; public partial class Window1:Window { FileStream stream; public Window1() { InitializeComponent(); } protected void OnInitialized(object sender, EventArgs e) { //включить и загрузить комментарии AnnotationsServace service = AnnotationService.GetService(reader); if (service == null) { stream = new FileStream("storage.xml", FileMode.OpenOrCreate); service = new AnnotationService(reader); AnnotationStore store = new XmlStreamStore(stream) store.AutoFlush = true; service.Enable(store); } } protected void OnClosed(object sender, EventArgs e) { // Выключить и сохранить комментарии AnnotationService service = AnnotationService.GetService(reader); if (service != null && service.IsEnabled) { service.Disable(); stream.Close(); } } } Основная задача методов OnInitialized и OnClosed - включать и выключать службу AnnotationService, ассоциированную с объектом FlowDocumentReader. Однако при включении службы необходимо также указать поток Stream, в котором будут сохранять комментарии. В листинге 11.3 для этого используется отдельный XML-файл в текущем каталоге. При закрытии приложения все комментарии сохраняются и появляются снова при следующем запуске (при условии, что файл storage.xml существует и не поврежден). Диапазонные элементы управления 383 На рис. 11.19 показано как выглядит это окно с добавленными комментариями. Рис. 11.19. Кнопки в верхней части окна позволяют работать с комментариями в документе FlowDocument СОВЕТ Элементы StickyNoteControl, которыми представлены комментарии, — это полноценные элементы управления WPF (находятся в пространстве имен System.Windows.Controls), Поэтому их внешний облик можно полностью изменить, задав другой шаблон. Диапазонные элементы управления Диапазонные элементы управления не предназначены для визуализации произвольного содержимого, как однодетные или многодетные элементы. Диапазонный элемент просто хранит и отображает числовое значение, принадлежащее определенному диапазону. Большую часть своей функциональности диапазонные элементы наследуют от абстрактного класса RangeBase. B нем определены свойства типа double, в которых хранятся текущее значение и границы диапазона:Value, Minimum, Maximum. Там же определено событие ValueChanged. В этом разделе мы рассмотрим два основных диапазонных элемента: ProgressBar и Slider. В WPF имеется также примитивный элемент ScrollBar, производный от 384 Глава 11. Изображения, текст и другие элементы управления RangeBase, но маловероятно, что вы захотите воспользоваться им напрямую. Вместо этого лучше прибегнуть к классу ScrollViewer, который был описан в главе 5. Элемент ProgresBar Если бы мир был идеален, то вам никогда не пришлось бы использовать в своих программах индикаторов выполнения PrоgressBar. Однако некоторые операции всетаки выполняются долго, и ProgressBar вселяет в пользователей уверенность в том, что программа что-то делает. Поэтому расположенный в нужном месте индикатор значительно улучшает впечатление пользователей от программы.(Разуметься, впечатление было бы еще лучше, если бы медленная программа выполнялась быстрее!). На рис. 11.20 показано, как выгляди элемент управления WPF ProgressBar по умолчанию. Рис.11.20. Элемент Управления WPF ProgressBar По умолчанию свойство Minimum элемента ProgressBar равно 0, а свойство Maximum100. Он добавляет к своему базовому классу RangeBase еще два свойства: • IsIndeterminate - если оно равно true, то в ProgressBar показывается обобщенная анимация, при этом значения свойств Minimum, Maximum и Value не учитываются. Это удобно, когда вы не знаете заранее, сколько времени займет операция, или ленитесь написать код, необходимый для показа реального положения вещей! • Orientation - по умолчанию равно Horizontal, но может быть сделано равным Vertical, тогда индикатор будет двигаться сверху вниз, а не слева направо. Мне не доводилось встречать приложения, в которых индикаторы выполнения имеют вид «термометра», если не считать старомодных полноэкранных установщиков. Но если очень хочется, это свойство позволяет добиться такого эффекта! FAQ Как с помощью ProgressBar показать, что операция приостановлена или остановлена из-за ошибки. Начиная с Windows Vista индикатор выполнения в Win32 может визуализировать состояние приостановки (желтым цветом) и остановки/ошибки (красным цветом). К сожалению, в элементе WPF ProgressBar такая возможность не реализована. Если вы хотите получить подобный эффект, то должны будете создать новые шаблоны для этих состояний и применять их к элементу программно с помощью техники описанной в главе 14 «стили, шаблоны, обложки и темы». Диапазонные элементы управления 385 Элемент Slider Элемент Slider (ползунок) несколько сложнее, чем ProgressBar, так как позволяет изменять текущее значение, перемещая ползунок на любое число необязательных делений . Этот элемент изображен на рис. 11.21. Рис 11.21. Элемент управления WPF Slider По умолчанию значение Minimum для него равно 0, а значение Maximum - 10. Кроме того, в классе Slider определено свойство Orientation (по умолчанию равное Horizontal), а также ряд свойств для задания положения и частоты делений, положения и точности всплывающих подсказок ToolTip, которые показывают текущее значение по мере перемещения ползунка, и признак, говорящий о том, должен ли ползунок точно совмещаться с дискретными делениями или может перемещаться плавно. Для работы с клавиатурой в классе Slider имеются также свойства Delay и Interval, которые по своему поведению очень похожи на одноименные свойства элемента RepeatButton. Чтобы появились деления, необходимо присвоить свойству TickPlacement значение TopLeft, BottomRight или Both. Странные названия объясняются желанием учесть обе ориентации Slider. Когда TickPlacement равно BottomRight, деления располагаются под ползунком, если он ориентирован горизонтально, и справа от него - если вертикально. Аналогично, когда TickPlacement равно TopLeft, деления располагаются над ползунком, если он ориентирован горизонтально, и слева от него - если вертикально. Когда TickPlacement равно None (режим по умолчанию), ползунок выглядит, как показано на рис. 11.22. Рис.11.22. Элемент Slider без делений Рис. 11.23. Элемент Slider поддерживает выделение меньшего поддиапазона У элемента Slider есть одна интересная особенность он умеет отображать меньший диапазон текущего, как показано на рис. 11.23. Если свойство IsSelectionEnabled равно true, то свойствам SelectionStart и SelectionEnd можно присвоить значения границ такого «поддиапазона». В классе нет никаких встроенных средств, позволяющих задавать поддиапазон с помощью клавиатуры или мыши, и не гарантируется, что ползунок всегда остается в пределах поддиапазона. Эта возможность позволяет сделать ползунок похожим на тот что используется в Windows Media Player, где закрашенная область показывает, какая часть воспроизводимого файла уже загружена. 386 Глава 11. Изображения, текст и другие элементы управления Календарные элементы управления В WPF 4 появилось два новых календарных элемента управления позволяющих очень наглядно выбирать и отображать даты: Calendar и DatePicker. Их отсутствие в предыдущих версиях WPF ощущалось очень сильно так что они стали желанным дополнением. Элемент Calendar Элемент управления Calendar, показанный на рис. 11.24, отображает календарь, очень похожий на стандартный календарь в Windows. С помощью свойства DisplayMode он поддерживает три разных режима. Щелкая по тексту в заголовке календаря, пользователь может расширять временной период, проходя от месяца (Month) к году (Year) или к десятилетию (Decade), а щелчок по любой ячейке календаря уменьшает период. В отличие от календаря Windows, элемент WPF Calendar не поддерживает режим показа столетия, а готовый стиль, к сожалению, не предусматривает приятной глазу анимации при переключении режима. Рис 11.24 Элемент WPF Calendar при различных значениях DisplayMode в том виде, в каком он выглядел бы 20 апреля 2012 года Свойство DisplayDate элемента Calendar (типа DateTime) по умолчанию инициализируется текущей датой (на рис. 11.24 это 20 апреля 2012 года). Дата DisplayDate при открытии календаря всегда видна, хотя в режиме Month она ничем визуально не отличается от других дат. Выделение 20 апреля серым цветом на рис. 11.24 объясняется тем, что Calendar подсвечивает сегодняшнюю дату независимо от значения DisplayDate. Чтобы отключить подсветку, присвойте свойству IsTodayHighlighted значение false. В зависимости от свойства SelectionMode в календаре можно выделять одну или несколько дат: • SingleDate - в любой момент времени может быть выделена только одна дата, которая хранится в свойстве SelectedDate. Это режим по умолчанию. • SlngleRange - можно выделять несколько дат, но они должны образовывать один непрерывный диапазон. Выделенные даты хранятся в свойстве SelectedDates. Календарные элементы управления 387 MultipleRange - выделенные даты не обязаны быть соседними, они хранятся в свойстве SelectedDates. • None - выделять даты вообще нельзя. Чтобы ограничить диапазон дат, отображаемых в элементе Calendar, можно задать свойства DisplayDateStart и/или DisplayDateEnd (типа DateTime). На рис. 11.25 показано, как это выглядит в каждом режиме DisplayMode. Иногда результат получается нелепым, потому что компоновка «шесть столбцов недель» в режиме Month и 4x4 в остальных режимах задана жестко. • Рис.11.25. Так выглядит календарь, когда DisplayDateStart равно 10 апреля 2012, а DisplayDateEnd – 25 апреля 2012. Можно вместо этого указать диапазоны, в которых запрещено выделять даты, хотя они и отображаются. Для этого служит свойство BlackoutDates, содержащее коллекцию объектов типа CalendarDateRange. На рис. 11.26 показано, что получается, когда в BlackoutDates записано два диапазона: Это работает только в режиме Month. Рис 11.26. Так выглядит календарь, когда в коллекции BlackoutDates находятся два диапазона CalendarDateRanges 388 Глава 11. Изображения, текст и другие элементы управления СОВЕТ Типом свойства BlackoutDates является класс CalendarBlackoutDatesCollection, производный от ObservableCollaection. В нем есть один особенно полезный метод – AddDatesInPast. Обратившись к нему, можно запретить все даты равные текущей. Но поскольку вызвать его можно только в процедурном коде, иногда проще явно указать элемент CalendarDateRange, задав в нем DateTimeMinValue (1 января 0001 года) в качестве значения Start и DateTime.Today минус один день – в качестве значения End. Свойство FirstDayOfWeek класса Calendar предназначено для культур, в которых первым днем недели считается воскресенье, но, а в принципе, в него можно записать любое значение из перечисления System.DayOfWeek, и тогда отображение соответственно измениться. В классе Calendar имеются также события, отражающие все существенные изменения: DisplayDateChanged, DisplayModeChanged, SelectionModeChanged и SelectionDatesChanged (возникают при выделении как одной, так и нескольких дат). ЭЛЕМЕНТ DatePicker Еще один календарный элемент – DatePicker – по существу, представляет собою поле TextBox для отображения и ввода даты, с которым ассоциирован всплывающий элемент Calendar, позволяющий изменять дату визуально. Внешний вид элемента DatePicker изображен на рис. 11.27. Рис. 11.27. Элемент WPF DatePicker вместе с ассоциированным всплывающим календарем При щелчке по значку календаря появляется уже знакомый нам элемент Calendar , которому DatePicker и обязан большинством своих интересных возможностей. DatePicker обладает теми же свойствами и событиями, что и Calendar за исключением свойств DisplayMode, SelectionMode и соответствующих им событий изменения. Для всплывающего календаря всегда установлены режимы DisplayМode=Month в SelectionMode=SingleDate. Поскольку выделить можно только одну дату, то вместо события SelectedDatesChanged в классе DatePicker Резюме 389 определено событие SelectedDateChanged. По какой-то непонятной причине y DatePicker нет также события DisplayDateChanged, присутствующего в Calendar. В классе DatePicker имеется также несколько уникальных свойств и событий для управления поведением TextBox и взаимодействия со всплывающим календарем. Булевское свойство IsDropDownOpen позволяет открывать и закрывать всплывающий календарь из программы, а также опрашивать его текущее состояние. События CalendarOpened и CalendarClosed генерируются, когда календарь соответственно открывается или закрывается. Свойство SelectedDateFormat определяет формат строки, помещаемой в TextBox после выбора даты в календаре. По умолчанию оно равно Short, что соответствует формату 4/20/2012. Можно задать значение Long, что даст представление в виде Friday, April 20, 2012. Строку, отображаемую в поле TextBox, молено установить или получить с помощью свойства Text. Если введена строка, которую невозможно интерпретировать как дату, то генерируется событие DateValidationError. Поле ввода в элементе DatePicker (типа класса DatePickerTextBox, производного от TextBox) нельзя назвать образцом изящества — оно странно выглядит при наведении указателя мыши, а на значке календаря по какой-то необъяснимой прихоти разработчиков всегда отображается число «15». Единственный способ изменить его внешний вид — полностью заменить шаблон. Резюме Итак, мы ознакомились с основными встроенными элементами управления, применяемыми для создания традиционных (и не очень традиционных) пользовательских интерфейсов. Их внешний вид можно радикально изменить с помощью приемов, описанных в главе 14, но описанное выше поведение останется неизменным. IV Средства для профессиональных разработчиков Глава 12 «Ресурсы» Глава 13 «Привязка к данным» Глава 14 «Стили, шаблоны, обложки и темы» 12 Ресурсы • Двоичные ресурсы • Логические ресурсы В каркас .NET Framework встроена общая инфраструктура пакетирования и доступа к ресурсам — частям приложения или компонента, отличным от кода. К ним относятся, например, растровые изображения, шрифты, аудио- и видеофайлы и таблицы строк. Как и во многих других случаях, WPF не только пользуется базовой системой ресурсов .NET, но и немного расширяет ее. WPF поддерживает два принципиально разных вида ресурсов: двоичные и логические. Двоичные ресурсы Первый тип - двоичные ресурсы — это в точности то, что понимается под ресурсами в остальных частях .NET Framework. В WPF-приложениях в этой роли обычно выступают традиционные ресурсы вроде растровых изображений. Но и откомпилированный XAML-код также хранится в виде двоичного ресурса. Существует три способа пакетирования двоичных ресурсов: • Внедрить в сборку • Оставить в виде автономных файлов, известных приложению на этапе компиляции Оставить в виде автономных файлов, не известных приложению на этапе компиляции Двоичные ресурсы приложения часто относят к одной из двух категорий: локализуемые ресурсы, которые должны изменяться в зависимости от текущей культуры, и не зависящие от языка (или нелокализуемые), то есть одинаковые при любой культуре. В этом разделе мы остановимся на различных способах определения, доступа и локализации ресурсов. 394 Глава 12.Ресурсы Определение двоичного ресурса Типичная процедура определения двоичного ресурса заключается в том, чтобы добавить файл в проект Visual Studio и выбрать в сетке свойства подходящее действие при построении, как показано на рис. 12.1 на примере файл изображения logo.jpg. Рис. 12.1. Пометка файла как двоичного ресурса в Visual Studio Visual Studio поддерживает несколько действий при построении для WPF-приложений, два из которых имеют непосредственное отношение к двоичным ресурсам: • Resource - внедрить ресурс в сборку (или в соответствующую конкретной культуре сателлитную сборку). • Content - оставить ресурс в виде автономного файла, но добавить в сборку специальный атрибут (AssemblyAssociatedContentFile), в котором говорится о существовании и относительном местоположении файла. Если вы вручную редактируете проект для программы MSBuild, то такой файл можно добавить следующим образом: где BuildAction - название действия при построении. Этот элемент может содержать вложенные элементы, уточняющие поведение, например: Always Если вы хотите оставить ресурсы в виде автономных файлов, то добавлять их в проект, указывая действие при построении Content, необязательно; можно просто поместить их в известное на этапе выполнения место и не добавлять в проект вовсе. Но так поступать не рекомендуется, потому что доступ к ре сурсу оказывается менее естественным Двоичные ресурсы 395 (см. следующий раздел). Тем не менее иногда невозможно избежать использования ресурсов, неизвестных на этапе компиляции, например файлов, которые динамически генерируются во время работы программы. ПРЕДУПРЕЖДЕНИЕ Избегайте действия при построении Embedded Resource! Действие при построении Resource часто путают с похожим действием EmbeddedResource (в сетке свойств Visual Studio оно называется Embedded Resource). И то и другое приводит к внедрению двоичного ресурса в сборку, но в WPF-проектах второе использовать не следует. Действие Resource было добавлено специально для WPF, a EmbeddedResource появилось раньше WPF (и применяется для внедрения двоичных ресурсов в проектах Windows Forms). Классы WPF, ссылающиеся на ресурсы по их унифицированным идентификаторам (см. следующий раздел), предназначены только для работы с ресурсами, для которых было указано действие при построении Content или Resource. Это также означает, что на ресурсы, внедренные с помощью действия Content или Resource, можно легко ссылаться из XAML-разметки, тогда как для ресурсов, внедренных с помощью действия EmbeddedResource, это невозможно (по крайней мере, без написания дополнительного кода). Ресурсы следует внедрять в сборку (указывая действие при построении Resource), если либо они локализуемые, либо вам кажется, что иметь всего один двоичный файл лучше, чем включать в дистрибутив автономный файл, пусть даже его можно заменять независимо от кода. Если ни одно из этих условий не выполнено или необходим также доступ к содержимому ресурса из внешних программ (быть может, из HTML-страниц, генерируемых приложением), то наиболее подходящим вариантом будет действие при построении Content. Доступ к двоичным ресурсам Неважно, внедрены ли ресурсы с помощью действия при построении Resource, оставлены в виде автономных файлов, связанных с приложением за счет действия при построении Content, или оставлены в виде автономных файлов без какой-либо специальной обработки на этапе компиляции, WPF предоставляет механизм для доступа к ним как из кода, так и из XAML, — по унифицированному идентификатору ресурса (URI). Имеется конвертер типа, который позволяет задавать URI в XAMLразметке в виде простой строки, с несколькими встроенными упрощениями для наиболее распространенных случаев. Это можно видеть на примере исходного кода приложения Photo Gallery из главы 7 «Структурирование и развертывание приложения». В следующем взятом оттуда фрагменте XAML имеются ссылки на несколько изображений, включенных в проект с помощью действия при построении Resource: 396 Глава 12. Ресурсы Отметим, что тот же самый XAML-код будет работать и в случае, когда все GIF-файлы включены в проект с действием при построении Content, а не Resource (при условии, что автономные файлы скопированы в один каталог с исполняемым файлом). Но если автономные GIF-файлы не включены в проект то эта разметка работать не будет. ПРЕДУПРЕЖДЕНИЕ Откомпилированный XAML-код не может ссылаться на двоичный ресурс в текущем каталоге по имени файла без указания каталога, если этот файл не был включен в проект! Часто удивляются, что откомпилированный XAML-код, в отличие от автономного, не может следующим образом ссылаться на произвольный файл в текущем каталоге: Если вам необходимо, чтобы ресурс остался автономным, и включать его в проект вы не хотите, то есть несколько альтернативных решений. Одно (плохое) состоит в том, чтобы указать полный путь к файлу: Более приемлемая альтернатива - воспользоваться довольно странным синтаксисом, который мы опишем ниже, в разделе "Доступ к ресурсам в первоисточнике": Чтобы разобраться в механизме доступа к двоичным ресурсам, неважно, идет ли речь об элементе Image или каком-то другом, нужно понимать, как устроен URI, адресующий внедренный или автономный ресурс. В табл. 12.1 перечислены основные форматы URI в XAML-разметке. Отметим, что не все они доступны приложениям с частичным доверием. Отметим, что первые два варианта в табл. 12.1 годятся как для внедренных, так и для автономных двоичных ресурсов. Это означает, что автономный ресурс можно заменить внедренным (или наоборот), не внося никаких изменений в ХАМL-разметку. Двоичные ресурсы 669 Таблица 12.1. URI для доступа к двоичным ресурсам из XAML-разметки (ресурс называется logo.jpg) Если URI имеет вид... logo.jpg To ресурс... Внедрен в текущую сборку или является автономным и находится в той же папке, что и текущая XAML-страница либо сборка (последнее верно, только если для ресурса в проекте было указано действие при построении Content) A/B/logo.jpg c:\temp\logo.jpg Внедрен в текущую сборку с использованием внутренней структуры подпапок (А\В), определенной на этапе компиляции, или является автономным и находится в подпапке А\В относительно текущей XAML-страницы либо сборки (последнее верно, только если для ресурса в проекте было указано действие при построении Content) Автономный и находится в локальной папке с:\ temp file://c:/temp/logo.jpg Автономный и находится в локальной папке c:\temp \\pc1\images\logo.jpg Автономный и находится в общей папке \\pcl\images http://adamnathan.net/logo.jpg Автономный и находится на веб-сайте adamna- than.net /MyDll;Component/logo.jpg Внедрен в другую сборку с именем MyDll.dll или MyDll.exe /HyDll;Component/A/B/logo. Внедрен в другую сборку с именем MyDll.dll или jpg MyDll.exe с использованием внутренней структуры подпапок (А\В), определенной на этапе компиляции pack://siteOf Origin:,,,/logo, jpg pack://siteOf Origin:,,, /А/B/logo. j pg Автономный в первоисточнике Автономный в первоисточнике в подпапке А\В FAQ Что происходит при попытке доступа к ресурсу по медленной или недоступной сети? В табл. 12.1 сказано, что двоичные ресурсы могут находиться в потенциально ненадежных источниках, таких как веб-сайт или общая папка. При этом доступ производится синхронно, то есть вы будете наблюдать, как приложение «зависает» на время, необходимое для скачивания всего ресурса до последнего бита. Ко всему прочему, ошибка при доступе к ресурсу возбуждает неперехватываемое исключение. 398 Глава 12. Ресурсы Идея об использовании подпапок в контексте внедренных ресурсов может показаться странной, но на самом деле это удобный способ организовать внедренные ресурсы так же, как вы обычно организуете автономные файлы. Пусть, например, файл logo.jpg находился в папке images в проекте Visual Studio; в файле проекта это описывается строкой: or Тогда вне зависимости от того, чем является logo.jpg во время выполнения автономным файлом в подпапке images или внедренным в сборку ресурсом,- обращаться к нему можно следующим образом: Последние четыре строки в табл. 12.1 нуждаются в дополнительных пояснениях. Первые два варианта позволяют получать доступ к двоичным ресурсам, внедренным в другую сборку, а последние два - к ресурсам, находящимся в так называемом первоисточнике (site of origin). Доступ к ресурсам, внедренным в другую сборку Возможность легко получать доступ к двоичным ресурсам, внедренным в другую сборку, очень удобна (и дает большую свободу при обновлении ресурсов, не заставляя заменять основной исполняемый файл), но синтаксис выглядит странновато. Как видно из табл. 12.1, URI записывается в виде AssemblyReference; Component/ResourceName где AssemblyReference идентифицирует конкретную сборку, a Component - ключевое слово, которое нельзя изменять. ResourceName- это имя файла (может включать подпапки). AssemblyReference может быть простым отображаемым именем сборки или включать необязательные части идентификатора сборки .NET: номер версии и маркер открытого ключа (если это сборка со строгим именем). Таким образом, имеется четыре варианта записи AssemblyReference: • • • • AssemblyName AssemblyName;vVersionNumber (префикс v обязателен) AssemblyName;PublicKeyToken AssemblyName;vVersionNumber;PublicKeyToken Доступ к ресурсам в первоисточнике Приложения с полным доверием могут содержать жестко защитый унифицированный указатель ресурса (URL) или путь к файлу для автономных двоичных ресурсов, но с точки зрения сопровождения разумнее воспользоваться понятием первоисточника. (К тому же для приложений с частичным доверием альтернативы просто нет.) Во время выполнения первоисточнику могут быть сопоставлены различные физические места Двоичные ресурсы 671 в зависимости от способа развертывания приложения: • Для приложения с полным доверием, установленного с помощью установщика Windows, первоисточником будет корневая папка приложения. • Для ClickOnce-приложения с полным доверием первоисточником будет URL или UNC-путь, из которого было развернуто приложение. • Для ХВАР- или ClickOnce-приложения с частичным доверием первоисточником будет URL или UNC-путь к месту, где находится приложение. • Для автономных XAML-страниц, просматриваемых в браузере, первоисточника нет. При попытке воспользоваться ресурсом возникает исключение. Формат записи первоисточника еще более чудной, чем при доступе к ресурсу, внедренному в другую сборку! Необходимо указывать префикс pack://siteOfOrigin:…/, за которым следует имя ресурса (возможно, содержащее подпапки). Отметим, что siteOfOrigin - ключевое слово, а не строка, замещаемая другим текстом, так что записывать его нужно без изменений.Для приложения с полным доверием, установленного с помощью установщика Windows, первоисточником будет корневая папка приложения. FAQ Откуда взялся этот ужасный синтаксис с тремя запятыми? Формат URI со схемой pack определен в спецификации XML Paper Specification (XPS), которую можно найти по адресу http://microsoft.com/whdc/xps/xpsspec. mspx. Вот как он описан: pack://packageURI/partPath Здесь packageURI - это фактически один URI внутри другого, поэтому он кодируется путем преобразования символов косой черты в запятые. packageURI может указывать на XPS-документ, например file:///C:/Document.xps, а в закодированном представлении получится file:…С:,Document.xps. Что же касается WPF-npoграмм, то это может быть один из двух URI, которые на этой платформе имеют особый смысл: • siteOfOrigin:/// (кодируется в виде siteOf Origin:…) • application:/// (кодируется в виде application:…) Следовательно, три запятых - это всего лишь три закодированных косых черты, а не место для подстановки необязательных параметров! (Отметим, что можно было бы задать две косых черты/запятых вместо трех.) Пакет application:/// неявно используется во всех ссылках на ресурсы, приведенных в табл. 12.1, за исключением тех, где встречается siteOfOrigin. (Объясняется это тем, что участвующие в механизме объекты классов WPF реализуют интерфейс IUriContext. В этом интерфейсе определено всего одно свойство Basellri, задающее контекст для относительных URI.) Иными словами, следующий URI, задаваемый в XAML-разметке: logo.jpg 400 Глава 12. Ресурсы на самом деле представляет собой сокращенную запись для pack://application:,,,/logo.jpg а такой URI: /MyDll:Component/logo.jpg будет сокращенной записью для pack ://application:,,,/MyDll; Component/logo.jpg При желании можно использовать в XAML и такую более длинную и более явную запись URI, но разумных причин для этого не видно. Доступ к ресурсам из процедурного кода При создании URI ресурсов в программе на языке C# не разрешается использовать применяемые в XAML сокращенные формы записи, показанные в табл. 12.1. Вместо этого необходимо конструировать URI из полного URI со схемой pack или из абсолютного пути либо URL. Например, в следующем фрагменте свойству Source объекта Image присваивается содержимое файла logo.jpg: Image image = new Image(); Image.Source = new BitmapImage(new Uri("pack://application:,,,/logo. jpg")); В результате создается объект типа System.Windows.Media.Imaging.BitmapImage (этот механизм работает для таких популярных графических форматов, как JPEG, PNG, GIF и BMP), являющегося косвенным потомком абстрактной класса ImageSource (это тип свойства Source). Сам URI представлен объектом типа System.Uri. Конструкция pack://application:,,,/ работает только для ресурсов, принадлежащих текущему проекту, для которых указано действие при построении Resource или Content. Чтобы сослаться на автономные файлы, не имеющие отношения к проекту, по относительному имени, проще всего воспользоваться URI на базе siteOfOrigin. Локализация двоичных ресурсов Если приложение содержит двоичные ресурсы, относящиеся к определенным культурам, то их можно поместить в сателлитные сборки (по одной на каждую культуру), которые будут автоматически загружаться при необходимости. Если вы именно так и делаете, то, скорее всего, в пользовательском интерфейсе вашего приложения есть еще и строки, нуждающиеся в локализации. Инструментальное средство LocBaml, входящее в состав Windows SDK, позволяет выполнять локализацию строк и других частей приложения, не выдергивая их из XAML и не применяя вручную механизмы косвенного доступа. В этом разделе мы рассмотрим основы работы с LocBaml и сателлитными сборками. Двоичные ресурсы 401 Подготовка проекта для нескольких культур Чтобы задать подразумеваемую по умолчанию культуру для ресурсов и автоматически построить подходящую сателлитную сборку, необходимо добавить в файл проекта элемент UICulture. В Visual Studio нет средств, позволяющих сделать это непосредственно в интегрированной среде,поэтому откройте файл в редакторе и внесите изменения вручную. СОВЕТ Можно открыть файл проекта и не покидая Visual Studio. Но для этого нужно сначала выгрузить его из текущего решения (щелкнув по проекту правой кнопкои мыши и выбрав нужный пункт из контекстного меню). Сделав это снова щелкните по проекту правой кнопкой мыши и выберите из контекстного меню пункт Edit (правка). Элемент UlCulture нужно помещать внутрь некоторых или всех элементов РгоpertyGroup, соответствующих тем конфигурациям построения, которые вас интересуют (Debug, Release и т. д.), или же в группу свойств, вообще не связанную с конфигурацией построения, — тогда элемент автоматически будет относиться ко всем конфигурациям. Выглядеть это должно следующим образом (в данном случае культурой по умолчанию объявлен американский диалект английского языка): <Ргоject ... > en-US Перестроив проект с такой настройкой, вы обнаружите на одном уровне со своей сборкой папку en-US, в которой будет находиться сателлитная сборка с именем AssemblyName. resources.dll. Следует также пометить свою сборку атрибутом NeutralResourcesLanguage уровня сборки, значение которого совпадает с выбранной культурой по умолчанию: [assembly: NeutralResourcesl_anguage("en-US , UltimateResourceFallbackLocation.Satellite)] Пометка пользовательского интерфейса идентификаторами локализации Следующий шаг - применение директивы Uid из пространства имен языка XAML (x:Uid) ко всем объектным элементам, нуждающимся в локализации. значением каждой такой директивы должен быть уникальный идентификатор. Делать это вручную очень утомительно, но, к счастью, есть и автоматический спосою - вызвать MSBuild из командой строки следующим образом: msbuild /t:updateuid ProjectName.csproj 402 Глава 12.Ресурсы В результате каждый объектный элемент в каждом XAML-файле проекта получит директиву x:Uid с уникальным значением. Можете добавить эту задачу в проект перед задачей Build, хотя в этом случае она будет выполняться при каждом построении, что совершенно ни к чему. Создание новой сателлитной сборки с помощью LocBaml Откомпилировав проект, в который добавлены Uid, можно запустить программу LocBaml из Windows SDK для файла .resources, сгенерированного в процессе построения (он находится в каталоге оbj\debug): LocBaml /parse ProjectName.g.en-US. resources /out:en-US.csv В результате создается простой текстовый CSV-файл, содержащий все значения свойств, подлежащие локализации. Этот файл можно отредактирован, так, чтобы он соответствовал новой культуре (никаких хитростей в этой части локализации нет и в помине!). Сохраните файл и запустите LocBaml в обратном направлении, чтобы сгенерировать сателлитную сборку по CSV-файлу! Например, если в CSV-файле находится перевод строк на канадский диалект французского языка, то можно сохранить его с именем fr-CA.csv, а затем запустить LocBaml следующим образом: LocBaml /generate ProjectName. resources.dll /trans:fr-CA.csv /cul:fr-CA Новую сателлитную сборку следует скопировать в ту же папку, где находится основная сборка, присвоив ей имя, соответствующее культуре (в данном случае fr-CA). Чтобы протестировать приложение для другой культуры, присвойте свойству System.Threading.Thread.CurrentThread.CurrentUICulture (и System.Threading.Thread.CurrentThread.CurrentCulture) ссылку на нужный экземпляр класса GultureInfo. Логические ресурсы Второй тип ресурсов был впервые введен в WPF и поддерживается как WPF, так и Silverlight. В этой главе ресурсы такого типа называются логическими за неимением более подходящего термина, хотя в большинстве книг их называют просто ресурсами, в отличие от рассмотренных выше двоичных ресурсов. (Может возникнуть искушение использовать термин XAML-ресурсы, но, как почти все в XAML, их можно создавать и использовать также и в процедурном коде.) Логические ресурсы представляют собой произвольные объекты .NET, хранящиеся в свойстве элемента Resources. Обычно предполагается, что таким ресурсом смогут сообща пользоваться все потомки данного элемента. Свойство Resources (типа System.Windows. ResourqeDictionary) определено в базовых классах FrameworkElement и FrameworkContentElement, а это означает, что оно есть в большинстве классов WPF. В качестве логических ресурсов часто выступают стили (см. главу 14 «Стили, шаблоны, обложки и темы») или поставщики данных (см. главу 13 «Привязка к данным»). Но в этой главе мы будем хранить в логическом ресурсе простую кисть Brush. Логические ресурсы 403 В листинге 12.1 показано простое окно Window, в нижней строке которого расположен ряд кнопок — как в пользовательском интерфейсе программы Photo Gallery. В разметке используется прямолинейный способ применения кисти Brush к свойствам Background и BorderBrush каждой кнопки Button (и всего окна Window). Результат изображен на рис. 12.2. Рис. 12.2. Результат визуализации разметки из листинга 12.1 Листинг 12.1. Применение цветной кисти без использования логических ресурсов 404 Глава 12. Ресурсы Вместо этого можно было бы сделать желтую и красную кисти логическими ресурсами элемента Window и ссылаться на них из разных элементов. Это правильный способ отделить и собрать в одном месте всю информацию о стилях Во многом он напоминает применение каскадных таблиц стилей (CSS) для управления всеми цветами и стилями в пределах веб-страницы, не задавая их для каждого элемента в отдельности. Механизм обобществления объектов, реализованный в схеме логических ресурсов, поможет заодно значительно сэкономить память, причем экономия будет тем больше, чем объекты сложнее. В листинге 12.2 предыдущая разметка переработана с использованием логических ресурсов для обеих кистей. Листинг 12.2. Хранение логических кистей в одном месте с помощью логических ресурсов Yellow Red Способ определения ресурсов и синтаксис х:Key уже встречались нам при рассмотрении словаря ResourceDictionary в главе 2 «Все тайны XAML». Для применения ресурса к элементам мы используем расширение разметки StaticResource (сокращенная запись System.Windows.StaticResourceExtension). К свойству Window. Background она применяется как элемент свойства, а к свойствам Button.Background и Button.BorderBrush - как атрибут свойства. Поскольку оба ресурса в этом примере кисти Brush, то и применять их допустимо всюду, где может встречаться кисть. Поскольку в листинге 12.2 по-прежнему используются простые кисти желтого и красного цветов, то и результат визуализации ничем не отличается от показанного на рис. 12.2. Зато теперь нам достаточно заменить кисть в одном месте, не трогая больше ничего в XAML-файле (при условии, что ключи в словаре ресурсов останутся теми же самыми). Например, если заменить ресурс backgroundBrush такой линейно-градиентной кистью: то получится результат, изображенный на рис. 12.3. Рис. 12.3. То же самое окно, что в листинге 12.2, но ресурс backgroundBrush определен по-другому 406 Глава 12. Ресурсы Поиск ресурса Расширение разметки StaticResource принимает единственный параметр - ключ объекта в словаре ресурсов. Однако этот объект не обязан находиться в словаре ресурсов текущего элемента. Он может быть в словаре ресурсов любого логического родителя или даже приложения либо системы в целом. В классе этого расширения разметки реализована возможность обхода логического дерева для поиска нужного объекта. Сначала проверяется коллекция Resources текущего элемента (его словарь ресурсов). Если объект не найден проверяется родительский элемент и т.д., пока не дойдем до корневого элемента. В этот момент проверяется коллекция Resources объекта Application. Если искомое не найдено и здесь, то проверяется коллекция ресурсов темы (см. главу 14). Если объекта нет и там, то на последнем шаге проверяется системная коллекция (в которой находятся системные шрифты, цвета и другие настройки). Если ресурс так и не удалось найти, возбуждается исключение InvalidOperationException. Принимая во внимание описанное поведение, ресурсы обычно хранят в словаре ресурсов корневого элемента или в словаре уровня приложения, чтобы обеспечить максимально широкое обобществление. Отметим, что, хотя в пределах одного словаря ключи ресурсов должны быть уникальны, в разных коллекциях могут встречаться ресурсы с одинаковыми ключами. Приоритет имеет ресурс, оказавшийся в словаре, «ближайшем» к запросившему этот ресурс элементу, - так устроен алгоритм обхода дерева. ПРЕДУПРЕЖДЕНИЕ Осторожнее с ресурсами уровня приложения в многопоточных приложениях! В главе 7 мы говорили, что в WPF-приложении может быть несколько потоков пользовательского интерфейса. В таком случае каждый поток будет напрямую обращаться к ресурсам уровня приложения. Чтобы при этом не возникало ошибок, все такие ресурсы должны принадлежать классу Freezable и при этом быть заморожены, либо их следует пометить атрибутом x:Shared=false, который мы опишем ниже, в разделе «Необобществляемые ресурсы». Статические и динамические ресурсы WPF предлагает два способа доступа к логическому ресурсу: • Статически с помощью расширения разметки StaticResource - это означает, что значение ресурса применяется только один раз (при первом обращении) • Динамически с помощью расширения разметки DynamicResource - это означает, что ресурс заново применяется после каждого изменения Расширение разметки DynamicResource (System.Windows. DynamicResourceExtension реализует такой же обход дерева, как StaticResource, поэтому зачастую между Логические ресурсы 407 применением DynamicResource и StaticResource нет никакой разницы. Ничто в самих объявлениях ресурсов не делает одно расширение более предпочтительным, чем другое; выбор того или другого определяется тем, хотите вы, чтобы потребитель ресурса видел его изменения, или нет. На самом деле можно даже в каком-то словаре применять к одному и тому же ключу ресурса расширение StaticResource, а в другом — DynamicResource, хотя непонятно, зачем это могло бы понадобиться. Описание различий Основное различие между StaticResource и DynamicResource заключается в том, что последующие обновления ресурса применяются только к тем элементам, для которых используется расширение DynamicResource. Причиной обновления может быть как ваш собственный код (например, замена желтой кисти на синюю), так и изменение системных настроек пользователем. Характеристики производительности классов StaticResource и DynamicResource различаются. С одной стороны, использование DynamicResource влечет за собой большие непроизводительные издержки, так как приходится отслеживать изменения. С другой стороны, использование DynamicResource может уменьшить время загрузки. Ресурсы, на которые есть ссылки типа StaticResource, загружаются вместе с окном Window или страницей Раде, тогда как ресурсы со ссылкой типа DynamicResource не загружаются, пока к ним не обратятся. Кроме того, DynamicResouгcе можно использовать только для установки свойств зависимости, a StaticResource — практически повсеместно. Например, StaticResource может служить в качестве элемента, чтобы абстрагировать целые элементы управления! Следующее окно Window: эквивалентно такому: Использование таких элементов, как Image, в виде ресурсов - возможно, и это интересный способ вынести фрагменты XAML-кода в отдельное место, но в качестве средства обобществления объекта он не годится. У элемента Image может быть только один родитель, поскольку он наследует классу Visual (и, значит, 408 Глава 12. Ресурсы принимает участие в логическом и визуальном деревьях), поэтому любая попытка использовать один и тот же объект в качестве ресурса несколько раз обречена на провал. Например, если вставить другой, но идентичный первому элемент StaticResource в предыдущий фрагмент XAML-кода, то мы получим исключение с сообщением "Specified Visual is already a child of another Visual or the root of a CompositionTarget" (Указанный элемент Visual уже является дочерним по отношению к другому элементу Visual или корневому элементу CompositionTarget). КОПНЕМ ГЛУБЖЕ Вынесение XAML-кода Механизм ресурсов дает удобный способ вынести фрагменты XAML-кода в одно место на странице, а если сохранить их в виде ресурсов уровня приложения - то даже и в отдельный XAML-файл. Но если вы хотите разнести набор ресурсов по нескольким XAML-файлам вне зависимости от того, в каком месте логического дерева они хранятся (быть может, ради удобства сопровождения или повышения гибкости), то придется воспользоваться свойством MergedDictionaries класса ResourceDictionary. Например, окно Window могло бы следующим образом организовать свою коллекцию Resources, объединив словари ресурсов, хранящиеся в разных файлах: В отдельных файлах элемент ResourceDictionary должен быть корневым. Например, файл filel.xaml мог бы выглядеть так: Если в объединяемых словарях встречаются одинаковые ключи, то предпочтение отдается последнему (в отличие от случая, когда одинаковые ключи встречаются в одном словаре). Помимо такого решения, другим способом разнести XAML-код по разным файлам является создание нестандартных элементов управления (см. главу 20 «Пользовательские и нестандартные элементы управления»). Универсального механизма включения типа директивы #include, обрабатываемой препроцессорами языков С и C++, в XAML нет. Логические ресурсы 409 Существует еще одно тонкое различие между статическим и динамическим доступом к ресурсам. Расширение разметки StaticResource в XAML не поддерживает опережающие ссылки. Иными словами, всякому использованию ресурса должно предшествовать его объявление в XAML-файле. Это означает, что StaticResource нельзя использовать как атрибут свойства, если ресурс определен в том же самом элементе (потому что в этом случае ссылка на ресурс неизбежно оказывается раньше его определения)! У расширения DynamicResource такого ограничения нет. Именно из-за этого правила опережающей ссылки в окне Window в листинге 12.2 для задания Background применен синтаксис элемента свойства. Тем самым гарантируется, что ресурс определен раньше, чем используется. DynamicResource можно было бы использовать точно так же, но можно воспользоваться и синтаксисом атрибута свойства, потому что в этом случае безразлично, находится ссылка на ресурс до его определения или после: Yellow Red Необобществляемые ресурсы По умолчанию, когда ресурс применяется в нескольких местах, всюду используется один и тот же объект. Обычно это желательное поведение. Но можно пометить некоторые объекты в откомпилированном словаре ресурсов ключевым словом x:Shared="False", и тогда при каждом запросе будет генерироваться новый объект, который можно модифицировать независимо от остальных. Такое поведение может быть полезно в предыдущем примере, где мы использовали в качестве ресурса целиком объект Image (или объект любого другого класса, производного от Visual). Подобный ресурс можно применить в дереве элементов только один раз, потому что у него не может быть больше одного родителя. Однако задание ключевого слова x:Shared="False" изменяет поведение, создавая возможность применять ресурс многократно в виде независимых объектов. Делается это следующим образом: 410 Глава 12. Ресурсы Отметим, что атрибут x:Shared можно использовать только в откомпилированном XAML-файле. В автономных XAML-файлах эта возможность не поддерживается. Определение и применение ресурсов в процедурном коде До сих пор мы говорили о том, как определять и применять логические ресурсы в XAML-коде, но еще не рассмотрели, как то же самое делается в процедурном коде. Определить ресурс в коде очень просто. В предположении, что элемент Window называется window, два ресурса типа SolidColorBrush, встречавшиеся в листинге 12.2, можно определить в коде на C# следующим образом: window.Resources.Add("backgroundBrush", new SolidColorBrush(Colors.Yellow)); window.Resources.Add("borderBrush", new SolidColorBrush(Colors.Red)); Но вот применение ресурсов в процедурном коде - совсем другое дело. Поскольку StaticResource и DynamicResource - расширения разметки, то эквивалентный код поиска и применения ресурса на C# не вполне очевиден. Чтобы получить поведение, эквивалентное StaticResource, необходимо записать в свойство элемента результат вызова метода FindResource (унаследованного от класса FrameworkElement или FrimeworkContentElement). Таким образом, следующее объявление элемента Button (похожее на то, что встречается в листинге 12.2):