Текст
                    Pro
WPF
in C# 2010:
Windows Presentation
Foundation in .NET 4
Matthew MacDonald
Apress®


WPF Windows Presentation Foundation в .NET 4 с примерами на С# 2010 ДЛЯ ПРОФЕССИОНАЛОВ Мэтью Мак-Дональд Москва • Санкт-Петербург • Киев 2011
ББК 32.973.26-018.2.75 М15 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией СИ. Тригуб Перевод с английского Я.П. Волковой, А.А. Моргунова, Н.А. Мухина Под редакцией Ю.Н. Артпеменко По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: info@williamspublishing.com, http://www.williamspublishing.com Мак-Дональд, Мэтью. М15 WPF 4: Windows Presentation Foundation в .NET 4.0 с примерами на С# 2010 для профессионалов. : Пер. с англ. — М. : ООО "И.Д. Вильяме", 2011. — 1024 с. : ил. — Парал. тит. англ. ISBN 978-5-8459-1657-0 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства APress, Berkeley, CA. Authorized translation from the English language edition published by APress, Inc., Copyright © 2010 by Matthew MacDonald. All rights reserved. No part of this work may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval system, without the prior written permission of the copyright owner and the publisher. Trademarked names may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. Russian language edition is published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2011. Научно-популярное издание Мэтью Мак-Дональд WPF 4: Windows Presentation Foundation в .NET 4.0 с примерами на С# 2010 для профессионалов Верстка Т.Н. Артпеменко Художественный редактор ВТ. Павлютпин Подписано в печать 12.01.2011. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 82,56. Уч.-изд. л. 71,4. Тираж 1500 экз. Заказ № 25054. Отпечатано по технологии CtP в ОАО "Печатный двор" им. А. М. ГЪрького 197110, Санкт-Петербург, Чкаловский пр., 15. ООО "И. Д. Вильяме", 127055, г. Москва, ул. Лесная, д. 43, стр. 1 ISBN 978-5-8459-1657-0 (рус.) © Издательский дом "Вильяме", 2011 ISBN 978-1-43-027205-2 (англ.) © by Matthew MacDonald, 2010
Оглавление Введение 20 Глава 1. Введение в WPF 25 Глава 2. XAML 46 Глава 3. Компоновка 81 Глава 4. Свойства зависимости 120 Глава 5. Маршрутизируемые события 133 Глава 6. Элементы управления 168 Глава 7. Класс Application 217 Глава 8. Привязка элементов 248 Глава 9. Команды 262 Глава 10. Ресурсы 288 Глава 11. Стили и поведения 302 Глава 12. Фигуры, кисти и трансформации 324 Глава 13. Классы Geometry и Drawing 361 Глава 14. Эффекты и класс Visual 381 Глава 15. Основы анимации 402 Глава 16. Расширенная анимация 441 Глава 17. Шаблоны элементов управления 469 Глава 18. Пользовательские элементы 508 Глава 19. Привязка данных 559 Глава 20. Форматирование привязанных данных 600 Глава 21. Представления данных 641 Глава 22. Элементы управления ListView, TreeView и DataGrid 657 Глава 23. Окна 696 Глава 24. Страницы и навигация 734 Глава 25. Меню, панели инструментов и ленты 780 Глава 26. Звук и видео 805 Глава 27. Трехмерная графика 827 Глава 28. Документы 866 Глава 29. Печать 914 Глава 30. Взаимодействие с Windows Forms 942 Глава 31. Многопоточность 964 Глава 32. Модель дополнений 976 Глава 33. Развертывание ClickOnce 998 Предметный указатель 1016
Содержание Об авторе 19 О техническом рецензенте 19 Благодарности 19 Введение 20 Об этой книге 21 Обзор глав 21 Что необходимо для чтения этой книги 24 Исходный код примеров 24 От издательства 24 Глава 1. Введение в WPF 25 Эволюция графики в Windows 25 DirectX: новый графический механизм 26 Аппаратное ускорение и WPF 26 WPF: высокоуровневый API-интерфейс 28 Независимость от разрешения 31 Архитектура WPF 36 Иерархия классов 37 WPF4 40 Новые средства 41 WPF Toolkit 42 Visual Studio 2010 42 Поддержка множества целевых платформ 42 Клиентский профиль .NET 43 Визуальный конструктор Visual Studio 44 Резюме 45 Глава 2. XAML 46 Особенности XAML 47 Графический интерфейс пользователя до WPF 47 Разновидности XAML 48 Компиляция XAML 48 Основы XAML 50 Пространства имен XAML 51 Класс отделенного кода 52 Свойства и события в XAML 55 Простые свойства и конвертеры типов 56 Сложные свойства 57 Расширения разметки 59 Присоединенные свойства 60 Вложенные элементы 61 Специальные символы и пробелы 64 События 65 Полный пример автоответчика 66 Использование типов из других пространств имен 67 Загрузка и компиляция XAML 69 Только код 70 Код и не компилированный XAML 72 Код и скомпилированный XAML 74 Только XAML 75 XAML 2009 76 Автоматическая привязка событий 77 Ссылки 78 Встроенные типы 78
Содержание 7 Расширенное создание объектов 79 Резюме 80 Глава 3. Компоновка 81 Понятие компоновки в WPF 81 Философия компоновки WPF 82 Процесс компоновки 83 Контейнеры компоновки 83 Простая компоновка с помощью StackPanel 85 Свойства компоновки 87 Выравнивание 88 Поля 88 Минимальные, максимальные и явные размеры 90 Элемент Border 92 WrapPanelHDockPanel 93 WrapPanel 93 DockPanel 94 Вложение контейнеров компоновки 96 Grid 98 Тонкая настройка строк и колонок 100 Округление компоновки 102 Объединение строк и колонок 103 Разделенные окна 104 Группы с общими размерами 107 UniformGnd 110 Координатная компоновка с помощью Canvas 110 Z-порядок 111 InkCanvas 112 Примеры компоновки 114 Колонка настроек 114 Динамическое содержимое 116 Модульный пользовательский интерфейс 117 Резюме 118 Глава 4. Свойства зависимости 120 Свойства зависимости 120 Определение свойства зависимости 121 Регистрация свойства зависимости 121 Проверка свойств 128 Резюме 132 Глава 5. Маршрутизируемые события 133 Знакомство с маршрутизируемыми событиями 133 Определение, регистрация и упаковка маршрутизируемых событий 134 Совместное использование маршрутизируемых событий 135 Генерация маршрутизируемого события 135 Обработка маршрутизируемого события 135 Маршрутизация событий 137 События WPF 145 События времени существования 145 События ввода 147 Ввод с клавиатуры . 149 Ввод с использованием мыши 154 Сенсорный многопозиционный ввод 159 Резюме 167
8 Содержание Глава 6. Элементы управления 168 Класс Control 169 Кисти фона и переднего плана 169 Шрифты 171 Курсоры мыши 176 Элементы управления содержимым 178 Выравнивание содержимого 181 Метки 183 Кнопки 183 Всплывающие подсказки 187 Специализированные контейнеры 194 КлассScrollViewer 194 Элементы управления содержимым с заголовками 197 Класс GroupBox 197 Класс Tabltem 197 Класс Expander 199 Текстовые элементы управления 202 Многострочный текст 202 Выделение текста 203 Проверка правописания 204 Класс PasswordBox 206 Элементы управления списками 206 КлассListBox 207 Класс ComboBox 210 Элементы управления, основанные на диапазонах значений 211 Класс Slider 211 Класс ProgressBar 212 Элементы управления датами 213 Резюме 216 Глава 7. Класс Application 217 Жизненный цикл приложения 217 Создание объекта Application 217 Наследование специального класса приложения 218 Останов приложения 220 События класса Application 221 Задачи приложения 223 Отображение экрана заставки 223 Обработка аргументов командной строки 224 Доступ к текущему приложению 225 Взаимодействие между окнами 226 Приложение одного экземпляра 227 Ресурсы сборки 233 Добавление ресурсов 234 Извлечение ресурсов 235 Упакованные URI 237 Ресурсы в других сборках 237 Файлы содержимого 238 Локализация 239 Создание локализуемых пользовательских интерфейсов 239 Подготовка приложения для локализации 240 Процесс перевода 241 Резюме 247
Содержание 9 Глава 8. Привязка элементов 248 Связывание элементов вместе 248 Выражение привязки 249 Ошибки привязки 249 Режимы привязки 250 Привязка OneWayToSource 252 Привязка Defaut 252 Создание привязки в коде 252 Множественные привязки 253 Обновления привязок 256 Привязка к объектам, не являющимся элементами 258 Свойство Source 258 Свойство RelativeSource 259 Свойство DataContext 260 Резюме 261 Глава 9. Команды 262 Общие сведения о командах 262 Модель команд WPF 264 Интерфейс ICommand 264 КлассRoutedCommand 265 Класс RoutedUICommand 266 Библиотека команд 267 Выполнение команд 268 Источники команд 268 Привязки команд 269 Использование множества источников команд 271 Точная настройка текста команды 272 Вызов команды напрямую 273 Отключение команд 274 Элементы управления со встроенными командами 276 Расширенные команды 278 Специальные команды 278 Использование одной и той же команды в разных местах 280 Использование параметра команды 281 Отслеживание и отмена команд 282 Резюме 287 Глава 10. Ресурсы 288 Общие сведения о ресурсах 288 Коллекция ресурсов 289 Иерархия ресурсов 290 Статические и динамические ресурсы 291 Неразделяемые ресурсы 293 Доступ к ресурсам в коде 294 Ресурсы приложения 294 Ресурсы системы 295 Словари ресурсов 296 Создание словаря ресурсов 296 Использование словаря ресурсов 297 Разделение ресурсов между сборками 298 Резюме 301 Глава 11. Стили и поведения 302 Основные сведения о стилях 302
10 Содержание Создание объекта стиля 306 Установка свойств 307 Присоединение обработчиков событий 308 Множество уровней стилей 310 Автоматическое применение стилей по типу 311 Триггеры 313 Простой триггер 313 Триггер события 315 Поведения 317 Получение поддержки для поведений 317 Модель поведений 318 Создание поведения 319 Использование поведения 321 Поддержка использования поведений во время проектирования в Expression Blend 322 Резюме 323 Глава 12. Фигуры, кисти и трансформации 324 Понятие фигур 324 Классы фигур 325 Rectangle и Ellipse 327 Установка размеров и расположения фигур 328 Масштабирование фигур в Viewbox 330 Line 333 Polyline 334 Polygon 335 Наконечники и стыки линий 336 Пунктирные линии 337 Привязка к пикселям 339 Кисти 340 SolidColorBrush 341 LinearGradientBrush 341 RadialGradientBrush 344 ImageBrush 345 Мозаичная кисть ImageBrush 347 VisualBrush 350 BitmapCacheBrush 351 Трансформации 352 Трансформация фигур 354 Трансформация элементов 355 Прозрачность 356 Как сделать элемент частично прозрачным 356 Маски непрозрачности 358 Резюме 360 Глава 13. Классы Geometry и Drawing 361 Классы Path и Geometry 361 Геометрии линий, прямоугольников и эллипсов 362 Комбинирование фигур в GeometryGroup 363 Комбинирование объектов Geometry и CombinedGeometry 365 Кривые и прямые линии, представляемые с помощью PathGeometry 367 Мини-язык описания геометрии 372 Кадрирование геометрии 374 Рисунки 375 Отображение рисунка 376
Содержание 11 Экспорт рисунка 379 Резюме 380 Глава 14. Эффекты и класс Visual 381 Классы Visual 381 Рисование объектов Visual 382 Помещение визуальных объектов в оболочку элемента 384 Проверка попадания 387 Сложная проверка попадания 389 Эффекты 392 BlurEffect 393 КлассDropShadowEffeet 393 Класс ShaderEffeet 395 КлассWriteableBitmap 396 Генерация растрового изображения 397 Запись в WriteableBitmap 398 Более эффективная запись пикселей 399 Резюме 401 Глава 15. Основы анимации 402 Что собой представляет анимация WPF 403 Анимация на основе таймера 403 Анимация на основе свойств 404 Базовая анимация 405 Классы анимации 405 Анимация в коде 408 Одновременные анимации 413 Время жизни анимации 413 Класс TimeLine 414 Раскадровки 417 Раскадровка 418 Триггеры событий 418 Перекрывающиеся анимации 421 Синхронизированные анимации 422 Управление воспроизведением 423 Отслеживание хода анимации 427 Плавность анимации 428 Использование функции плавности 429 Режимы плавности 430 Классы функций плавности 431 Создание специальной функции плавности 434 Производительность анимации 436 Желательная частота кадров 436 Кэширование растровых изображений 438 Резюме 440 Глава 16. Расширенная анимация 441 Еще раз о типах анимаций 441 Анимированные трансформации 442 Анимированные кисти 446 Анимация построителей текстур 449 Анимация ключевого кадра 450 Дискретные анимации ключевого кадра 451 Плавные ключевые кадры 452 Сплайновые анимации ключевого кадра 453
12 Содержание Анимация на основе пути 454 Анимация на основе кадра 456 Раскадровки в коде 459 DiaBHoe окно 460 Пользовательский элемент управления Bomb 461 Сброс бомб 463 Перехват бомбы 465 Подсчет бомб и очистка 466 Резюме 468 Глава 17. Шаблоны элементов управления 469 Логические и визуальные деревья 470 Что собой представляют шаблоны 474 Классы Chrome 477 Анализ элементов управления 478 Создание шаблонов элементов управления 481 Простая кнопка 481 Привязки шаблона 483 Триггеры, изменяющие свойства 484 Триггеры, использующие анимацию 487 Организация ресурсов для шаблонов 489 Рефакторинг шаблона элемента управления для кнопки 490 Применение шаблонов со стилями 491 Автоматическое применение шаблонов 494 Обложки, выбранные пользователем 495 Построение более сложных шаблонов 497 Вложенные шаблоны 497 Модификация полосы прокрутки 500 Примеры шаблонов элементов управления 504 Визуальные состояния 505 Резюме 507 Глава 18. Пользовательские элементы 508 Что собой представляют пользовательские элементы в WPF 509 Построение базового пользовательского элемента управления 512 Определение свойств зависимости 513 Определение маршрутизируемых событий 516 Добавление кода разметки 517 Использование элемента управления 519 Поддержка команд 519 Пристальный взгляд на UserControl 521 Создание элемента управления, лишенного внешнего вида 523 Рефакторинг кода указателя цвета 523 Рефакторинг кода разметки указателя цвета 524 Оптимизация шаблона элемента управления 526 Стили, специфичные для темы, и стиль по умолчанию 529 Поддержка визуальных состояний 531 Начало проектирования класса Flip Panel 532 Выбор частей и состояний 534 Шаблон элемента управления, принятый по умолчанию 535 Использование FlipPanel 542 Использование другого шаблона элемента управления 542 Пользовательские панели 544 Двухшаговый процесс компоновки 545 Клон Canvas 547
Содержание 13 Улучшенная панель WrapPanel 548 Рисованные элементы 551 Метод OnRender () 552 Выполнение специального рисования 553 Элемент, выполняющий специальное рисование 554 Специальный декоратор 556 Резюме 558 Глава 19. Привязка данных 559 Привязка пользовательских объектов к базе данных 559 Построение компонента доступа к данным 560 Построение объекта данных 563 Отображение привязанного объекта 563 Обновление базы данных 566 Уведомление об изменениях 566 Привязка к коллекции объектов 568 Отображение и редактирование элементов коллекции 569 Вставка и удаление элементов коллекций 572 Привязка объектов ADO.NET 573 Привязка к выражению LINQ 574 Повышение производительности больших списков 576 Виртуализация 577 Повторное использование контейнера элементов 578 Отложенная прокрутка 579 Проверка достоверности 579 Проверка достоверности в объекте данных 580 Класс ExceptionValidationRule 581 КлассDataErrorValidationRule 582 Специальные правила проверки достоверности 583 Реакция на ошибки проверки достоверности 586 Получение списка ошибок 586 Отображение отличающегося индикатора ошибки 587 Проверка достоверности множества значений 590 Поставщики данных 593 Поставщик ObjectDataProvider 594 Поставщик XmlDataProvider 597 Резюме 598 Глава 20. Форматирование привязанных данных 600 Еще раз о привязке данных 600 Преобразование данных 602 Свойство StringFormat 602 Что собой представляют конвертеры значений 604 Форматирование строк с помощью конвертера значений 604 Создание объектов с конвертером значений 606 Применение условного форматирования 608 Оценка множества свойств 610 Списочные элементы управления 611 Стили списков 614 СтильItemContainerStyle 614 Элемент ListBoxc флажками или переключателями 616 Стиль чередующихся элементов 618 Селекторы стиля 619 Шаблоны данных 622 Отделение и повторное использование шаблонов 624
14 Содержание Более развитые шаблоны 625 Варьирование шаблонов 628 Селекторы шаблонов 629 Шаблоны и выбор 632 Изменение компоновки элемента 636 Элемент ComboBox 638 Резюме 640 Глава 21. Представления данных 641 Объект представления 641 Извлечение объекта представления 642 Навигация в представлении 642 Создание представления декларативным образом 645 Фильтрация, сортировка и группирование 647 Фильтрация коллекций 647 Фильтрация объекта DataTable 650 Сортировка 651 Группирование 652 Резюме 656 Глава 22. Элементы управления ListView, TreeView и DataGrid 657 Элемент управления L i s tVi e w 658 Создание столбцов с помощью Grid View 659 Изменение размера столбцов 660 Шаблоны ячеек 661 Создание специального представления 663 Элемент управления TreeView 671 Привязка данных к элементу управления Т г е еVi e w 672 Привязка элемента управления TreeView к объекту DataSet 674 Оперативное создание узлов 675 Элемент управления DataGrid 678 Изменение размера и порядка следования столбцов 680 Определение столбцов 682 Форматирование и стилизация столбцов 686 Форматирование строк 688 Детали строк 690 Закрепление столбцов 691 Выбор 692 Сортировка 692 Редактирование BDataGrid 692 Резюме 695 Глава 23. Окна 696 Класс Window 696 Отображение окна 699 Позиционирование окна 700 Сохранение и восстановление информации о местоположении окна 701 Взаимодействие окон 703 Владение окнами 705 Модель диалогового окна 705 Общие диалоговые окна 706 Непрямоугольные окна 708 Простое окно нестандартной формы 708 Прозрачные окна с содержимым необычной формы 711 Перемещение окон нестандартной формы 712
Содержание 15 Изменение размеров окон нестандартной формы 713 Шаблон элемента управления для окон 714 Эффект Aero Glass 718 Программирование для панели задач Windows 7 722 Применение списков часто используемых элементов 723 Изменение значков и окон предварительного просмотра, отображаемых в панели задач 728 Резюме 733 Глава 24. Страницы и навигация 734 Общие сведения о страничной навигации 735 Страничные интерфейсы 735 Простое страничное приложение с элементом NavigationWindow 736 Класс Page 737 Гиперссылки 738 Размещение страниц во фрейме 741 Размещение страниц внутри другой страницы 742 Размещение страниц в веб-браузере 743 Хронология страниц 744 Более детальное рассмотрение URI-адресов в WPF 744 Хронология навигации 745 Добавление специальных свойств 747 Служба навигации 748 Программная навигация 748 События навигации 749 Управление журналом 751 Добавление в журнал специальных элементов 753 Страничные функции 757 Приложения ХВАР 760 Требования для приложений ХВАР 761 Создание приложения ХВАР 761 Развертывание приложения ХВАР 762 Обновление приложения ХВАР 764 Безопасность приложения ХВАР 765 Приложения ХВАР с полным доверием 766 Комбинирование приложений ХВАР и автономных приложений 767 Кодирование с обеспечением различных уровней безопасности 767 Эмуляция диалоговых окон с помощью элемента управления Popup 770 Вставка ХВАР-приложения в веб-страницу 772 Элемент управления WebBrowser 773 Навигация к странице 774 Построение дерева DOM 775 Написание сценариев для веб-страницы с помощью кода .NET 777 Резюме 779 Глава 25. Меню, панели инструментов и ленты 780 Меню 780 Класс Menu 781 Элементы меню 782 Класс ContextMenu 784 Разделители меню 785 Панели инструментов и строки состояния 786 Элемент управления Tool Bar 786 Элемент управления StatusBar 790
16 Содержание Ленты 791 Добавление элемента управления Ribbon 792 Стилизация элемента управления Ribbon 793 Команды 794 Меню приложения 795 Вкладки, группы и кнопки 798 Изменение размеров элемента управления Ribbon 800 Панель быстрого запуска 802 Резюме 804 Глава 26. Звук и видео 805 Воспроизведение WAV-аудио 805 Класс SoundPlayer 806 Класс SoundPlayerAction 807 Системные звуки 808 Класс MediaPlayer 808 Элемент MediaElement 810 Программное воспроизведение аудио 810 Обработка событий 811 Воспроизведение аудио с помощью триггеров 812 Воспроизведение множества звуков 814 Изменение громкости, баланса, скорости и позиции воспроизведения 815 Синхронизация анимации с аудио 817 Воспроизведение видео 818 Видео-эффекты 819 Речь 822 Синтез речи 822 Распознавание текста 824 Резюме 826 Глава 27. Трехмерная графика 827 Основы трехмерной графики 828 Окно просмотра 828 Трехмерные объекты 829 Камера 836 Дополнительные сведения о трехмерной графике 840 Текстурирование и нормали 841 Более сложные фигуры 844 Коллекции Model3DGroup 845 Снова о материалах 847 Отображение текстур 849 Интерактивность и анимация 852 Трансформации 853 Вращения 854 Полеты 854 Шаровой манипулятор 857 Проверка попадания 858 Двухмерные элементы на трехмерных поверхностях 862 Резюме 865 Глава 28. Документы 866 Документы 866 Потоковые документы 867 Потоковые элементы 868 Форматирование элементов вывода содержимого 870
Содержание 17 Создание простого потокового документа 871 Блочные элементы 873 Строковые элементы 878 Программное взаимодействие с элементами 884 Выравнивание текста 887 Контейнеры потоковых документов, доступные только для чтения 888 Изменение масштаба 889 Страницы и колонки 890 Загрузка документов из файла 893 Печать 893 Редактирование потокового документа 894 Загрузка файла 894 Сохранение файла 896 Форматирование выделенного текста 897 Получение отдельных слов 899 Фиксированные документы 901 Аннотации 902 Классы аннотаций 902 Включение службы аннотаций 903 Создание аннотаций 905 Просмотр аннотаций 908 Реагирование на изменения аннотаций 911 Хранение аннотаций в фиксированном документе 911 Настройка внешнего вида наклеек 912 Резюме 913 Глава 29. Печать 914 Базовая печать 914 Печать элемента 915 Трансформация печатного вывода 917 Печать элементов без их отображения 919 Печать документа 920 Манипуляции страницами в печатном выводе документа 923 Специальная печать 925 Печать с помощью классов визуального уровня 926 Специальная печать с разбиением на страницы 928 Настройки и управление печатью 933 Поддержка настроек печати 933 Печать диапазонов страниц 934 Управление очередью печати 934 Печать через XPS 937 Создание документа XPS для предварительного просмотра перед печатью 938 Запись в документ XPS, находящийся в памяти 939 Печать непосредственно на принтер через XPS 939 Асинхронная печать 940 Резюме 941 Глава 30. Взаимодействие с Windows Forms 942 Оценка способности к взаимодействию 942 Средства, которые отсутствуют в WPF 943 Смешивание окон и форм 945 Добавление форм к приложению WPF 945 Добавление окон WPF в приложение Windows Forms 946 Отображение модальных окон и форм 946
18 Содержание Отображение немодальных окон и форм 947 Визуальные стили элементов управления Windows Forms 947 Классы Windows Forms, которые не нуждаются во взаимодействии 948 Создание окон со смешанным содержимым 952 Зазор между WPF и Windows Forms 952 Размещение элементов управления Windows Forms в WPF 954 WPF и пользовательские элементы управления Windows Forms 956 Размещение элементов управления WPF в форме Windows Forms 957 Клавиши доступа, мнемоники и фокус 959 Отображение свойств 961 Резюме 963 Глава 31. Многопоточность 964 Многопоточность 964 Диспетчер 965 Класс DispatcherObject 966 Класс BackgroundWorker 968 Резюме 975 Глава 32. Модель дополнений 976 Выбор между MAF и MEF 976 Конвейер дополнения 977 Как работает конвейер 978 Структура каталогов дополнений 980 Подготовка решения, использующего модель дополнений 981 Приложение, использующее дополнения 982 Контракт 983 Представление дополнения 984 Дополнение 984 Адаптер дополнения 985 Представление хоста 986 Адаптер хоста 986 Хост 987 Добавление новых дополнений 990 Взаимодействие с хостом 990 Визуальные дополнения 995 Резюме 997 Глава 33. Развертывание ClickOnce 998 Что собой представляет развертывание приложения 999 Модель установки ClickOnce 1000 Ограничения ClickOnce 1001 Простая публикация ClickOnce 1002 Настройка издателя и продукта 1003 Запуск мастера публикации 1004 Развернутая файловая структура 1008 Установка приложения ClickOnce 1009 Обновление приложения ClickOnce 1010 Дополнительные параметры ClickOnce 1011 Версия публикации 1011 Обновления 1012 Ассоциации файлов 1013 Параметры публикации 1014 Резюме 1015 Предметный указатель 1016
Об авторе Мэтью МакДональд — автор, преподаватель и обладатель звания Microsoft MVP. Он написал свыше десятка книг по программированию в .NET, включая Pro Silverlight 3 in C# (Silverlight 3 с примерами на С# для профессионалов, ИД "Вильяме", 2010 г.), Pro ASP.NET 3.5 in C# [Microsoft ASP.NET 3.5 с примерами на С# 2008 для профессионалов, 2-е изд., ИД "Вильяме", 2008 г.), а также предыдущее издание этой книги — Pro WPF in С# 2008 [WPF: Windows Presentation Foundation в .NET 3.5 с примерами на С# 2008 для профессионалов, 2-е изд., ИД "Вильяме", 2008 г.). Проживает в Торонто с женой и двумя дочерьми. 0 техническом рецензенте Фабио Клаудио Феррачати — плодовитый писатель на темы передовых технологий. Фабио внес вклад в более чем десяток книг по .NET, C#, Visual Basic и ASP.NET. Имеет звание .NET Microsoft Certified Solution Developer (MCSD) и живет в Риме. Благодарности Ни один автор не может завершить книгу без коллектива помощников. Я глубоко признателен всей команде из Apress, включая Энн Коллетт (Anne Collett), которая сопровождала это издание на протяжении всей работы, Ким Уимпсет (Kim Wimpsett) и Мэрилин Смит (Marylin Smith), которые быстро и качественно выполняли редактирование текста, а также многим другим людям, которые занимались версткой, рисованием иллюстраций и вычиткой окончательной копии. Особую признательность хочу выразить Гарри Корнеллу (Gary Cornell) за его неоценимые консультации по проекту. Фабио Клаудио Феррачати и Кристоф Насарре (Christophe Nasarre) заслужили моей искренней благодарности за проницательные и поучительные комментарии. Я также благодарен легиону преданных блогеров из разных команд WPF, которые никогда не забывали пролить свет на наиболее темные места WPF. Настоятельно рекомендую всем, кто хочет узнать больше о будущем WPF, следить за их записями. Наконец, я бы никогда не написал ни одной книги без поддержки жены и следующих замечательных людей: Нора, Разя, Пол и Хамид. Спасибо вам всем!
Введение Платформа .NET принесла с собой небольшую лавину новых технологий. Появился совершенно новый способ написания веб-приложений (ASP.NET), совершенно новый способ подключения к базам данных (ADO.NET), новые безопасные к типам языки (С# и VB.NEH4) и управляемая исполняющая среда (CLR). Не последнее место занимала и технология Windows Forms — библиотека классов для построения Windows- приложений. Хотя Windows Forms — зрелый и полнофункциональньгй набор инструментов, он был тесно привязан к некоторым частям внутреннего устройства Windows, которые не слишком изменились за последние 10 лет. Что более существенно, при создании визуального представления стандартных пользовательских интерфейсных элементов, таких как кнопки, текстовые поля, флажки и т.п., Windows Forms полагается на Windows API. В результате его ингредиенты мало поддаются настройке и изменениям. Например, чтобы построить стилизованную блестящую кнопку, придется создать специальный элемент управления и нарисовать каждый аспект этой новой кнопки (во всех разных состояниях), используя низкоуровневую модель рисования. Хуже того, обычные окна разрезаются на отдельные области, и каждому элементу управления отводится собственная такая область. В результате не существует такого хорошего способа рисования в одном элементе управления (например, эффекта свечения под кнопки), чтобы он распространялся на область, принадлежащую другому элементу. И не стоит даже думать о создании анимационных эффектов вроде вращающегося текста, мерцающих кнопок, сворачивающихся окон или активных предварительных просмотров, потому что каждая-деталь должна быть нарисована вручную. В Windows Presentation Foundation (WPF) эта ситуация изменилась за счет ввода модели с совершенно другим устройством. Хотя платформа WPF включает знакомые стандартные элементы управления, она рисует каждый текст, контур и фон самостоятельно. В результате WPF может предоставить намного более мощные средства, которые позволяют изменить визуализацию любой части экранного содержимого. С помощью этих средств можно изменить стиль обычных элементов управления, таких как кнопки, часто даже без написания кода. Кроме того, можно применять трансформации объектов для вращения, растяжения, масштабирования и сдвига любой части пользовательского интерфейса, и даже использовать встроенную систему анимации WPF, чтобы делать все это прямо на глазах пользователя. И поскольку механизм WPF визуализирует содержимое окна как часть одной операции, он может обрабатывать неограниченное количество слоев перекрывающихся элементов, даже имеющих нерегулярную форму и частичную прозрачность. В основе WPF лежит мощная инфраструктура, основанная на DirectX — API- интерфейсе графики с аппаратным ускорением, который обычно используется в современных компьютерных играх. Это означает возможность применения развитых графических эффектов, не платя за это производительностью, как это было в Windows Forms. Фактически даже становятся доступными такие расширенные средства, как поддержка видеофайлов и трехмерное содержимое. Используя эти средства (при наличии хорошего инструмента графического дизайна), можно создавать бросающиеся в глаза пользовательские интерфейсы и визуальные эффекты, которые были просто невозможны в Windows Forms.
Введение 21 Хотя новейшие средства видео, анимации и 3-D часто привлекают максимум внимания в WPF, важно отметить, что WPF можно применять для построения обычных Windows-приложений со стандартными элементами управления и привычным внешним видом. Фактически использовать стандартные элементы управления в WPF так же легко, как и в Windows Forms. Более того, WPF расширяет средства, адресованные именно бизнес-разработчикам, включая значительно усовершенствованную модель привязки данных, набор классов для печати содержимого и управления очередями печати, а также средства работы с документами для отображения огромных объемов форматированного текста. Доступна даже модель для построения приложений на основе страниц, которые гладко работают в Internet Explorer и могут запускаться с веб-сайта — и все это без привычных предупреждений о безопасности или надоедливых приглашений к установке. Вообще говоря, WPF комбинирует лучшее из мира Windows-разработки с новейшими технологиями для построения современных, графически развитых пользовательских интерфейсов. Хотя приложения Windows Forms будут еще жить долгие годы, разработчикам, приступающим к новым проектам Windows, сначала стоит обратить внимание HaWPF. Об этой книге Эта книга представляет собой углубленное руководство по WPF для профессиональных разработчиков, знакомых с платформой .NET, языком С# и средой разработки Visual Studio. Опыт работы с предыдущими версиями WPF не обязателен, хотя новые средства в книге специально выделены во врезках "Что нового?" в начале каждой главы. Книга предлагает полное описание каждого из основных средств WPF — от XAML (языка разметки, используемого для определения пользовательских интерфейсов WPF) до трехмерного рисования и анимации. По ходу чтения вы ознакомитесь с кодом, который включает работу с другими средствами .NET Framework, такими как классы ADO.NET, которые служат для запросов к базе данных. Эти средства здесь не рассматриваются. За дополнительной информацией о средствах .NET, которые не являются специфичными для WPF, обращайтесь к соответствующим книгам. Обзор глав Эта книга включает в себя 33 главы. Если вы только начинаете знакомство с WPF, лучше читайте их по порядку, поскольку более поздние главы опираются на приемы, продемонстрированные в ранних. Ниже приводятся краткие описания всех глав. Глава 1. Введение в WPF. Описана архитектура WPF, внутренние механизмы DirectX, а также новая, независимая от устройства система измерения, которая автоматически изменяет размеры пользовательских интерфейсов. Глава 2. XAML. Рассматривается стандарт XAML, который используется для определения пользовательских интерфейсов. Будет показано, зачем он был создан и как работает, а также, как создавать базовые окна WPF с помощью различных подходов к кодированию. Глава 3. Компоновка. Дается углубленное представление панелей компоновки, которые позволяют организовать элементы в окне WPF Будут рассматриваться различные стратегии компоновки и строится некоторые распространенные типы окон. Глава 4. Свойства зависимости. Описано использование в WPF свойств зависимости для обеспечения поддержки таких ключевых средств, как привязка данных и анимация.
22 Введение Глава 5. Маршрутизируемые события. Рассматривается использование в WPF маршрутизации событий с пузырьковым распространением и туннелированием через элементы пользовательского интерфейса. В главе также содержится описание базового набора событий мыши, клавиатуры и сенсорных панелей, поддерживаемых всеми элементами WPF. Глава 6. Элементы управления. Описаны элементы управления, знакомые каждому разработчику Windows, такие как кнопки, текстовые поля и метки, и их воплощение в WPF. Глава 7. Класс Application. Рассматривается модель приложений WPF. Будет показано, как создавать приложения одного экземпляра и приложения WPF, основанные на документах. Глава 8. Привязка элементов. Объясняется привязка данных в WPF Будет показано, как привязать объекты любого типа к пользовательскому интерфейсу. Глава 9. Команды. Описана модель команд WPF, которая позволяет связывать несколько элементов управления с одинаковым логическим действием. Глава 10. Ресурсы. Показано, как с помощью ресурсов встраивать двоичные файлы в сборку и многократно использовать важные объекты по всему пользовательскому интерфейсу. Глава 11. Стили и поведения. Рассматривается система стилей WPF, которая позволяет применять набор общих значений свойств к целой группе элементов управления. Глава 12. Фигуры, кисти и трансформации. Описана модель двухмерного рисования в WPF. Будет показано, как создавать фигуры, изменять элементы с помощью трансформаций и получать экзотические эффекты с помощью градиентов, укладки плиткой и изображениями. Глава 13. Классы Geometry и Drawing. Более глубоко рассматривается двухмерное рисование. Будет показано, как создавать сложные пути, включающие дуги и кривые, а также эффективно использовать сложную графику. Глава 14. Эффекты и класс Visual. Рассматривается программирование низкоуровневой графики. Будет показано, как применять эффекты в стиле Photoshop с помощью построителей текстур, вручную создавать растровые изображения, а также использовать визуальный уровень WPF для оптимизации рисования. Глава 15. Основы анимации. Описана платформа анимации WPF, которая позволяет интегрировать динамические эффекты в приложение, используя прямолинейную декларативную разметку. Глава 16. Расширенная анимация. Рассматриваются более сложные приемы анимации, такие как анимация ключевого кадра, анимация, основанная на пути, и анимация на основе кадров. Также предлагается детальный пример, демонстрирующий создание и управление динамической анимацией в коде. Глава 17. Шаблоны элементов управления. Показано, как придать совершенно новый вид (и новое поведение) любому элементу управления WPF, подключая специализированный шаблон. Также описано, как с помощью шаблонов создавать приложения со сменными обложками. Глава 18. Пользовательские элементы. Посвящена расширению существующих элементов управления WPF и созданию собственных. Будет предложено несколько примеров, включая основанный на шаблоне селектор цвета, переворачиваемую па-
Введение 23 нель, специальный контейнер компоновки и декоратор, который выполняет специальное рисование. Глава 19. Привязка данных. Рассматривается, как извлекать информацию из базы данных, вставлять в специальные объекты данных и привязывать эти объекты к элементам управления WPF. Также описаны приемы повышения производительности больших связанных с данными списков посредством виртуализации, а также перехват ошибок редактирования за счет проверки достоверности. Глава 20. Форматирование привязанных данных. Описаны некоторые трюки для превращения неформатированных данных в развитое экранное представление, включающее изображения, элементы управления и эффекты выбора. Глава 21. Представления данных. Объясняется использование представления в окне, привязанном к данным, для навигации по списку элементов данных и применения фильтрации, сортировки и группировки. Глава 22. Элементы управления ListView, TreeView и DataGrid. Дается экскурс по многофункциональным элементам управления WPF, включая ListView, TreeView и DataGrid. Глава 23. Окна. Рассматривается работа окон в WPF. Будет показано, как создавать окна неправильной формы и использовать "стеклянные" эффекты Windows Vista. Кроме того, будет реализовано большинства средств Windows 7 за счет настройки списков часто используемых элементов в панели задач, миниатюр и налагаемых значков. Глава 24. Страницы и навигация. Описано построение страниц в WPF и отслеживание хронологии навигации. Также будет показано, как строить основанные на браузере приложения WPF, которые могут быть запущены с веб-сайта. Глава 25. Меню, панели инструментов и ленты. Посвящена командно-ориентированным элементам управления, таким как меню и панели инструментов. Также демонстрируется более современный интерфейс на основе свободного загружаемого элемента управления Ribbon. Глава 26. Звук и видео. Описана поддержка мультимедиа в WPF. Будет показано, как управлять воспроизведением звука и видео, и как реализовать синхронизированные анимации и живые эффекты. Глава 27. Трехмерная графика. Рассматривается поддержка рисования трехмерных фигур в WPF. Будет показано, как создавать, трансформировать и анимировать трехмерные объекты, а также, как помещать интерактивные двухмерные элементы управления на трехмерные поверхности. Глава 28. Документы. Описана поддержка форматированных документов в WPF. Будет показано, как использовать потоковые документы для представления больших объемов текста в наиболее читабельном виде и фиксированные документы для отображения страниц, готовых к печати. Кроме того, рассматривается применение элемента управления RichTextBox для редактирования документа. Глава 29. Печать. Представлена модель печати WPF, которая позволяет выводить текст и фигуры в печатный документ. Будет также показано, как управлять настройками страниц и очередями печати. Глава 30. Взаимодействие с Windows Forms. Описаны способы комбинирования содержимого WPF и Windows Forms в пределах одного приложения и даже одного окна.
24 Введение Глава 31. Многопоточность. Рассматривается создание отзывчивых приложений WPF, которые выполняют длительные задачи в фоновом режиме. Глава 32. Модель дополнений. Показано, как создавать расширяемое приложение, которое может динамически обнаруживать и подгружать отдельные компоненты. Глава 33. Развертывание ClickOnce. Рассматриваются вопросы, связанны с развертыванием приложений WPF с помощью технологии ClickOnce. Что необходимо для чтения этой книги Для того чтобы запустить приложение WPF, на компьютере должна быть установлена система Windows 7, Windows Vista или Windows XP с Service Pack 2. Также понадобится .NET Framework 4. Чтобы создавать приложения WPF (и открывать примеры проектов, включенные в эту книгу), необходима среда Visual Studio 2010, которая включает .NET Framework 4. Существует еще одна возможность. Вместо использования любой версии Visual Studio строить и тестировать приложения WPF можно с помощью инструмента графического дизайна Expression Blend. В целом, Expression Blend предназначен для дизайнеров графики, которые большую часть времени занимаются созданием внешнего вида, в то время как Visual Studio — идеальная среда для работы программистов, пишущих код приложений. В книге предполагается применение Visual Studio. За дополнительными сведениями об Expression Blend следует обращаться к одной из специализированных книг. (Кстати, для создания приложений с помощью WPF 4 понадобится Expression Blend 4.) Исходный код примеров Исходный код всех рассматриваемых в настоящей книге примеров доступен на вебсайте издательства по адресу http://www.williamspublishing.com/. От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: info@williamspublishing.com WWW: http://www.williamspublishing.com Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152
ГЛАВА 1 Введение в WPF Windows Presentation Foundation (WPF) — это графическая система отображения для Windows. Платформа WPF спроектирована для .NET под влиянием таких современных технологий отображения, как HTML и Flash, и использует аппаратное ускорение. Она также является наиболее радикальным изменением в пользовательском интерфейсе Windows со времен Windows 95. В этой главе вы ознакомитесь с архитектурой WPF. Вы узнаете, как она справляется с различными разрешениями экрана, и получите общее представление о ее сборках и классах. Также будут рассмотрены новые средства, добавленные к WPF 4. Что нового? Если вы — опытный разработчик WPF, то можете сразу перейти к разделу "WPF 4" настоящей главы, в котором подытожены изменения, произошедшие в последнем выпуске WPF Эволюция графики в Windows Трудно оценить важность WPF, не принимая во внимание тот факт, что разработчики Windows-приложений в течение более 15 лет пользовались, по сути, одной и той же технологией отображения. Стандартное Windows-приложение при создании пользовательского интерфейса полагается на две основополагающие части операционной системы Windows: • User32 обеспечивает знакомый внешний вид и поведение таких элементов, как окна, кнопки, текстовые поля и т.п.; • GDI/GDI+ предоставляет поддержку рисования фигур, текста и изображений за счет дополнительного усложнения (и часто неважной производительности). С годами обе технологии совершенствовались, и API-интерфейсы, используемые разработчиками для взаимодействия с ними, значительно менялись. Но как бы ни разрабатывалось приложение — с помощью .NET и Windows Forms, (в прошлом) Visual Basic 6 или кода C++ на основе MFC — "за кулисами" работают одни и те же части операционной системы Windows. Новые платформы просто предоставляют улучшенные оболочки для взаимодействия с User32 и GDI/GDI+. Они могут быть более эффективными, менее сложными, могут включать некоторые заранее подготовленные средства, чтобы не приходилось создавать их самостоятельно, однако они не могут преодолеть фундаментальные ограничения системных компонентов, разработанных более 10 лет назад. На заметку! Базовое разделение ответственности между User32 и GDI/GDI+ было заложено свыше 15 лет назад в Windows 3.0. Конечно, часть User32 в те времена была просто User, поскольку тогда программное обеспечение еще не вошло в 32-разрядный мир.
26 Глава 1. Введение в WPF DirectX: новый графический механизм В Microsoft разработали один обходной путь для преодоления ограничений, присущих библиотекам User32 и GDI/GDI+. Этим путем является DirectX. Он начинался как "топорный", полный ошибок инструментальный набор для создания игр на платформе Windows. Главной его целью была скорость, и потому Microsoft тесно сотрудничала с производителями видеокарт, чтобы обеспечить для DirectX аппаратную поддержку, необходимую для отображения сложных текстур, специальных эффектов вроде частичной прозрачности и трехмерной графики. За годы, прошедшие с момента появления (вскоре после выхода Windows 95), механизм DirectX обрел зрелость. Теперь это неотъемлемая часть Windows, которая включает поддержку всех современных видеокарт. Однако API-интерфейс DirectX по-прежнему несет в себе наследие своих корней как средства для разработки игр. Из-за присущей DirectX сложности он почти никогда не использовался в традиционных Windows- приложениях (в частности, в бизнес-приложениях). Технология WPF в корне меняет ситуацию. Лежащая в основе WPF графическая технология — это не GDI/GDI+. Теперь это DirectX. Примечательно, что приложения WPF используют DirectX независимо от создаваемого типа пользовательского интерфейса. Это значит, что создается ли сложная трехмерная графика (DirectX's forte), или просто рисуются кнопки и простой текст — вся работа по рисованию проходит через конвейер DirectX. В результате даже самые заурядные бизнес-приложения могут использовать богатые эффекты вроде прозрачности и сглаживания. Также получается выигрыш от аппаратного ускорения, и это означает, что DirectX передает как можно больше работы узлу обработки графики (graphics processing unit — GPU), который представляет собой отдельный процессор на видеокарте. На заметку! Технология DirectX более эффективна, поскольку оперирует высокоуровневыми конструкциями вроде текстур и градиентов, которые могут отображаться непосредственно видеокартой. Компонент GDI/GDH- на это не способен, поэтому ему приходится преобразовывать их в инструкции рисования пикселей, и потому отображение проходит намного медленнее даже на современных видеокартах. Один компонент, который остается на сцене (в ограниченной степени) — это User32. Это объясняется тем, что WPF по-прежнему полагается на User32 в отношении таких служб, как обработка и маршрутизация ввода, а также определение того, какое приложение какой частью экрана владеет. Однако все рисование производится через DirectX. На заметку! Это наиболее существенное изменение в WPR Технология WPF — это не оболочка для GDI/GDI+. На самом деле это его замена — отдельный уровень, работающий через DirectX. Аппаратное ускорение и WPF Возможно, вам известно, что видеокарты различаются между собой в плане поддержки специализированных средств визуализации и оптимизации. К счастью, проблемой это не является, поскольку WPF обладает способностью выполнять всю работу с использованием программных вычислений вместо того, чтобы полагаться на встроенную поддержку видеокарты. На заметку! В отношении программной поддержки WPF существует одно исключение. Из-за слабой поддержки драйверов WPF выполняет сглаживание трехмерной графики только в случае, если приложение запущено под управлением Windows Vista или Windows 7 (и есть встроенный драйвер WDDM для установленной видеокарты).
Глава 1. Введение в WPF 27 Это значит, что при рисовании трехмерных фигур на компьютере с Windows XP вместо гладких линий будут получены ступенчатые ломаные. Однако для двумерной графики сглаживание обеспечивается всегда, независимо от операционной системы и поддержки драйверов. Наличие мощной видеокарты не дает абсолютной гарантии, что вы получите максимальную, с аппаратной поддержкой производительность на WPF. Программное обеспечение также играет важную роль. Например, WPF не может обеспечить аппаратного ускорения на видеокартах, если используются устаревшие драйверы. (Для устаревших видеокарт, такие драйверы, скорее всего, будут единственно доступными.) Технология WPF также обеспечивает более высокую производительность в средах операционных систем Windows Vista и Windows 7, где она может пользоваться преимуществами новой модели дисплейных драйверов Windows (Windows Display Driver Model — WDDM). Модель WDDM предлагает несколько важных усовершенствований по сравнению с Windows XP Display Driver Model (XPDM). Что более важно, WDDM позволяет запланировать несколько операций GPU одновременно и отображать страницы памяти видеокарты на обычную системную память, если вся память видеокарты израсходована. Запомните в качестве главного эмпирического правила: WPF предоставляет некоторого рода аппаратное ускорение всем драйверам WDDM и драйверам XPDM, созданным после ноября 2004 г., когда Microsoft издала новые руководства по разработке драйверов. Разумеется, уровень поддержки отличается. Когда инфраструктура WPF запускается в первый раз, она оценивает видеокарту и назначает ей рейтинг от 0 до 2, как описано во врезке "Уровни WPF". Среди обещаний, связанных с WPF, было и то, что вам не нужно беспокоиться о деталях и сложностях, связанных со специфическим аппаратным обеспечением. Технология WPF достаточно интеллектуальна, чтобы по возможности использовать аппаратную оптимизацию, но в случае неудачи все будет обработано программно. Поэтому если вы запустите WPF-приложение на компьютере с унаследованной видеокартой, интерфейс будет выглядеть так, как он был разработан. Конечно, программные альтернативы могут оказаться значительно медленнее, так что вы столкнетесь с тем, что компьютеры со старыми видеокартами не очень хорошо отрабатывают расширенные приложения WPF — особенно те, что включают сложную анимацию или другие сложные графические эффекты. На практике может быть принято решение упростить некоторые сложные эффекты в пользовательском интерфейсе, в зависимости от уровня аппаратной поддержки, доступной клиенту (определяется свойством RenderCapability.Tier). На заметку! Целью WPF является взвалить на видеокарту как можно больше работы, чтобы сложные графические процедуры ограничивались возможностями визуализации (узлом обработки графики), а не вычислительной мощностью процессора (центральным процессором компьютера). При таком подходе центральный процессор высвобождается для другой работы, видеокарта используется максимально эффективно и появляется возможность пользоваться преимуществами новых видеокарт по мере их появления. Уровни WPF Видеокарты значительно различаются между собой. Когда WPF оценивает видеокарту, то учитывает множество факторов, включая объем памяти видеокарты, поддержку построителей текстур (встроенные процедуры вычисления пиксельных эффектов наподобие прозрачности), вершинных построителей текстур (встроенные процедуры вычисления значений вершин треугольника, которые применяются при текстурировании трехмерных объектов). На основе всех этих деталей определяется значение уровня визуализации WPF.
28 Глава 1. Введение в WPF WPF распознает три уровня визуализации. 1. Уровень визуализации 0. Видеокарта не предоставляет никакого аппаратного ускорения. Это соответствует версии DirectX ниже 7.0. 2. Уровень визуализации 1. Видеокарта обеспечивает частичное аппаратное ускорение. Это соответствует версии DirectX выше 7.0, но ниже 9.0. 3. Уровень визуализации 2. Все средства, которые могут быть ускорены аппаратно, будут ускорены. Это отвечает версии DirectX 9.0 и выше. В некоторых ситуациях требуется программно проверить текущий уровень визуализации, чтобы выборочно отключить некоторые сложные графические средства на менее мощных картах. Для этого используется статическое свойство Tier класса System.Windows.Media. RenderCapability. Но здесь должен быть предпринят один трюк. Чтобы извлечь значение уровня из свойства Tier, необходимо выполнить сдвиг на 16 бит, как показано ниже: int renderingTier = (RenderCapability.Tier » 16); if (renderingTier == 0) {...} else if (renderingTier == 1) {...} Такое проектное решение допускает расширяемость. В будущих версиях WPF другие биты свойства Tier могут быть использованы для сохранения информации о поддержке других свойств, создавая в результате подуровни. За дополнительной информацией об аппаратно ускоряемых средствах WPF для уровней 1 и 2, а также за списками видеокарт соответствующих уровней обращайтесь по адресу http://msdn. microsoft.com/ru-ru/library/ms742196(v=VS. 100) .aspx. WPF: высокоуровневый API-интерфейс Даже если бы единственным достоинством WPF было аппаратное ускорение через DirectX, это уже стало бы значительным усовершенствованием, хоть и не революционным. Однако WPF на самом деле включает целый набор высокоуровневых служб, ориентированных на прикладных программистов. Ниже приведен список некоторых наиболее существенных изменений, которые привнесла с собой технология WPF в мир программирования Windows-приложений. • Веб-подобная модель компоновки. Вместо того чтобы фиксировать элементы управления на месте с определенными координатами, WPF поддерживает гибкий поток, размещающий элементы управления на основе их содержимого. В результате получается пользовательский интерфейс, который может быть адаптирован для отображения высоко динамичного содержимого или к разным языкам. • Богатая модель рисования. Вместо рисования пикселей в WPF вы имеете дело с примитивами — базовыми фигурами, блоками текста и прочими графическими ингредиентами. Кроме того, доступны такие новые средства, как действительно прозрачные элементы управления, возможность укладывания друг на друга множества уровней с разной степенью прозрачности, а также встроенная поддержка трехмерной графики. • Развитая текстовая модель. После многих лет нестандартной обработки текстов WPF наконец-то предоставляет Windows-приложениям возможность отображения расширенного стилизованного текста в любом месте пользовательского интерфейса. И если нужно отображать значительные объемы текста, для повышения читабельности можно воспользоваться развитыми средствами отображения документов, такими как переносы, разбиение на колонки и выравнивание.
Глава 1. Введение в WPF 29 • Анимация как первоклассная программная концепция. В WPF нет необходимости использовать таймер для того, чтобы заставить форму перерисовать себя. Вместо этого доступна анимация — неотъемлемая часть платформы. Анимация определяется декларативными дескрипторами, и WPF запускает ее в действие автоматически. • Поддержка аудио и видео. Прежние инструментальные наборы для построения пользовательских интерфейсов, такие как Windows Forms, были весьма ограничены в работе с мультимедиа. Однако WPF включает поддержку воспроизведения любого аудио- или видеофайла, поддерживаемого проигрывателем Windows Media, позволяя воспроизводить более одного медиафайла одновременно. Что еще больше впечатляет — WPF предоставляет в ваше распоряжение инструменты для интеграции видеосодержимого в остальную часть пользовательского интерфейса, позволяя выполнять такие экзотические трюки, как размещение видеоокна на поверхности вращающегося трехмерного куба. • Стили и шаблоны. Стили позволяют стандартизировать форматирование и многократно использовать его по всему приложению. Шаблоны дают возможность изменить способ отображения элементов, даже таких основополагающих, как кнопки. Построение интерфейса с обложками еще никогда не было таким простым. • Команды. Большинству пользователей известно, что не имеет значения, откуда они инициируют команду открытия (Open) — через меню или панель инструментов; конечный результат один и тот же. Теперь эта абстракция доступна коду — можно определять команды приложения в одном месте и привязывать их к множеству элементов управления. • Декларативный пользовательский интерфейс. Хотя можно конструировать окно WPF в коде, в Visual Studio используется другой подход. Содержимое каждого окна сериализуется в виде XML-дескрипторов в документе XAML. Преимущество состоит в том, что пользовательский интерфейс полностью отделяется от кода, и дизайнеры графики могут использовать профессиональные инструменты для редактирования файлы XAML, улучшая внешний вид всего приложения. (XAML — это сокращение от Extensible Application Markup Language (расширяемый язык разметки приложений), который описан в главе 2.) • Приложения на основе страниц. Используя WPF, можно строить браузер-подобные приложения, которые позволяют перемещаться по коллекции страниц, оснащенной кнопками навигации "вперед" и "назад". WPF автоматически обрабатывает все сложные детали, такие как хронология посещения страниц. Проект можно даже развернуть в виде браузерного приложения, которое выполняется внутри Internet Explorer. Технология Windows Forms продолжает существовать WPF — это платформа для будущего разработки пользовательских интерфейсов Windows-приложений. Однако она не заменит полностью Windows Forms. Во многих отношениях Windows Forms представляет собой кульминацию предшествующего поколения технологий отображения, построенных на основе GDI/GDI+ и User32. Так какую же платформу следует использовать при разработке нового Windows- приложения? Если вы начинаете с нуля, идеальным выбором будет WPF, поскольку она предлагает лучшие возможности для будущих расширений и лучшую жизнеспособность. Аналогично, если нужно одно из средств, которые в WPF доступны, a Windows Forms — нет, например, трехмерное рисование или страничная организация приложений, то имеет смысл перейти на новую платформу. С другой стороны, если вы сделали существенные вложения в бизнес-приложение на основе Windows Forms, то не стоит
30 Глава 1. Введение в WPF перекодировать его на WPF. В ближайшие годы поддержка платформы Windows Forms будет продолжаться. Возможно, лучшая часть истории состоит в том, что в Microsoft предприняли значительные усилия для построения уровня взаимодействия между WPF и Windows Forms (он играет роль, аналогичную уровню взаимодействия, который позволяет приложениям .NET продолжать пользоваться унаследованными компонентами СОМ). В главе 30 вы узнаете о том, как использовать эту поддержку элементов управления Windows Forms в приложениях WPF и наоборот WPF предлагает аналогичную надежную поддержку интеграции с более старыми приложениями в стиле Win32. DirectX также продолжает существовать Существует одна область, для которой WPF не слишком хорошо подходит — создание приложений с требованиями к графике реального времени, таких как эмуляторы сложных физических процессов или современные интерактивные игры. Поскольку для такого рода приложений нужна максимально возможная видеопроизводительность, необходимо программировать на более низком уровне и использовать DirectX напрямую. Библиотеки управляемого кода .NET для программирования DirectX доступны для загрузки на сайте http://msdn.microsoft.com/directx. На заметку! Начиная с WPF 3.5 SP1, в Microsoft начали разрушать некоторые границы между DirectX и WPF. Теперь можно создать содержимое DirectX и поместить его внутри приложения WPF. Фактически, можно даже создать на его основе кисть и использовать ее для рисования элемента управления WPF, или же сделать ее текстурой и отобразить на трехмерную поверхность WPF Тема интеграции WPF и DirectX выходит за рамки настоящей книги, поэтому обращайтесь за этим в документацию MSDN, начиная с http://msdn.microsoft.com/ru-ru/ library/system.windows.interop.d3dimage(v=VS.100) .aspx. Silverlight Как и .NET Framework в целом, WPF представляет собой технологию, ориентированную на Windows. Это значит, что приложения WPF могут использоваться только на компьютерах, работающих под управлением операционной системы Windows. Приложения WPF, основанные на браузерах, ограничены аналогичным образом — они работают только на компьютерах Windows, хотя поддерживают браузеры и Internet Explorer, и Firefox. Эти ограничения не изменятся: в конце концов, отчасти целью Microsoft в отношении WPF является использование широких возможностей компьютеров Windows и сохранение инвестиций в такие технологии, как DirectX. Однако технология Silverlight спроектирована как подмножество платформы WPF, работает в любом современном браузере (Firefox, Google Chrome и Safari) за счет использования подключаемого модуля, и открыта для других операционных систем, таких как Linux и Mac OS. Этот амбициозный проект вызвал значительный интерес среди разработчиков. Во многих отношениях технология Silverlight основана на WPF и включает в себя многие соглашения WPF (наподобие разметки XAML, которая рассматривается в следующей главе). Тем не менее, Silverlight не охватывает ряд областей, среди которых трехмерная графика и отображение форматированных документов. В будущих выпусках Silverlight могут появиться некоторые новые средства, но наиболее сложные из них — вряд ли. Конечной целью Silverlight является предоставление мощного ориентированного на разработчика конкурента Adobe Flash. Однако Flash обладает ключевым преимуществом — он используется в веб-приложениях повсеместно, и подключаемые модули Flash установлены почти везде. Чтобы заставить разработчиков перейти на новую, менее устоявшуюся технологию, Microsoft придется снабдить Silverlight средствами следующего
Глава 1. Введение в WPF 31 поколения, обеспечить основательную совместимость и непревзойденную проектную поддержку. На заметку! Silverlight имеет две потенциальные аудитории: веб-разработчики, которые хотят создавать более интерактивные приложения, и разработчики Windows, которые хотят расширить свои приложения. Более подробно технология Silverlight описана в книге Silverlight 3 с примерами на С# для профессионалов (ИД "Вильяме", 2010 г.). Можно также посетить веб-сайт http://silverlight.net. Независимость от разрешения Традиционные Windows-приложения связаны определенными предположениями относительно разрешения экрана. Обычно разработчики рассчитывают на стандартное разрешение монитора (вроде 1024x768 пикселей) и проектируют свои окна с учетом этого, стараясь обеспечить разумное поведение при изменении размеров в большую и меньшую сторону. Проблема в том, что пользовательский интерфейс в традиционных Windows- приложениях не является масштабируемым. В результате, если вы используете монитор с высоким разрешением, который располагает пиксели более плотно, окно приложения становится меньше и читать текст в нем труднее. Эта проблема особенно актуальна для новых мониторов, которые имеют высокую плотность пикселей и соответственно работают с более высоким разрешением. Например, легче встретить мониторы (особенно на портативных компьютерах), которые имеют плотность пикселей в 120 dpi или 144 dpi (точек на дюйм), чем более традиционные 96 dpi. При их встроенном разрешении эти мониторы располагают пиксели более плотно, создавая напрягающие глаз мелкие элементы управления и текст. В идеале приложения должны использовать более высокую плотность пикселей, чтобы отобразить больше деталей. Например, монитор с высоким разрешением может отображать одинакового размера значки панели инструментов, но использовать дополнительные пиксели для отображения мелкой графики. Подобным образом можно сохранить некоторую базовую компоновку, но обеспечить более высокую четкость деталей. По разным причинам такое решение было невозможно в прошлом. Хотя можно изменять размер графического содержимого, нарисованного в GDI/GDI+, компонент User32 (который генерирует визуальное представление распространенных элементов управления) не поддерживает реального масштабирования. WPF не страдает от этой проблемы, потому что самостоятельно визуализирует все элементы пользовательского интерфейса — от простых фигур до таких распространенных элементов управления, как кнопки. В результате если вы создаете кнопку шириной в 1 дюйм на обычном мониторе, она останется шириной в 1 дюйм и на мониторе с высоким разрешением. WPF просто визуализирует ее более детализировано, с большим количеством пикселей. Так выглядит картина в целом, но нужно уточнить еще несколько деталей. Самое важное, что следует осознать — WPF базирует свое масштабирование на системной установке DPI, а не на DPI физического дисплейного устройства. Это совершенно логично — в конце концов, если вы отображаете приложение на 100-дюймовом проекторе, то, скорее всего, отойдете подальше на несколько шагов и будете ожидать увидеть огромную версию окон. Конечно, не желательно, чтобы WPF масштабировал приложение, уменьшая его до "нормального" размера. Аналогично, если вы используете портативный компьютер с дисплеем высокого разрешения, то хотите увидеть несколько уменьшенные окна; это цена, которую приходится платить за то, чтобы уместить всю информацию на маленьком экране. Более того, у разных пользователей разные предпочтения на
32 Глава 1. Введение в WPF этот счет. Некоторым нужны расширенные подробности, в то время как другие хотят увидеть больше содержимого. Так каким же образом WPF определяет, насколько большим должно быть окно приложения? Краткий ответ состоит в том, что при вычислении размеров WPF использует системную установку DPI. Но чтобы понять, как это в действительности работает, необходимо более детально ознакомиться с системой измерений WPF. Единицы WPF Окно WPF и все элементы внутри него измеряются в независимых от устройства единицах. Такая единица определена как 1/96 дюйма. Чтобы понять, что это означает на практике, нужно рассмотреть пример. Предположим, что вы создаете в WPF маленькую кнопку размером 96x96 единиц. Если вы используете стандартную установку Windows DPI (96 dpi), то каждая независимая от устройства единица измерения соответствует одному реальному физическому пикселю. Это потому, что WPF использует следующее вычисление: [Размер в физических единицах] = [Размер в независимых от устройства единицах] х [DPI системы] = 1/96 дюйма х 96 dpi = 1 пиксель По сути, WPF предполагает, что ему нужно 96 пикселей, чтобы отобразить один дюйм, потому что Windows сообщает ему об этом через системную настройку DPI. Однако в действительности это зависит от применяемого дисплейного устройства. Например, рассмотрим 20-дюймовый жидкокристаллический монитор с максимальным разрешением в 1600x1200 пикселей. Используя теорему Пифагора, вы можете вычислить плотность пикселей для этого монитора, как показано ниже: Гг_т л Vl6002 +12002 пикселей ^ пп ^ [DPI экрана] = = 100 dpi 19дюймов В этом случае плотность пикселей составляет 100 dpi — немного больше того, что предполагает Windows. В результате на этом мониторе кнопка размером 96x96 пикселей будет несколько меньше одного дюйма. С другой стороны, рассмотрим 15-дюймовый жидкокристаллический монитор с разрешением 1024x768 пикселей. Здесь плотность пикселей составит около 85 dpi, поэтому кнопка размером 96x96 пикселей окажется размером немного больше 1 дюйма. В обоих случаях, если вы уменьшите размер экрана (скажем, переключившись на разрешение 800x600), то кнопка (и любой другой экранный элемент) станет пропорционально больше. Причина в том, что системная установка DPI останется 96 dpi. Другими словами, Windows продолжает предполагать, что 96 пикселей составляют дюйм, несмотря на то, что при меньшем разрешении потребуется существенно меньше пикселей. Совет. Возможно, вам известно, что жидкокристаллические мониторы создаются с единственным разрешением, которое называется естественным разрешением. При более низком разрешении монитору приходится использовать интерполяцию, чтобы заполнить лишние пиксели, в это может вызвать нерезкость. Чтобы получить наилучшее качество изображения, всегда лучше использовать естественное разрешение. Если хотите иметь более крупные окна, кнопки и текст, рассмотрите вместо этого возможность модификации системной установки DPI (как описано далее). Системная установка DPI До сих пор пример кнопки WPF работал точно так же, как любой другой интерфейсный элемент в Windows-приложении любого иного типа. Отличие проявляется при из-
Глава 1. Введение в WPF 33 менении вашей системной установки DPI. В предыдущем поколении Windows это средство иногда называли крупными шрифтами. Это потому, что системная установка DPI влияет на размер системных шрифтов, часто оставляя прочие детали неизменными. На заметку! Многие Windows-приложения не полностью поддерживают увеличенные установки DPI. В худшем случае увеличение системной установки DPI может привести к появлению окон, в которых некоторое содержимое увеличено, а другое — нет, что может привести к утере части содержимого или даже к нефункциональным окнам. Здесь поведение WPF отличается. WPF воспринимает системную установку DPI естественным образом и без особых затрат. Например, если вы измените системную установку DPI на 120 dpi (распространенный выбор пользователей экранов с большим разрешением), WPF предполагает, что для заполнения дюйма пространства нужно 120 пикселей. WPF использует следующее вычисление для определения того, как он должен транслировать логические единицы в физические пиксели устройства: [Размер в физических единицах] = [Размер в независимых от устройства единицах] х [DPI системы] =1/96 дюйма х 120 dpi =1,25 пикселя Другими словами, когда вы устанавливаете системную настройку DPI в 120 dpi, то механизм визуализации WPF предполагает, что одна независимая от устройства единица измерения соответствует 1,25 пикселя. Если вы отображаете кнопку 96x96, то физический ее размер составит 120x120 пикселей (потому что 96 х 1,25 = 120). Именно такого результата вы и ожидаете — кнопка размером в 1 дюйм имеет такой же размер на мониторе с повышенной плотностью пикселей. Такое автоматическое масштабирование было бы не слишком полезным, если бы касалось только кнопок. Но WPF использует независимые от устройства единицы для всего, что отображает, включая фигуры, элементы управления, текст и любые другие ингредиенты, которые помещаются в окно. В результате можно изменять системную установку DPI, как вам заблагорассудится, и WPF незаметно подгонит размеры окон приложения. На заметку! В зависимости от системной установки DPI вычисляемый размер пикселя может быть выражен дробным значением. Можно предположить, что WPF просто округляет все размеры до ближайшего пикселя. Однако по умолчанию WPF поступает несколько иначе. Если грань элементов приходится на точку между пикселями, WPF использует сглаживание, чтобы размыть эту грань. Это может показаться странным решением, но на самом деле оно вполне оправдано. Элементы управления не обязательно должны иметь прямые четкие грани, если для их отображения применяется специально отрисованная графика, поэтому некоторая степень сглаживания все равно необходима. Шаги для изменения системной установки DPI зависят от операционной системы. В следующих разделах объясняется, что следует делать, в зависимости от используемой операционной системы. Windows XP 1. Щелкните правой кнопкой мыши на рабочем столе и выберите в контекстном меню пункт Свойства. 2. В открывшемся диалоговом окне перейдите на вкладку Параметры и щелкните на кнопке Дополнительно.
34 Глава 1. Введение в WPF 3. На вкладке Общие выберите в списке Масштаб (количество точек на дюйм) вариант Обычный размер (96 точек/дюйм) или Крупный размер A20 точек/дюйм). Это две рекомендованных опции для Windows XP, потому что специальные установки DPI, скорее всего, не будут поддерживаться старыми программами. Чтобы попробовать установить собственное значение DPI, выберите вариант Особые параметры. Затем можно указать определенное значение в процентах (например, 175% увеличивает стандартное значение 96 dpi до 168 dpi). Windows Vista 1. Щелкните правой кнопкой мыши на рабочем столе и выберите в контекстном меню пункт Персонализация. 2. В списке ссылок слева щелкните на Корректировка размеров шрифта (DPI). 3. Выберите один из переключателей 96 точек/дюйм и 120 точек/дюйм либо щелкните на кнопке Другой размер шрифта, чтобы указать специальное значение DPI. После этого можно задать значение в процентах (например, 175% увеличивает стандартное значение 96 dpi до 168 dpi). Кроме того, здесь имеется флажок Использовать масштабы в стиле Windows XP, который описан во врезке "Масштабирование DPI в Windows Vista и Windows 7". Windows 7 1. Щелкните правой кнопкой мыши на рабочем столе и выберите в контекстном меню пункт Персонализация. 2. В списке ссылок внизу слева щелкните на Экран. 3. Выберите один из переключателей Мелкий (опция по умолчанию), Средний или Крупный. Эти опции также описаны в процентах масштабирования A00%, 125% или 150%) и на самом деле соответствуют значениями 96 dpi, 120 dpi и 144 dpi. Первые две соответствуют стандартам, имеющимся в Windows Vista и Windows XP, а третья — несколько больше. В качестве альтернативы можно щелкнуть на ссылке Другой размер шрифта (точек на дюйм) и указать специальное значение масштаба, как показано на рис. 1.1 (например, 175% увеличивает стандартное значение 96 dpi до 168 dpi). Кроме того, здесь имеется флажок Использовать масштабы в стиле Windows XP, который описана во врезке "Масштабирование DPI в Windows Vista и Windows 7". Выбор масштаба ggj] Для установки масштаба выберите процентное соотношение из списка или переместите ползунок с Масштаб от обычного размера: 1 | 1 1 ' 0 1 Segoe 1Д 9 пт, 96 пикселей на дюйм льзовать масштабы в иле Windows № помощью мыши. 100% - 1 1 2 •1 3 • ОК ] [ Отмена ) Рис. 1.1. Изменение системной установки DPI
Глава 1. Введение в WPF 35 Масштабирование DPI в Windows Vista и Windows 7 Поскольку старые приложения печально известны отсутствием поддержки высоких значений DPI, в Windows Vista появился новый прием, который получил название масштабирование растровых изображений (bitmap scaling). В Windows 7 это средство также поддерживается. Если вы запускаете приложение, которое не поддерживает высоких значений DPI, то Windows изменяет размер содержимого окна до желаемого DPI, как если бы это было просто графическое изображение. Преимущество такого решения в том, что приложению кажется, что оно работает при стандартных 96 dpi. ОС Windows незаметно транслирует ввод (такой как щелчки кнопками мыши) и маршрутизирует его в правильное место соответствующей "реальной" координатной системы. Алгоритм масштабирования, используемый Windows, достаточно хорош — он старается избегать размытости граней и использует аппаратную поддержку видеокарты, когда это позволяет увеличить скорость, но это неизбежно приводит к некоторой общей размытости изображения. К тому же это имеет серьезные ограничения, связанные с тем, что Windows не может распознать старые приложения, которые поддерживают высокие значения DPI. Поэтому приложения должны включать манифест или вызывать SetProcessDPIAware (в User32) для объявления о своей поддержке высоких значений DPI. Хотя WPF-приложения обрабатывают этот шаг корректно, приложения, разработанные до появления Windows Vista, не могут воспользоваться ни одним из подходов, и обречены на неидеальное масштабирование растровых изображений. Существуют два возможных решения. При наличии нескольких специфичных приложений, которые поддерживают высокие установки DPI, но не сообщают об этом, эту деталь можно сконфигурировать вручную. Для этого щелкните правой кнопкой мыши на ярлыке, запускающем приложение (в меню Пуск) и выберите в контекстном меню пункт Свойства. На вкладке Совместимость отметьте флажок Отключить масштабирование изображения при высоком разрешении экрана. Однако если придется конфигурировать много приложений, эти действия могут оказаться довольно утомительными. Другое возможное решение заключается в том, чтобы вообще отключить масштабирование растровых изображений. Для этого отметьте флажок Использовать масштабы в стиле Windows ХР в диалоговом окне Выбор масштаба, которое показано на рис. 1.1. Единственное ограничение этого подхода связано с тем, что могут существовать приложения, которые некорректно отображаются (и потому могут даже оказаться неработоспособными) при высоких установках DPI. По умолчанию флажок Использовать масштабы в стиле Windows XP отмечен для значений 120 dpi и менее, но не отмечен для значений свыше 120 dpi. Растровая и векторная графика Когда вы имеете дело с обычными элементами управления, то можете рассчитывать на независимость WPF от разрешения. WPF автоматически заботится о том, чтобы все имело правильные размеры. Однако если в приложении планируется использовать изображения, подобной уверенности быть не может. Например, в традиционных Windows- приложениях для команд панели инструментов применяются крошечные растровые изображения. В приложении WPF такой подход не идеален, потому что растровое изображение может отображать артефакты (размытые), которые будут масштабироваться вверх и вниз согласно системной установке DPI. Вместо этого при проектировании пользовательского интерфейса WPF даже самые мелкие значки обычно реализованы в векторной графике. Векторная графика определена как набор фигур, каждая из которых может быть легко масштабирована до любых размеров. На заметку! Разумеется, отображение векторной графики требует больше времени, чем отрисов- ка базового растрового изображения, но WPF включает набор приемов оптимизации, которые призваны снизить накладные расходы, всегда обеспечивая разумную производительность.
36 Глава 1. Введение в WPF Важность независимости от разрешения переоценить трудно. На первый взгляд это кажется очевидным, элегантным решением старой проблемы (что так и есть). Однако чтобы проектировать полностью масштабируемые интерфейсы, разработчики должны взять на вооружение новый образ мышления. Архитектура WPF Технология WPF использует многоуровневую архитектуру. На вершине ваше приложение взаимодействует с высокоуровневым набором служб, которые полностью написаны на управляемом коде С#. Действительная работа по трансляции объектов .NET в текстуры и треугольники Direct3D происходит "за кулисами", с использованием низкоуровневого неуправляемого компонента по имени milcore.dll. Библиотека milcore.dll реализована в неуправляемом коде потому, что ей требуется тесная интеграция с Direct3D, и вдобавок для нее чрезвычайно важна производительность. На рис. 1.2 показаны уровни, на которых построена работа приложения WPF. PresentationFramework.dll Управляемый API-интерфейс WPF PresentationCore.dll WindowsBase.dll / \ / X milcore.dll WindowsCodecs.dll i т Direct3D User32 Уровень медиа-интеграции Рис. 1.2. Архитектура WPF Ниже описаны ключевые компоненты, присутствующие на рис. 1.2. • PresentationFramework.dll содержит типы WPF верхнего уровня, включая те, что представляют окна, панели и прочие виды элементов управления. Также он реализует высокоуровневые программные абстракции, такие как стили. Большинство классов, которые вы будете использовать, находятся непосредственно в этой сборке. • PresentationCore.dll содержит базовые типы, такие как UIElement и Visual, от которых унаследованы все фигуры и элементы управления. Если вам не нужен полный уровень абстракции окон и элементов управления, можете опуститься ниже, на этот уровень, и продолжать пользоваться преимуществами механизма визуализации WPF. • WindowsBase.dll содержит еще более базовые ингредиенты, которые потенциально могут применяться вне WPF, такие как Dispatcher Object и Dependency Object, поддерживающие механизм свойств зависимости (эта тема будет детально рассмотрена в главе 4).
Глава 1. Введение в WPF 37 • milcore.dll — ядро системы визуализации WPF и фундамент уровня медиа- интеграции (Media Integration Layer — MIL). Его составной механизм транслирует визуальные элементы в треугольники и текстуры, которых ожидает Direct3D. Хотя milcore.dll считается частью WPF, это также важнейший компонент операционных систем Windows Vista и Windows 7. В действительности DWM (Desktop Window Manager — диспетчер окон рабочего стола) использует milcore.dll для отображения рабочего стола. На заметку! C6opKymilcore.dll иногда называют механизмом "управляемой графики". Подобно тому, как общеязыковая исполняющая среда (common language runtime — CLR) управляет жизненным циклом приложения .NET, milcore.dll управляет состоянием дисплея. И так же, как CLR избавляет от забот об освобождении объектов и восстановлению памяти, milcore.dll избавляет от необходимости думать о недействительности и перерисовке окна. Вы просто создаете объекты с содержимым, которое хотите отобразить, a milcore.dll рисует соответствующие части окна, когда оно перемещается, скрывается и раскрывается, сворачивается и восстанавливается, и т.д. • WindowsCodecs.dll —низкоуровневый API-интерфейс, обеспечивающий поддержку изображений (например, обработку, отображение и масштабирование растровых изображений и файлов JPEG). • Direct3D — низкоуровневый API-интерфейс, через который визуализируется вся графика в WPF. • User32 используется для определения того, какое место на экране к какой программе относится. В результате он по-прежнему вовлечен в WPF, но не участвует в визуализации распространенных элементов управления. Наиболее важный факт, который потребуется осознать, состоит в том, что Direct3D визуализирует все рисование в WPF. При этом не важно, установлена на компьютере видеокарта со скромными возможностями или же более мощная, используются базовые элементы управления или рисуется более сложное содержимое, запускается приложение в Windows ХР, Windows Vista или Windows 7. Даже двумерные фигуры и обычный текст трансформируются в треугольники и проходят по трехмерному конвейеру. Какие- либо обращения к GDI+ или User32 отсутствуют. Иерархия классов Читая эту книгу, большую часть времени вы потратите на изучение пространств имен и классов WPF. Но прежде чем начать, полезно взглянуть на общую иерархию классов, которые ведут к базовому набору элементов управления WPF На рис. 1.3 показан базовый обзор некоторых ключевых ветвей иерархии классов. Продвигаясь по главам этой книги, вы будете знакомиться с указанными (и связанными с ними) классами более подробно. В последующих разделах описаны основные классы из этой диаграммы. Многие из них ведут к целым ветвям элементов (таких как фигуры, панели и элементы управления). На заметку! Основные пространства имен WPF начинаются в System.Windows (например, System.Windows, System.Windows .Controls и System.Windows .Media). Единственным исключением являются пространства имен, начинающиеся с System.Windows. Forms, которые относятся к инструментам Windows Forms.
38 Глава 1. Введение в WPF Shape DispatcherObject i DependencyObject i Visual Условные обозначения Абстрактный класс Конкретный класс ж UlElement FrameworkElement I Control Panel ContentControl 4 ItemsControl Рис. 1.3. Фундаментальные классы WPF System. Threading. DispatcherObject Приложения WPF используют знакомую однопоточную модель (single-thread affinity — STA), а это означает, что весь пользовательский интерфейс принадлежит единственному потоку. Взаимодействовать с элементами пользовательского интерфейса из других потоков небезопасно. Чтобы содействовать работе этой модели, каждое WPF-приложение управляется диспетчером, координирующим сообщения (появляющиеся в результате клавиатурного ввода, перемещений курсора мыши и таких процессов платформы, как компоновка). Будучи унаследованным от DispatcherObject, каждый элемент пользовательского интерфейса может удостовериться, выполняется ли код в правильном потоке, и обратиться к диспетчеру, чтобы направить код в поток пользовательского интерфейса. Подробнее о модели многопоточности WPF речь пойдет в главе 31. System.Windows. DependencyObject В WPF центральный путь взаимодействия с экранными элементами пролегает через свойства. На ранней стадии цикла проектирования архитекторы WPF решили создать более мощную модель свойств, которая положена в основу таких средств, как уведомления об изменениях, наследуемые значения по умолчанию и более экономичное хранилище свойств. Конечным результатом стало средство свойств зависимости
Глава 1. Введение в WPF 39 (dependency property), с которым вы ознакомитесь в главе 4. За счет наследования от DependencyObject, классы WPF получают поддержку свойств зависимости. System.Windows.Media.Visual Каждый элемент, появляющийся в WPF, в основе своей является Visual. Класс Visual можно воспринимать как единственный объект рисования, инкапсулирующий в себе инструкции рисования, дополнительные подробности рисования (наподобие отсечения, прозрачности и настроек трансформации) и базовую функциональность (вроде проверки попадания). Класс Visual также обеспечивает связь между управляемыми библиотеками WPF и сборкой milcore.dll, которая визуализирует отображение. Любой класс, унаследованный от Visual, обладает способностью отображаться в окне. Если вы предпочитаете создавать свой пользовательский интерфейс с применением легковесного API-интерфейса, не обладающего высокоуровневыми средствами WPF, то можете программировать непосредственно с использованием объектов Visual, как описано в главе 14. System. Windows. UIElement Класс UIElement добавляет поддержку таких сущностей WPF, как компоновка (layout), ввод (input), фокус (focus) и события (events) — все, что команда разработчиков WPF называет аббревиатурой LIFE. Например, именно здесь определен двухшаговый процесс измерения и организации компоновки, о котором вы узнаете в главе 18. Здесь же щелчки кнопками мыши и нажатия клавиш трансформируются в более удобные события, такие как MouseEnter. Как и со свойствами, WPF реализует расширенную систему передачи событий, именуемую маршрутизируемыми событиями (routed events). В главе 5 будет показано, как она работает. И, наконец, UIElement добавляет поддержку команд (см. главу 9). Sys tern. Windows. FrameworkElemen t Класс FrameworkElement — конечный пункт в центральном дереве наследования WPF Он реализует некоторые члены, которые просто определены в UIElement. Например, UIElement устанавливает фундамент для системы компоновки WPF, но FrameworkElement включает ключевые свойства (вроде HorizontalAlignment и Margin), которые поддерживают его. UIElement также добавляет поддержку привязки данных, анимации и стилей — все они являются центральными средствами. System. Windows. Shapes. Shape От этого класса наследуются базовые фигуры, такие как Rectangle, Polygon, Ellipse, Line и Path. Эти фигуры могут использоваться наряду с более традиционными графическими элементами Windows вроде кнопок и текстовых полей. Построением фигур мы займемся в главе 12. System. Windows. Controls. Control Элемент управления (control) — это элемент, который может взаимодействовать с пользователем. К нему очевидным образом относятся такие классы, как Text Box, Button и ListBox. Класс Control добавляет дополнительные свойства для установки шрифта, а также цветов переднего плана и фона. Но наиболее интересная деталь, которую он предоставляет — это поддержка шаблонов, которая позволяет заменять стандартный внешний вид элемента управления собственным рисованием. Шаблоны элементов управления рассматриваются в главе 17.
40 Глава 1. Введение в WPF На заметку! В программировании с применением Windows Forms любой визуальный компонент в форме называется элементом управления. В WPF это не так. Визуальные единицы называются элементами (element), и только некоторые из них являются элементами управления (те, что могут принимать фокус и взаимодействовать с пользователем). Еще более запутывает эту систему то, что многие элементы определены в пространстве имен System.Windows.Controls, хотя они не унаследованы от System.Windows.Controls.Control и не могут считаться элементами управления. Примером может служить класс Panel. Sys tern. Windows. Controls. ContentControl Это базовый класс для всех элементов управления, которые имеют отдельный фрагмент содержимого. Сюда относится все — от скромной метки Label до окна Window. Наиболее впечатляющая часть этой модели (которая более детально описана в главе 6) заключается в том, что единственный фрагмент содержимого может быть чем угодно — от обычной строки до панели компоновки, содержащей комбинацию других фигур и элементов управления. System. Windows. Controls. ItemsControl Это базовый класс для всех элементов управления, которые отображают коллекцию каких-то единиц информации, вроде ListBox и TreeView. Списочный элемент управления замечательно гибок; например, используя встроенные средства класса ItemsControl, можно трансформировать обычный ListBox в список переключателей, список флажков, упорядоченный набор картинок или комбинацию совершенно разных элементов по своему выбору. Фактически в WPF все меню, панели инструментов и линейки состояния на самом деле являются специализированными списками, и классы, реализующие их, наследуются от ItemsControl. Вы начнете использовать списки в главе 19, когда пойдет речь о привязке данных. Их расширение вы изучите в главе 20, а наиболее специализированные списочные элементы управления — в главе 22. System. Windows. Controls. Panel Это базовый класс для всех контейнеров компоновки — элементов, которые содержат в себе один или более дочерних элементов и упорядочивают их в соответствии с определенными правилами компоновки. Эти контейнеры образуют фундамент системы компоновки WPF, и их использование — ключ к упорядочиванию содержимого наиболее привлекательным и гибким способом. Система компоновки WPF более детально рассматривается в главе 3. WPF4 WPF 4 — относительно новая технология. Частично она входила в несколько выпусков .NET и постепенно совершенствовалась. • WPF 3.0. Первая версия WPF вышла вместе с двумя другими технологиями: Windows Communication Foundation (WCF) и Windows Workflow Foundation (WF). Все вместе это называлось .NET 3.0. • WPF 3.5. ГЪд спустя, вышла новая версия WPF, как часть .NET Framework 3.5. Новые средства WPF в основном были слегка усовершенствованы, включая исправление ошибок и повышение производительности. • WPF 3.5 SP1. Когда вышел пакет обновлений .NET Framework Service Pack 1 (SP1), проектировщики WPF получили возможность добавить некоторые новые средства, подобные сглаженной графике (благодаря построителям текстуры) и изощренному элементу управления DataGrid.
Глава 1. Введение в WPF 41 • WPF 4. В последнем выпуске WPF появилось множество улучшений, включая ценные новые средства, построенные на базе существующей инфраструктуры WPF. Среди некоторых наиболее заметных изменений — улучшенная визуализация текста, более естественная анимация и поддержка средств Windows 7, таких как сенсорные возможности и новая панель задач. Новые средства В этой книге охвачены все концепции WPF, включая самые броские новые средства и базовые принципы, которые остаются неизменными с момента появления этой технологии. Однако если вы — опытный разработчик WPF, заглядывайте во врезки "Что нового?", предлагаемые в начале каждой главы. В них детализируется относительно новый материал, т.е. средства, которые появились в WPF 3.5 SP1 или WF 4. Если такой врезки нет, то, скорее всего, в главе рассматриваются устоявшиеся средства WPF, которые в последнем выпуске не изменились. Приведенный ниже список поможет идентифицировать ряд наиболее заметных изменений, произошедших со времени выхода WPF 3.0, а также отыскать главы, в которых обсуждается каждое из средств. • Новые элементы управления. Семейство элементов WPF продолжает расти. Теперь оно включает профессиональный выглядящий DataGrid (глава 22), стандартные DataPicker и Calendar (глава 6) и встроенный WebBrowser для просмотра HTML-разметки и веб-серфинга (глава 24). Отдельная загрузка также добавляет полезный элемент управления Ribbon (глава 25), который придает приложениям современный вид. • Усовершенствования двухмерной графики. Теперь визуальное представление каждого элемента может быть радикально изменено посредством эффектов в духе PhotoShop — через построители текстур (с использованием вплоть до версии 3 стандарта построителей текстуры). Разработчики, которые желают манипулировать индивидуальными пикселями вручную, могут также генерировать и модифицировать изображения с помощью класса WriteableBitmap. Оба средства рассматриваются в главе 14. • Облегчение анимации. Эти функции позволяют создавать более жизнеподобные анимации, которые прыгают, ускоряются и качаются естественным образом. Полное описание содержится в главе 15. • Диспетчер визуального состояния. Впервые появившийся в Silverlight, диспетчер визуального состояния (см. главу 17) облегчает изменение обложек элементов управления без необходимости понимания их внутреннего устройства и работы. • Windows 7. Новейшая операционная система от Microsoft добавила целый пакет новых средств. WPF включает естественную поддержку улучшенной панели задач, позволяя использовать списки переходов, перекрытия значков, уведомления о ходе работ и панели инструментов с миниатюрами (все это рассматривается в главе 23). При наличии соответствующего оборудования можно использовать поддержку WPF сенсорных возможностей Windows 7 (глава 5), которые позволяют с помощью жестов на сенсорном экране управлять визуальными объектами. • Улучшенная визуализация. В WPF продолжает улучшаться качество отображения за счет преодоления проблем, связанных с моделью рисования, не зависящей от разрешения монитора. В WPF 4 можно использовать округление компоновки, которое выравнивает контейнеры по границам пикселей, гарантируя чистое изображение (см. главу 3). То же самое можно сделать при визуализации текста, гарантируя его четкость даже при самых маленьких размерах (см. главу 6).
42 Глава 1. Введение в WPF • Кэширование растровых изображений. При правильном сценарии рабочую нагрузку процессора можно снижать, кэшируя сложную векторную графику в памяти видеокарты. Эта техника удобна, в частности, в случае использования анимации и описана в главе 16. • XAML 2009. В WPF появилась новая версия стандарта разметки XAML, используемого для объявления пользовательского интерфейса в окне или на странице. В нем добавлен ряд небольших улучшений, но, скорее всего, вы пока не захотите ими пользоваться, потому что стандарт не встроен в компилятор WPF XAML. Подробнее об этой ситуации читайте в главе 2. WPF Toolkit Прежде чем новый элемент управления найдет свое место в библиотеках WPF платформы .NET, он начинает свою жизнь в составе отдельной загрузки инструментального набора WPF Toolkit. Хотя WPF Toolkit не предсказывает будущего направления развития WPF, это замечательное место, где можно найти практичные компоненты и элементы, выходящие за рамки обычных выпусков WPF. Так, например, WPF не включает никаких инструментов построения диаграмм, а в WPF Toolkit вы найдете набор элементов для создания столбчатых, круговых, линейных и прочих диаграмм. В этой книге периодически встречаются ссылки на WPF Toolkit, когда имеет смысл указать на полезную часть функциональности, которая не доступна в ядре исполняющей среды .NET Для загрузки WPF Toolkit, ознакомления с его кодом либо изучения документации обратитесь по адресу http://wpf.codeplex.com. Там же вы найдете ссылки на другие управляемые Microsoft проекты WPF, включая WPF Features (куда входят экспериментальные средства WPF) и средства тестирования WPF Visual Studio 2010 Хотя пользовательские интерфейсы WPF можно строить вручную либо с помощью графического инструмента Expression Blend, большинство разработчиков начинают с Visual Studio и проводят в нем большую часть времени. В этой книге предполагается, что вы пользуетесь Visual Studio, и периодически объясняется, как применять Visual Studio для решения важнейших задач, таких как добавление ресурса, конфигурирование свойств проекта или создание сборки с библиотекой элементов управления. Однако много времени на исследование разнообразных средств времени проектирования тратиться не будет. Вместо этого внимание будет сосредоточено на лежащей в основе разметке и коде, что понадобится для создания профессиональных приложений. На заметку! Возможно, вы уже знаете, как создается проект WPF в Visual Studio, но стоит кратко напомнить. Сначала выберите пункт меню File о New о Project (Файл ^Создатьо Проект) Затем в открывшемся диалоговом окне выберите группу Visual C#c=>Windows (в дереве слева), а в ней — шаблон WPF Application (в списке справа). В главе 24 вы узнаете о более специализированном шаблоне WPF Browser Application. Выбрав каталог, введите имя проекта и щелкните на кнопке ОК. В результате получается базовая структура приложения WPF Поддержка множества целевых платформ В прошлом каждая версия Visual Studio была тесно привязана к определенной версии .NET. Версия Visual Studio 2010 свободна от этого ограничения и позволяет проектировать приложения, ориентированные на любую версию .NET— от 2.0 до 4. Хотя очевидно невозможно создать приложение WPF для .NET 2.0, в версиях .NET 3.0 и 3.5 поддержка WPF имеется. Выбор в качестве целевой платформы .NET 3.0 обеспе-
Глава 1. Введение в WPF 43 чивает наиболее широкую совместимость (т.к. приложения .NET 3.0 могут работать под управлением исполняющих сред .NET 3.0, 3.5 и 4). Выбор в качестве целевой платформы .NET 3.5 или .NET 4 открывает доступ к новейшим средствам WPF, имеющимся в .NET. При создании нового проекта в Visual Studio можно выбирать целевую версию .NET Framework в раскрывающемся списке, который расположен в верхней части диалогового окна New Project (Новый проект) прямо над списком шаблонов проектов (рис. 1.4). New Project Instated Templates л Visual С* Windows Web Office Cloud Reporting SharePoint Sirverlight ::ШШ) ; NET Framework 20 NET Framework ЗЛ NET Framework 35 e! ' <|М1?1ге|п?1|Г|ТПн^,,1> Sort by: Default bplication Visual C* Visual C* Type: Visual C* Windows Presentation Foundation client application ЧН Console Application ^gfj Class Library d WPF Browser Application Visual C* Location: Solution: Solution name: Wpf Application DADes ktop\ Create пел solution Bro, Create directory for solution Add to source control Рис. 1.4. Выбор целевой версии .NET Framework Целевую версию можно изменить в любой момент позже, дважды щелкнув на узле Properties (Свойства) в окне Solution Explorer (Проводник решения) и изменив выбор в списке Target Framework (Целевая платформа). Для обеспечения аккуратной поддержки множества целевых платформ Visual Studio 2010 включает ссылочные сборки для каждой версии .NET. Эти сборки содержат метаданные каждого типа, но ничего из кода, нужного для их реализации. Это значит, что Visual Studio 2010 может использовать ссылочную сборку для настройки средства IntelliSense и проверки ошибок, гарантируя, что вы не сможете использовать элементы управления, классы или члены, которые не доступны в выбранной версии .NET. Эти метаданные также используются для определения того, что должно появиться в окне Properties (Свойства) и браузере объектов (Object Browser), и т.д., гарантируя, что вся IDE-среда будет ограничена выбранной версией .NET Клиентский профиль .NET Как ни странно, доступны два способа выбрать в качестве цели WPF 4. Первый способ — построить приложение, которое требует стандартной установки полной платформы .NET Framework 4. Второй способ — построить приложение, которому требуется .NET Framework 4 Client Profile (Клиентский профиль .NET Framework 4). Клиентский профиль — это подмножество .NET Framework, которое требуется многофункциональным клиентским приложениями вроде WPF Сюда не входят средства серверной стороны, такие как ASP.NET, отладчики, средства разработки, компиляторы кода и унаследованные средства (подобные поддержке баз данных Oracle). Более важно то, что клиент имеет меньший размер, требуя загрузки около 30 Мбайт, в то время как полный комплект распространения .NET Framework занимает около 100 Мбайт.
44 Глава 1. Введение в WPF Естественно, если приложение ориентировано на .NET Framework 4 Client Profile, оно без проблем будет работать под управлением полной версии .NET Framework. Концепция клиентского профиля появилась в .NET 3.5 SP1. Однако в ней по-прежнему присутствуют несколько моментов, которые мешают ей стать стандартом. В .NET 4 были проведены работы по тонкой настройке средств, включаемых в комплект клиентского профиля, предполагая сделать его стандартным выбором для любого приложения. В Visual Studio 2010 большинство проектов автоматически нацелены на .NET Framework 4 Client Profile. (Именно это вы получаете, выбирая .NET Framework 4 в диалоговом окне New Project.) Изменив настройку Target Framework (Целевая платформа) в свойствах проекта, можно увидеть более подробный список, который имеет отдельные опции для полной версии .NET Framework 4 и .NET Framework 4 Client Profile. При выборе целевой версии .NET часто важно учитывать, насколько широко распространены различные исполняющие среды в настоящее время. В идеале пользователи должны иметь возможность запускать приложения, не требуя дополнительного шага по загрузке и установке. Ниже дано несколько советов, которые помогут принять правильное решение. • Windows Vista включает .NET Framework 3.0. • Windows 7 включает .NET Framework 3.5 SP1. • .NET Framework 4 Client Profile является рекомендуемым обновлением (через службу Windows Update) для Windows Vista и Windows 7. Для компьютеров Windows XP оно является необязательным. Визуальный конструктор Visual Studio Несмотря на тот факт, что Visual Studio является важнейшим инструментом для программирования с применением WPF, в предыдущих версиях был существенный пробел в доступных возможностях — они не предлагали графического визуального конструктора для создания пользовательского интерфейса. В результате разработчики были вынуждены писать код XAML вручную либо переключаться между Visual Studio и более ориентированным на дизайн инструментом Expression Blend. В Visual Studio 2010, наконец, этот недостаток был восполнен за счет появления мощного визуального конструктора для создания пользовательских интерфейсов WPF. Однако тот факт, что Visual Studio 2010 позволяет легко перетаскивать окна WPF на поверхность проектирования, не означает, что это нужно делать прямо сейчас или вообще когда-либо. Как будет показано в главе 3, в WPF используется гибкая и тонкая модель компоновки, которая позволяет применять разные стратегии для задания размеров и позиционирования элементов в рамках пользовательского интерфейса. Для получения нужного результата понадобится использовать корректную комбинацию контейнеров компоновки, правильно организовать их и должным образом сконфигурировать их свойства. Visual Studio может помочь в этом, но будет намного легче, если первым делом освоить основы разметки XAML и компоновки WPF Это позволит впоследствии просматривать код разметки, сгенерированный Visual Studio, и при необходимости модифицировать его вручную. Овладев синтаксисом XAML (глава 2) и ознакомившись с семейством контейнеров компоновки WPF (глава 3), вы сможете сами выбирать, каким образом создавать окна. Часть профессиональных разработчиков используют Visual Studio, часть — Expression Blend, есть те, кто пишет код XAML вручную, а есть те, кто применяет комбинацию перечисленных методов с последующим конфигурированием в визуальном конструкторе Visual Studio.
Глава 1. Введение в WPF 45 Резюме В этой главе был представлен начальный обзор WPF и тех возможностей, которые эта платформа предлагает Вы узнали о лежащей в ее основе архитектуре и кратко об основных классах. WPF — это будущее разработок Windows-приложений. Со временем WPF превратится в систему, подобную User32 и GDI/GDI+, к которой будут добавляться новые расширения и высокоуровневые средства. В конечном итоге WPF позволит проектировать приложения, которые было бы невозможно (или, по крайней мере, непрактично) построить средствами Windows Forms. Естественно, WPF несет в себе много революционных изменений. Однако есть несколько ключевых принципов, которые нужно немедленно сформулировать, поскольку они совершенно отличаются от тех, что лежат в основе предшествующих инструментов для построения пользовательского интерфейса Windows, таких как Windows Forms. Ниже перечислены эти принципы. • Аппаратное ускорение. Все рисование WPF выполняется через DirectX, что позволяет этой технологии пользоваться преимущества современных видеокарт. • Независимость от разрешения. Технология WPF настолько гибкая, что может автоматически выполнять масштабирование вверх и вниз, приспосабливаясь к предпочтениям монитора, в зависимости от системных установок DPI. • Отсутствие фиксированного внешнего вида элементов управления. В традиционной разработке для Windows существует огромная пропасть между элементами управления, которые можно подогнать под ваши нужды (они называются самостоятельно рисуемыми), и теми, которые визуализируются операционной системой, и чей внешний вид, по сути, фиксирован. В WPF все, начиная от базового Rectangle и до стандартного Button или более сложного Toolbar, рисуется посредством механизма визуализации и является полностью настраиваемым. По этой причине элементы управления WPF часто называют лишенными внешности — они определяют функциональность элемента управления, но не имеют жестко привязанной внешности. • Декларативный пользовательский интерфейс. В следующей главе мы рассмотрим XAML — стандарт языка разметки, который используется для определения пользовательских интерфейсов WPF Язык XAML позволяет строить окна без кода. Впечатляет то, что XAML не ограничивает фиксированным неизменным пользовательским интерфейсом. Можно применять такие средства, как привязка данных и триггеры, для автоматизации базового поведения пользовательского интерфейса (вроде текстовых полей, обновляющих себя, когда вы перемещаетесь по источнику записи, или меток, которые подсвечиваются при наведении на них курсора мыши) — и все это вообще без написания кода С#. • Рисование на основе объектов. Даже если планируется работать на низком визуальном уровне (вместо высокого уровня элементов), рисовать в терминах пикселей не придется. Вместо этого будут создаваться объекты фигур, a WPF будет поддерживать отображение в наиболее оптимизированной манере. Эти принципы будут демонстрироваться в действии на протяжении всей книги. Но прежде чем двигаться дальше, необходимо изучить еще один дополняющий стандарт. В следующей главе представлен XAML — язык разметки, предназначенный для определения пользовательских интерфейсов WPF
ГЛАВА 2 XAML XAML (Extensible Application Markup Language — расширяемый язык разметки приложений) представляет собой язык разметки, используемый для создания экземпляров объектов .NET. Хотя язык XAML — это технология, которая может быть применима ко многим различным предметным областям, его главное назначение — конструирование пользовательских интерфейсов WPF. Другими словами, документы XAML определяют расположение панелей, кнопок и прочих элементов управления, составляющих окна в приложении WPF. Маловероятно, что вам придется писать код XAML вручную. Вместо этого вы будете пользоваться инструментом, генерирующим необходимый код XAML. Если вы — дизайнер графики, скорее всего, таким инструментом будет программа графического дизайна вроде Expression Blend. Если же вы — разработчик, то наверняка начнете с Visual Studio. Поскольку оба инструмента поддерживают XAML, вы можете создать базовый пользовательский интерфейс в Visual Studio, а затем передать его команде дизайнеров, которые доведут его до совершенства, добавив специальную графику с помощью Expression Blend. Фактически такая способность интегрировать рабочий поток разработчиков и дизайнеров — одна из ключевых причин создания Microsoft языка XAML. В этой главе предлагается детальное введение в XAML. Будет рассмотрено его предназначение, общая архитектура и синтаксис. Поняв основные правила XAML, вы узнаете, что возможно и что невозможно в пользовательском интерфейсе WPF, и как при необходимости провести в нем ручные изменения. Что более важно — за счет исследования дескрипторов в XAML-документе WPF вы можете много узнать об объектной модели, которая положена в основу пользовательских интерфейсов WPF, и подготовиться к углубленному ее изучению. Что нового? В WPF 4 был представлен XAML 2009 — обновленная версия языка XAML, имеющая множество полезных усовершенствований. Однако есть некоторые ограничения- в настоящее время XAML 2009 можно использовать только в несвязанных файлах XAML. Хотя Visual Studio поддерживает и несвязанные, и скомпилированные файлы XAML (как будет показано в этой главе), скомпилированные файлы XAML являются стандартом. Они позволяют не только работать с моделью отделенного кода, давая возможность подключать код с минимальными усилиями, но также гарантируют, что скомпилированное приложение будет иметь меньший размер и загрузится немного быстрее. В связи с этим XAML 2009 не будет использоваться в примерах, рассматриваемых в книге. Тем не менее, вы сможете получить предварительное представление о расширениях XAML 2009 в разделе "XAML 2009". Эта информация подготовит к будущим выпускам WPF, поскольку XAML 2009 претендует на роль нового стандарта (как только в Microsoft найдут время для переписывания, тестирования и оптимизации XAML-компилятора WPF).
Глава 2. XAML 47 Особенности XAML Разработчики давно поняли, что создавать сложные, графически насыщенные приложения намного легче, если отделить графическую часть от лежащего в основе кода. Таким образом, художники могут заниматься графикой, а разработчики — кодом. Обе части могут проектироваться и совершенствоваться по отдельности, без проблем, связанных с множеством версий. Графический интерфейс пользователя до WPF В традиционных технологиях отображения не существовало простого способа отделить графическое содержимое от кода. Ключевая проблема приложений Windows Forms состоит в том, что каждая форма, которую вы создаете, целиком определяется в коде С#. При помещении элементов управления на поверхность проектирования и их конфигурировании Visual Studio молча вносит изменения в код соответствующего класса формы. К сожалению, дизайнеры графики не располагают инструментами, которые могут работать с кодом С#. Вместо этого художники вынуждены создавать и экспортировать свой продукт в растровом формате. Эти растровые изображения затем могут использоваться для оформления окон, кнопок и других элементов управления. Такой подход хорошо работает с простыми интерфейсами, которые мало изменяются с течением времени, но весьма ограничен в других сценариях. К его проблемам можно отнести перечисленные ниже. • Каждый графический элемент (фон, кнопка и т.п.) должен экспортироваться как отдельное растровое изображение. Это ограничивает возможности их комбинирования и применения динамических эффектов, таких как сглаживание, прозрачность и тени. • Значительная часть логики пользовательского интерфейса должна быть встроена в код разработчиком. Сюда относятся размеры кнопок, позиционирование, эффекты от перемещения курсора мыши и анимация. Дизайнер графики не может контролировать эти детали. • Не существует внутренней связи между разными графическими элементами, так что легко создать не соответствующие друг другу наборы изображений. Отслеживание всех этих элементов привносит дополнительную сложность. • Растровые изображения не могут изменяться в размерах без потери качества. По этой причине пользовательский интерфейс на основе растрового изображения зависит от разрешения. Это значит, что он не может быть адаптирован к большим мониторам и дисплеям высокого разрешения, что нарушает основы проектной философии WPF. Если вам когда-либо доводилось проходить через процесс проектирования приложений Windows Forms с использованием специальной графики в командной среде, вы, несомненно, сталкивались с массой разочарований. Даже если интерфейс спроектирован с нуля дизайнером графики, он должен быть воссоздан в коде С#. Обычно дизайнеру графики просто приходится подготавливать макет, который затем нужно мучительно транслировать в работающее приложение. В WPF эта проблема решается с помощью XAML. При проектировании WPF-прило- жения в Visual Studio создаваемое окно не транслируется в код. Вместо этого оно се- риализуется в набор дескрипторов XAML. После запуска приложения эти дескрипторы используются для генерации объектов, составляющих пользовательский интерфейс.
48 Глава 2. XAML На заметку! Важно понимать, что WPF не требует обязательного применения XAML. Нет причин, по которым система Visual Studio не могла бы использовать подход Windows Forms и сразу создавать операторы кода, конструирующие окна WPF. Но в этом случае окно будет "заперто" в среде Visual Studio и доступно только программистам. Другими словами, для WPF не требуется XAML. Однако XAML открывает возможности для кооперации, поскольку другие инструменты проектирования понимают формат XAML. Например, изобретательный дизайнер может использовать такой инструмент, как Expression Design, чтобы настроить графику для приложения WPF, или же инструмент вроде Expression Blend, чтобы построить для него изощренную анимацию. По окончании чтения этой главы имеет смысл ознакомиться с официальным документом от Microsoft, доступным по адресу http://windowsclient.net/wpf/white-papers/ thenewiteration.aspx, в котором предлагается обзор XAML, и объясняются некоторые способы кооперации разработчиков и дизайнеров при построении приложения WPF. Совет. XAML играет ту же роль для приложений Windows, что управляющие дескрипторы для веб- приложений ASP.NET. Отличие состоит в том, что синтаксис дескрипторов ASP.NET задуман похожим на HTML, так что дизайнеры могут создавать веб-страницы, используя обычные приложения для веб-дизайна, такие как FrontPage и Dreamweaver. Как и в WPF, сам код веб-страницы ASP.NET обычно размещается в отдельном файле, облегчая проектирование Разновидности XAML Существует несколько разных способов использования термина XAML. До сих пор он применялся для ссылки на весь язык XAML, предлагающий основанный на XML синтаксис для представления дерева объектов .NET. (Эти объекты могут быть кнопками и текстовыми полями в окне, а также специальным, определенным вами классом. Фактически XAML даже может использоваться на других платформах, чтобы представлять объекты, не имеющие отношения к .NET.) Существует несколько подмножеств XAML. • WPF XAML включает элементы, описывающие содержимое WPF, такое как векторная графика, элементы управления и документы. В настоящее время это наиболее важное применение XAML, и именно это его подмножество будет рассматриваться в настоящей книге. • XPS XAML — часть WPF XAML, определяющая XML-представление форматированных электронных документов. Она опубликована как отдельный стандарт XML Paper Specification (XPS). Вы узнаете о XPS в главе 28. • Silverlight XAML — подмножество WPF XAML, предназначенное для Silverlight- приложений. Silverlight — это межплатформенный браузерный подключаемый модуль, который позволяет создавать расширенное веб-содержимое с двумерной графикой, анимацией, аудио и видео. Дополнительная информация о Silverlight была дана в главе 1. Можно также посетить сайт http://silverlight.net. • WF XAML включает элементы, описывающие содержимое Windows Workflow Foundation (WF). Дополнительная информация о WF доступна на сайте http:// msdn.microsoft.com/ru-ru/netframework/aa663328.aspx. Компиляция XAML Создатели WPF знали, что XAML не только нужен для решения проблемы совместного проектирования, он также должен быть быстрым. И хотя такие основанные на XML
Глава 2. XAML 49 форматы, как XAML, гибки и легко переносимы на другие инструменты и платформы, они не всегда являются наиболее эффективным выбором. XML задуман как непротиворечивый, читабельный и прямолинейный, но не компактный формат В WPF этот недостаток преодолен посредством BAML (Binary Application Markup Language — двоичный язык разметки приложений). BAML — это не что иное, как двоичное представление XAML. Когда вы компилируете приложение WPF в Visual Studio, все файлы XAML преобразуются в код BAML, и этот код BAML затем встраивается в виде ресурса в финальную сборку DLL или ЕХЕ. Язык BAML поддерживает лексемъи а это значит, что длинные фрагменты XAML заменены короткими лексемами. И код BAML не только существенно меньше, но он также оптимизирован, чтобы быстрее интерпретироваться во время выполнения. Большинству разработчиков не приходится беспокоиться о преобразовании XAML в BAML, потому что компилятор это делает "за кулисами". Однако можно использовать XAML без предварительной компиляции. Это может иметь смысл в сценариях, когда часть пользовательского интерфейса должна быть применена прямо во время выполнения (например, извлечена из базы данных в виде блока дескрипторов XAML). В разделе "Загрузка и компиляция XAML' далее в главе будет показано, как это работает. Создание XAML в Visual Studio В этой главе мы рассмотрим детали разметки XAML. Разумеется, при проектировании приложения вручную писать код XAML не придется. Вместо этого с помощью инструмента, подобного Visual Studio, создается нужное окно методом перетаскивания. Потому столь подробное изучение синтаксиса XAML может показаться излишним. Тем не менее, это, безусловно, необходимо. Понимание XAML чрезвычайно важно для проектирования приложений WPF. Это поможет разобраться в ключевых концепциях WPF, таких как присоединенные свойства (настоящая глава), компоновка (глава 3), маршрутизируемые события (глава 4), модель содержимого (глава 6) и т.д. Что более важно — существует целый ряд задач, решение которых возможно только с помощью вручную написанного Kon.a~XAML либо в этом случае оно существенно облегчается. Ниже перечислены примеры таких задач. • Привязка обработчиков событий. Присоединение обработчиков событий в наиболее распространенных местах — например, к событию Click для Button — легко сделать в Visual Studio. Однако, однажды поняв, как события привязываются в XAML, можно создавать более изощренные соединения. Например, можно установить обработчик событий, реагирующий на событие Click каждой кнопки окна. Более подробно эта технология рассматривается в главе 5. • Написание выражений привязки данных. Привязка данных позволяет извлекать данные из объекта и отображать их в привязанном элементе. Чтобы установить это отношение и сконфигурировать его работу, в код разметки XAML понадобится добавить выражение привязки данных. Привязка данных рассматривается в главе 8. • Определение ресурсов. Ресурсы — это объекты, которые определяются в специальном разделе кода XAML, а затем многократно используются в разных местах кода разметки. Ресурсы позволяют централизовать и стандартизировать форматирование и создание невизуальных объектов, таких как шаблоны и анимации. Создание и использование ресурсов будет описано в главе 10. • Определение анимации. Анимация — распространенный ингредиент приложений XAML. Обычно они определяются в виде ресурсов, конструируются с использованием разметки XAML, а затем привязываются к другим элементам управления (либо инициируются в коде). В настоящее время в Visual Studio не предусмотрена поддержка создания анимации во время проектирования. Анимация рассматривается в главе 15. • Определение шаблонов элементов управления. Элементы управления WPF проектируются как лишенные внешнего вида; это значит, что вместо стандартных визуальных представлений
50 Глава 2. XAML можно подставлять собственные. Чтобы сделать это, понадобится создать собственный шаблон элемента управления, который представляет собой не что иное, как блок разметки XAML. Шаблоны элементов управления описаны в главе 17. Большинство разработчиков WPF используют комбинацию приемов, разрабатывая часть пользовательского интерфейса с помощью инструмента проектирования (Visual Studio или Expression Blend), а затем проводя тонкую настройку за счет ручного редактирования кода разметки. В главе 3 рассматриваются контейнеры компоновки, которые удобнее всего использовать для правильного размещения множества элементов управления в окне. Основы XAML Стандарт XAML достаточно очевиден, если понять несколько его основополагающих правил. • Каждый элемент в документе XAML отображается на экземпляр класса .NET. Имя элемента в точности соответствует имени класса. Например, элемент <Button> сообщает WPF, что должен быть создан объект Button. • Как и любой XML-документ, код XAML допускает вложение одного элемента внутрь другого. Как будет показано, XAML предоставляет каждому классу гибкость в принятии решения относительно того, как справиться с такой ситуацией. Однако вложение обычно является способом выразить включение (containment). Другими словами, если вы видите элемент Button внутри элемента Grid, то пользовательский интерфейс, возможно, включает Grid, содержащий внутри себя Button. • Свойства каждого класса можно устанавливать через атрибуты. Тем не менее, в некоторых ситуациях атрибуты не достаточно мощны, чтобы справиться с этой работой. В этих случаях понадобятся вложенные дескрипторы со специальным синтаксисом. Совет. Если вы — полный новичок в XML, то лучше изучить его основы и только затем заняться XAML. Для быстрого ознакомления с XML можно обратиться к бесплатному веб-руководству по адресу http://www.w3schools.com/xml. Прежде чем продолжить, взгляните на следующий простейший документ XAML, представляющий новое пустое окно (как оно создано в Visual Studio). Строки пронумерованы для облегчения ссылок на них. 1 <Window x:Class="WindowsApplicationl.Windowl" 2 xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 Title="Windowl" Height=00" Width=00"> 5 6 <Grid> 7 </Grid> 8 </Window> Этот документ содержит всего два элемента — элемент верхнего уровня Window, который представляет все окно, и элемент Grid, куда можно поместить свои элементы управления. Хотя можно использовать любой элемент верхнего уровня, приложение WPF полагается только на несколько из них: • Window; • Page (похож на Window, но используется для приложений с возможностями навигации); • Application (определяет ресурсы приложения и начальные установки).
Глава 2. XAML 51 Как и во всех документах XML, может существовать только один элемент верхнего уровня. В предыдущем примере это означает, что закрытие элемента Window дескриптором </Window> завершает документ. Никакое дополнительное содержимое уже не допускается. Если вы посмотрите на открывающий дескриптор элемента Window, то найдете там несколько интересных атрибутов, в том числе имя класса и два пространства имен XML (рассматриваются в последующих разделах). Также вы обнаружите три свойства, показанные ниже: 4 Title="Windowl" Height=00" Width=00"> Каждый атрибут соответствует отдельному свойству в классе Window. В конечном итоге это инструктирует WPF о необходимости создать окно с заголовком Windowl размером 300x300 единиц. На заметку! Как известно из главы 1, в WPF используется относительная система измерения, которая не похожа на то, чего ожидает большинство разработчиков Windows. Вместо того чтобы позволить задавать размеры в физических пикселях, в WPF применяются независимые от устройства единицы, которые могут масштабироваться для заполнения разных разрешений монитора, и определены как 1/96 часть дюйма. Это значит, что окно размером 300x300 единиц из предыдущего примера будет визуализировано в виде окна 300x300 пикселей, если системная установка DPI составляет стандартные 96 dpi. Однако в системах с более высоким значением системного DPI будет использовано больше пикселей. Подробности были даны в главе 1. Пространства имен XAML Ясно, что не достаточно просто указать имя класса. Анализатору XAML также нужно знать пространство имен .NET, где находится этот класс. Например, класс Window может существовать в нескольких пространствах имен — он может ссылаться на класс System.Windows.Window, на класс в компоненте от независимого разработчика или же на класс, определенный в вашем приложении. Чтобы определить, какой именно класс нужен на самом деле, анализатор XAML проверяет пространство имен XML, к которому относится элемент. Вот как это работает. В примере документа, показанном ранее, определено два пространства имен: 2 xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" На заметку! Пространства имен объявляются с помощью атрибутов. Эти атрибуты могут помещаться внутрь начального дескриптора любого элемента. Однако согласно принятым соглашениям все пространства имен, которые нужно использовать в документе, должны быть объявлены в самом первом дескрипторе, как это сделано в данном примере Как только пространство имен объявлено, оно может использоваться в любом месте документа. xmlns — это специализированный атрибут в мире XML, который зарезервирован для объявления пространств имен. В показанном выше фрагменте кода разметки объявлены два пространства имен, которые будут присутствовать в каждом создаваемом документе WPF XAML. • http://schemas.microsoft.com/winfx/2006/xaml/presentation — основное пространство имен WFP. Оно охватывает все классы WPF, включая элементы управления, которые применяются для построения пользовательских интерфейсов. В рассматриваемом примере это пространство имен объявлено без префикса
52 Глава 2. XAML пространства имен, поэтому становится пространством имен по умолчанию для всего документа. Другими словами, каждый элемент автоматически помещается в это пространство имен, если только не указано иное. • http://schemas.microsoft.com/winfx/2006/xaml — пространство имен XAML. Оно включает различные служебные свойства XAML, которые позволяют влиять на то, как интерпретируется документ. Данное пространство имен отображается на префикс х. Это значит, что его можно применять, помещая префикс пространства имен перед именем элемента (как в <х:ИмяЭлемента>). Как видите, пространство имен XML не соответствует какому-либо конкретному пространству имен .NET. Существует несколько причин, по которым создатели XML выбрали такое проектное решение. По существующему соглашению пространства имен XML часто имеют форму URI (как и в данном примере). Эти URI выглядят так, будто указывают на некоторое место в Интернете, хотя на самом деле это не так. Формат URI используется потому, что он делает маловероятным ситуацию, когда разные организации непреднамеренно создадут разные языки на базе XML с одинаковым пространством имен. Поскольку домен schemas.microsoft.com принадлежит Microsoft, только Microsoft использует его в названии пространства имен XML. Другая причина отсутствия отображения "один к одному" между пространствами имен XML, используемыми в XAML, и пространствами имен .NET заключается в том, что это могло бы значительно усложнить документы XAML. Проблема в том, что WPF содержит свыше десятка пространств имен (все начинаются с System.Windows). Если бы каждое пространство имен .NET отображалось на отдельное пространство имен XML, пришлось бы указывать корректное пространство имен для каждого используемого элемента управления, что быстро привело бы к путанице. Вместо этого создатели WPF предпочли скомбинировать все эти пространства имен .NET в единое пространство имен XML. Это работает, потому что внутри разных пространств имен .NET, образующих часть WPF, нет классов с одинаковыми именами. Информация пространства имен позволяет анализатору XAML находить правильный класс. Например, когда он просматривает элементы Window и Grid, то видит, что они помещены в пространство имен WPF по умолчанию. Затем он ищет соответствующие пространства имен .NET — до тех пор, пока не находит System.Windows.Window и System.Windows.Controls.Grid. Класс отделенного кода Язык XAML позволяет конструировать пользовательский интерфейс, но для создания функционирующего приложения необходим способ подключения обработчиков событий. XAML позволяет легко это сделать с помощью атрибута Class, показанного ниже: 1 <Window x:Class="WindowsApplicationl.Windowl" Префикс пространства имен х помещает атрибут Class в пространство имен XAML, что означает более общую часть языка XAML. Фактически атрибут Class сообщает анализатору XAML, чтобы он сгенерировал новый класс с указанным именем. Этот класс наследуется от класса, именованного элементом XML. Другими словами, этот пример создает новый класс по имени Windowl, который наследуется от базового класса Window. Класс Windowl генерируется автоматически во время компиляции. И здесь начинается самое интересное. Вы можете предоставить часть класса Windowl, которая будет объединена с автоматически сгенерированной частью этого класса. Указанная вами часть — блестящий контейнер для кода обработки событий.
Глава 2. XAML 53 На заметку! Эта "магия" возможна благодаря средству С#, известному под названием частичные классы (partial class). Частичные классы позволяют разделить класс на две или более отдельных части во время разработки, которые соединяются вместе в скомпилированной сборке. Частичные классы могут применяться во многих сценариях управления кодом, но более всего удобны, когда код должен объединяться с файлом, сгенерированным визуальным конструктором. Среда Visual Studio оказывает помощь, автоматически создавая частичный класс, куда можно поместить код обработки событий. Например, при создании приложения по имени WindowsApplicationl, содержащего окно по имени Windowl (как в предыдущем примере), Visual Studio начнет с создания следующего базового каркаса класса: namespace WindowsApplicationl { /// <summary> /// Логика взаимодействия для Windowl.xaml /// </summary> public partial class Windowl : Window { public Windowl() { InitializeComponent() ; } } } Во время компиляции приложения код XAML, определяющий пользовательский интерфейс (такой как Windowl.xaml), транслируется в объявление типа CLR, объединенного с логикой файла класса отделенного кода (подобного Windowl.xaml.cs), формируя один общий модуль. Метод InitializeComponent () В данный момент класс Windowl не содержит реальной функциональности. Однако он включает одну важную деталь — конструктор по умолчанию, который вызывает метод InitializeComponent (), когда создается экземпляр класса. На заметку! Метод InitializeComponent () играет ключевую роль в приложениях WPF. По этой причине никогда не следует удалять вызов InitializeComponent () из конструктора окна. В случае добавления к классу окна другого конструктора обязательно предусмотрите в нем вызов InitializeComponent (). Метод InitializeComponentO не видим в исходном коде, потому что генерируется автоматически при компиляции приложения. По существу все, что делает InitializeComponentO — это вызов метода LoadComponent () класса System. Windows.Application. Метод LoadComponent() извлекает код BAML (скомпилированный XAML) из сборки и использует его для построения пользовательского интерфейса. При разборе BAML он создает объекты каждого элемента управления, устанавливает их свойства и присоединяет все обработчики событий. На заметку! Если вам не терпится, загляните в конец главы. В разделе "Код и скомпилированный XAML" приведен код автоматически сгенерированного метода InitializeComponentO.
54 Глава 2. XAML Именование элементов Есть еще одна деталь, которая должна приниматься во внимание. В классе отделенного кода часто требуется программно манипулировать элементами управления. Например, необходимо читать либо изменять свойства, а также присоединять или отсоединять обработчики событий на лету. Чтобы обеспечить такую возможность, элемент управления должен включать XAML-атрибут Name. В предыдущем примере элемент Grid не содержит атрибут Name, поэтому манипулировать им в отделенном коде не получится. Ниже показано, как назначить имя элементу Grid: 6 <Grid x:Name="gridl"> 7 </Grid> Можно внести это изменение в документ XAML вручную или выбрать элемент в визуальном конструкторе Visual Studio и установить свойство Name в окне Properties (Свойства). 8 обоих случаях атрибут Name сообщит анализатору XAML о необходимости добавить поле следующего вида к автоматически сгенерированной части класса Windowl: private System.Windows.Controls.Grid gridl; Теперь с этим элементом можно взаимодействовать в коде класса Windowl указывая имя gridl: MessageBox.Show(String.Format("The grid is {0}x{l} units in size.", gridl.ActualWidth, gridl.ActualHeight)); Такая техника мало, что дает простому примеру, но становится намного важнее, когда требуется читать значения из элементов управления вводом, таких как текстовые поля и списки. Показанное ранее свойство Name является частью языка XAML и используется для того, чтобы помочь в интеграции класса отделенного кода. Из-за того, что многие классы определяют собственное свойство Name, происходит некоторая путаница. (Примером может служить базовый класс FrameworkElement, от которого наследуются все элементы WPF.) Анализаторы XAML элегантно справляются с этой проблемой. Можно либо установить XAML-свойство Name (используя префикс х:), либо свойство Name, относящееся к действительному элементу (опустив префикс). В любом случае результат один и тот же — указанное имя используется в файле автоматически сгенерированного кода и применяется для установки свойства Name. Это значит, что следующая разметка эквивалентна тому, что вы уже видели: <Grid Name="gridl"> </Grid> Такой трюк работает только в том случае, если включающий свойство Name класс оснащен атрибутом RuntimeNameProperty. Атрибут RuntimeNameProperty указывает на то, какое свойство должно трактоваться в качестве имени экземпляра этого типа. (Очевидно, обычно таким свойством является Name.) Класс FrameworkElement содержит атрибут RuntimeNameProperty, так что никаких проблем нет. Совет. В традиционном приложении Windows Forms каждый элемент управления имеет имя. В приложении WPF такого требования нет. Однако при создании окна перетаскиванием элементов на поверхность визуального конструктора Visual Studio каждому элементу назначается автоматически сгенерированное имя. Таково соглашение. Если вы не собираетесь взаимодействовать с элементом в коде, то можете удалить атрибут Name из кода разметки. В примерах, предлагаемых в настоящей книге, имена элементов обычно опускаются, если они не нужны, что сокращает код разметки.
Глава 2. XAML 55 Теперь у вас должно сформироваться базовое понимание того, как интерпретировать документ XAML, определяющий окно, и как документ XAML преобразуется в конечный скомпилированный класс (с добавлением любого написанного вами кода). В следующем разделе рассматривается синтаксис свойств более подробно, и будет показано, как привязывать к элементам обработчики событий. Свойства и события в XAML До сих пор мы рассматривали не слишком впечатляющий пример — пустое окно, содержащее пустой элемент управления Grid. Прежде чем двигаться дальше, стоит представить более реалистичное окно, включающее несколько элементов управления. На рис. 2.1 показан пример с автоответчиком на вопросы пользователя. Рис. 2.1. Спросите автоответчик и все узнаете Окно автоответчика включает четыре элемента управления: Grid (чаще всего используемый для организации компоновки в WPF), два объекта Text Box и один Button. Разметка, которая необходима для компоновки и конфигурирования этих элементов управления, существенно длиннее, чем в предыдущих примерах. Ниже приведен сокращенный листинг, в котором некоторые детали заменены многоточиями, чтобы продемонстрировать общую структуру. <Window x:Class="EightBall.Windowl" xmlns="http://schemas.microsoft.com/winfx/2 00 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2 00 6/xaml" Title="Eight Ball Answer" Height=28" Width=12"> <Grid Name="gridl"> <Grid.Background> </Grid.Background> <Grid.RowDefinitions> </Grid.RowDefinitions> <TextBox Name="txtQuestion" ... > </TextBox> <Button Name="cmdAnswer" ... > </Button>
56 Глава 2. XAML <TextBox Name="txtAnswer" ... > </TextBox> </Grid> </Window> В следующем разделе мы рассмотрим части этого документа и по ходу дела изучим синтаксис XAML. На заметку! XAML не ограничен классами, входящими в WPR Код XAML можно применять для создания экземпляра любого класса, который подчиняется нескольким основным правилам. Об использовании собственных классов в XAML речь пойдет далее в этой главе. Простые свойства и конвертеры типов Как вы уже видели, атрибуты элемента устанавливают свойства соответствующего объекта. Например, текстовые поля в примере автоответчика конфигурируют выравнивание, поля и шрифт: <TextBox Name="txtQuestion" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" FontFamily="Verdana" FontSize=4" Foreground="Green" ... > Чтобы это заработало, класс System.Windows.Controls.TextBox должен предоставить следующие свойства: VerticalAlignment, HorizontalAlignment, FontFamily, FontSize и Foreground. В последующих главах вы ознакомитесь со специфическим значением этих свойств. Чтобы заставить эту систему работать, анализатор XAML должен выполнить больше работы, чем может показаться на первый взгляд. Значение в атрибуте XAML всегда представлено простой строкой. Однако свойства объекта могут быть любого типа .NET. В предыдущем примере было два свойства, использующих перечисления (VerticalAlignment и HorizontalAlignment), одна строка (FontFamily), одно целое число (FontSize) и один объект Brush (Foreground). Чтобы преодолеть зазор между строковыми значениями и не строковыми свойствами, анализатор XAML должен выполнить преобразование. Это преобразование осуществляется конвертерами типов — базовой частью инфраструктуры .NET, которая существует еще со времен .NET 1.0. По сути, конвертер типов играет только одну роль — он предоставляет служебные методы, которые могут преобразовывать определенный тип данных .NET в любой другой тип .NET, такой как строку в данном случае. При поиске нужного конвертера типа анализатор XAML предпринимает следующие два действия. 1. Проверяет объявление свойства в поисках атрибута TypeConverter. (Если атрибут TypeConverter присутствует, он указывает класс, выполняющий преобразование.) Например, когда используется такое свойство, как Foreground, .NET проверяет объявление свойства Foreground. 2. Если в объявлении свойства отсутствует атрибут TypeConverter, то анализатор XAML проверяет объявление класса соответствующего типа данных. Например, свойство Foreground использует объект Brush. Класс Brush (и его наследники) используют BrushConverter, потому что класс Brush оснащен объявлением атрибута TypeConverter (typeof (Br us hConverter)). Если в объявлении свойства или объявлении класса не оказывается ассоциированного конвертера типа, то анализатор XAML генерирует ошибку.
Глава 2. XAML 57 Эта система проста и гибка. Если вы устанавливаете конвертер типа на уровне класса, то этот конвертер применяется к каждому свойству, использующему этот класс. С другой стороны, если вы хотите обеспечить тонкую настройку работы конвертера типа для конкретного свойства, то вместо этого можете применять атрибут TypeConverter объявления свойства. Формально возможно использовать конвертеры типов в коде, но синтаксис при этом несколько мудреный. Почти всегда лучше непосредственно установить свойство — это не только быстрее, но также позволяет избежать потенциальных ошибок от опечаток в строках, которые не проявляются до момента выполнения. (Эта проблема не затрагивает XAML, поскольку разметка XAML анализируется и проверяется во время компиляции.) Конечно, прежде чем можно будет устанавливать свойства в элементе WPF, необходимо узнать немного больше о базовых свойствах WPF и типах данных. Именно этому и посвящены несколько последующих глав. На заметку! XAML, как и все языки на базе XML, является чувствительным к регистру символов. Это значит, что нельзя подставлять <button> вместо <Button>. Однако конвертеры типов обычно не чувствительны к регистру, а потому Foreground="White" и Foreground="white" дают одинаковый результат. Сложные свойства Как бы ни были удобны конвертеры типов, они подходят не для всех сценариев. Например, некоторые свойства являются полноценными объектами с собственными наборами свойств. Хотя можно создать строковое представление, которое будет использовать конвертер типа, этот синтаксис может оказаться трудным в применении, к тому же он подвержен ошибкам. К счастью, XAML предусматривает другой выбор: синтаксис "свойство-элемент*'. С помощью этого синтаксиса можно добавлять дочерний элемент с именем в форме РодительскийЭлемент.ИмяСвойства. Например, у Grid имеется свойство Background, которое позволяет указывать кисть, используемую для рисования области, находящейся под элементами управления. Чтобы применить сложную кисть — более совершенную, чем сплошное заполнение цветом, — понадобится добавить дочерний дескриптор по имени Grid.Background, как показано ниже: <Grid Name="gridl"> <Grid.Background> </Grid.Васkground> </Grid> Ключевая деталь, которая заставляет это работать — точка (.) в имени элемента. Это отличает свойства от других типов и вложенного содержимого. Однако еще один вопрос остается: как установить сложное свойство после его идентификации? Трюк заключается в следующем. Внутрь вложенного элемента можно добавить другой дескриптор, чтобы создать экземпляр определенного класса. В примере с автоответчиком (см. рис. 2.1) для фона применяется градиентная заливка. Чтобы определить нужный градиент, понадобится создать объект LinearGradientBrush. Согласно правилам XAML, можно создать объект LinearGradientBrush, используя элемент по имени LinearGradientBrush: <Grid Name="gridl"> <Grid.Background> <LinearGradientBrush>
58 Глава 2. XAML </LinearGradientBrush> </Grid.Васkground> </Grid> LinearGradientBrush является частью набора пространств имен WPF, так что для дескрипторов можно применять пространство имен XML по умолчанию. Однако просто создать LinearGradientBrush недостаточно; нужно также указать цвета градиента. Это делается заполнением свойства LinearGradientBrush. GradientStops коллекцией объектов GradientStop. Опять-таки, свойство GradientStops слишком сложное, чтобы его можно было установить только одним значением атрибута. Вместо этого следует положиться на синтаксис "свойство-элемент": <Grid Name="gridl"> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Васkground> </Grid> И, наконец, можно заполнить коллекцию GradientStops серией объектов GradientStop. Каждый объект GradientStop имеет свойства Offset и Color. Указать эти два значения можно с помощью обычного синтаксиса "свойство-элемент": <Grid Name="gridl"> <Gnd. Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset=.00" Color="Red" /> <GradientStop Offset=.50" Color="Indigo" /> <GradientStop Offset="l.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Васkground> </Grid> На заметку! Синтаксис "свойство-элемент" можно использовать для любого свойства. Однако если свойство имеет подходящий конвертер типа, обычно будет применяться более простой подход "свойство-атрибут". Это дает более компактный код. Любой набор дескрипторов XAML может быть заменен набором операторов кода, решающих ту же задачу. Показанные ранее дескрипторы, которые заполняют фон необходимым градиентом, эквивалентны следующему коду: LinearGradientBrush brush = new LinearGradientBrush(); GradientStop gradientStopl = new GradientStop(); gradientStopl.Offset = 0; gradientStopl.Color = Colors.Red; brush.GradientStops.Add(gradientStopl); GradientStop gradientStop2 = new GradientStop (); gradientStop2.Offset = 0.5; gradientStop2.Color = Colors.Indigo; brush.GradientStops.Add(gradientStop2);
Глава 2. XAML 59 GradientStop gradientStop3 = new GradientStop(); gradientStop3.Offset = 1; gradientStop3.Color = Colors.Violet; brush.GradientStops.Add(gradientStop3); gridl.Background = brush; Расширения разметки Для большинства свойств синтаксис свойств XAML работает исключительно хорошо. Но в некоторых случаях просто невозможно жестко закодировать значение свойства. Например, значение свойства должно быть установлено в уже существующий объект. Или может понадобиться установить значение свойства динамически, привязывая его к свойству в другом элементе управления. В обоих таких случаях необходимо использовать расширение разметки (markup extension) — специализированный синтаксис, устанавливающий свойство нестандартным образом. Расширения разметки могут применяться во вложенных дескрипторах или атрибутах XML, что встречается чаще. Когда они используются в атрибутах, то всегда окружаются фигурными скобками {}. Например, ниже показано, как можно использовать расширение разметки StaticExtension, позволяющее ссылаться на статическое свойство другого класса: <Button ... Foreground="{x:Static SystemColors.ActiveCaptionBrush}" > Расширения разметки используют синтаксис {КлассРасширенияРазметки Аргумент}. В этом случае расширением разметки служит класс StaticExtension. (По соглашению при ссылке на класс расширения последнее слово Extension можете опустить.) Префикс х указывает на то, что StaticExtension находится в одном из пространств имен XAML. Также вы встретите расширения разметки, являющиеся частью пространств имен WPF, но не имеющие префикса х. Все расширения разметки реализованы классами, производными от System. Windows .Markup.MarkupExtension. Базовый класс MarkupExtension чрезвычайно прост — он включает единственный метод ProvideValue (), получающий необходимое значение. Другими словами, когда анализатор XAML встречает предыдущий оператор, он создает экземпляр класса StaticExtension (передавая строку "SystemColors.ActiveCaptionBrush" в качестве аргумента конструктора), а затем вызывает ProvideValue(), чтобы получить объект, возвращенный статическим свойством SystemColors.ActiveCaption.Brush. Свойство Foreground кнопки cmdAnswer затем устанавливается равным извлеченному объекту. Конечный результат этого фрагмента XAML-разметки эквивалентен следующему коду: cmdAnswer.Foreground = SystemColors.ActiveCaptionBrush; Поскольку расширения разметки отображаются на классы, они могут также применяться в виде вложенных свойств, как уже известно из предыдущего раздела. Например, StaticExtension можно использовать со свойством Button.Foreground следующим образом: <Button ... > <Button.Foreground> <х:Static Member="SystemColors.ActiveCaptionBrush"></x:Static> </Button.Foreground> </Button> В зависимости от сложности расширения разметки и количества свойств, которые требуется установить, иногда синтаксис будет проще.
60 Глава 2. XAML Как и большинство расширений разметки, расширение StaticExtension должно вычисляться во время выполнения, потому что только тогда можно будет определить текущие системные цвета. Некоторые расширения разметки могут определяться во время компиляции. К ним относятся NullExtension (представляющее значение null) и TypeExtension (конструирующее объект, который представляет тип .NET). На протяжении этой книги будет приведено множество примеров расширений разметки в действии, в частности, когда пойдет речь о ресурсах и привязке данных. Присоединенные свойства Наряду с обычными свойствами XAML также включает концепцию присоединенных свойств (attached property) — свойств, которые могут применяться к нескольким элементам управления, но определены в другом классе. В WPF присоединенные свойства часто используются для управления компоновкой. Рассмотрим, как это работает. Каждый элемент управления обладает собственным набором внутренних свойств. (Например, текстовое поле имеет специфический шрифт, цвет текста и текстовое содержимое — все это определено свойствами FontFamily, Foreground и Text.) После помещения внутрь контейнера элемент управления получает дополнительные свойства, которые зависят от типа контейнера. (Например, если текстовое поле помещается внутрь экранной сетки, то нужно каким-то образом указать ячейку для помещения.) Эти дополнительные детали устанавливаются с использованием присоединенных свойств. Присоединенные свойства всегда имеют имя, состоящее из двух частей, в форме ОпределяемыйТип.ИмяСвойства. Этот синтаксис позволяет анализатору XAML отличать нормальные свойства от присоединенных. В примере с автоответчиком присоединенные свойства позволяют индивидуальным элементам управления размещать себя в разных строках (невидимой) сетки. <TextBox ... Grid.Row="> [Place question here.] </TextBox> <Button ... Grid.Row="> Ask the Eight Ball </Button> <TextBox ... Grid.Row="> [Answer will appear here.] </TextBox> Присоединенные свойства в действительности вообще свойствами не являются. На самом деле они транслируются в вызовы методов. Анализатор XAML вызывает статический метод, имеющий форму ОпределяемыйТип.SetИмяСвойства(). Например, в предыдущем фрагменте XAML определяемым типом является класс Grid, а свойством — Row, поэтому анализатор вызывает метод Grid.SetRow(). При вызове метода $^ИмяСвойства{) анализатор передает два параметра: модифицируемый объект и указанное значение свойства. Например, в случае установки свойства Grid.Row на элементе управления TextBox анализатор XAML выполняет следующий код: Gnd.SetRow(txtQuestion, 0); Этот шаблон (с вызовом статического метода определенного типа) удобен тем, что скрывает то, что происходит на самом деле. На первый взгляд этот код выглядит так, будто номер строки сохраняется в объекте Grid. Однако номер строки в действительности сохраняется в объекте, которого он касается, в данном случае — TextBox.
Глава 2. XAML 61 Этот фокус удается потому, что Text Box унаследован от базового класса Dependency Object, как и все элементы управления WPF. И, как вы узнаете в главе 4, класс Dependency Object предназначен для хранения практически неограниченной коллекции свойств зависимости. (Присоединенные свойства, о которых говорилось ранее — это специальный тип свойства зависимости.) Фактически метод Grid.SetRow() — это на самом деле сокращение, эквивалентное вызову метода DependencyObject.SetValue(): txtQuestion.SetValue(Grid.RowProperty, 0) ; Присоединенные свойства — центральный ингредиент WPF. Они действуют как система расширения общего назначения. Например, определяя свойство Row как присоединенное, вы гарантируете его применимость с любым элементом управления. Другой вариант — сделать его частью базового класса, такого как FrameworkElement, однако это усложнит жизнь. Это не только засорит общедоступный интерфейс свойствами, которые понадобятся только при определенных условиях (в данном случае — когда элемент используется внутри Grid), но также сделает невозможным добавление новых типов контейнеров, которые потребуют новых свойств. На заметку! Присоединенные свойства очень похожи на поставщики расширений в приложении Windows Forms. И те, и другие позволяют добавлять "виртуальные" свойства для расширения другого класса. Отличие в том, что вы должны создавать экземпляр поставщика расширений перед тем, как сможете использовать его, и значение расширенного свойства сохраняется в поставщике расширений, а не в расширяемом элементе управления. Механизм присоединенных свойств — наилучший выбор для WPF, потому что позволяет избежать проблем управления жизненным циклом (например, решая, когда следует уничтожать поставщика расширений). Вложенные элементы Как уже было показано, документы XAML представляют собой дерево элементов с высокой степенью вложенности. В рассмотренном примере элемент Window содержит элемент Grid, который, в свою очередь, содержит элементы TextBox и Button. XAML позволяет каждому элементу решать, как ему следует поступать с вложенными элементами. Это взаимодействие осуществляется при посредничестве одного из трех механизмов, запускаемых в описанном ниже порядке. • Если родительский элемент реализует интерфейс IList, анализатор вызывает ILi s t. Add (), передавая ему дочерний элемент. • Если родительский элемент реализует интерфейс IDictionary, анализатор вызывает I Dictionary. Add () и передает ему дочерний элемент. При использовании коллекции-словаря понадобится также устанавливать атрибут х:Кеу, чтобы назначить ключевое имя каждому элементу. • Если родительский элемент оснащен атрибутом ContentProperty, анализатор использует дочерний элемент, чтобы установить это свойство. Например, ранее в этой главе уже было показано, что LinearGradientBrush может содержать коллекцию объектов GradientStop, используя синтаксис вроде следующего: <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset=.00" Color=MRed" /> <GradientStop Offset=.50" Color=MIndigo" /> <GradientStop Of f set="l. 00" Color=,,Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush>
62 Глава 2. XAML Анализатор XAML распознает элемент LinearGradientBrush.GradientStops как сложное свойство, потому что оно включает точку. Однако ему нужно обработать внутренние дескрипторы (три элемента Gradient Stop) слегка по-другому. В этом случае анализатор распознает, что свойство GradientStops возвращает объект GradientStopCollection, a GradientStopCollection реализует интерфейс IList. Поэтому он предполагает (грубо), что каждый GradientStop должен быть добавлен к коллекции с помощью метода IList.AddO: GradientStop gradientStopl = new GradientStop(); gradientStopl.Offset = 0; gradientStopl.Color = Colors.Red; IList list = brush.GradientStops; list.Add(gradientStopl); Некоторые свойства могут поддерживать более одного типа коллекций. В этом случае вы должны добавить дескриптор, указывающий класс коллекции: <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Of f set= . 00" Color=,,Red" /> <GradientStop Offset=.50" Color="Indigo" /> <GradientStop Offset="l.00" Color="Violet" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> На заметку! Если по умолчанию коллекция установлена в null, понадобится включить дескриптор, указывающий класс коллекции, что обеспечит создание объекта коллекции. Если имеется экземпляр коллекции по умолчанию, который нужно просто заполнить, эту часть можно опустить Вложенное содержимое не всегда обозначает коллекцию. Например, рассмотрим элемент Grid, который содержит несколько других элементов управления: <Grid Name="gridl"> <TextBox Name="txtQuestion" . . . > </TextBox> <Button Name="cmdAnswer" . . . > </Button> <TextBox Name="txtAnswer" . . . > </TextBox> </Grid> Эти вложенные дескрипторы не соответствуют сложным свойствам, поскольку не включают точки. Более того, элемент Grid не является коллекцией и потому не реализует IList или IDictionary. Что Grid действительно поддерживает— так это атрибут ContentProperty, указывающий свойство, которое должно принимать любое вложенное содержимое. Технически атрибут ContentProperty применяется к классу Panel, от которого унаследован Grid, и выглядит он так: [ContentPropertyAttribute("Children") ] public abstract class Panel Это указывает на то, что для установки свойства Children должны применяться любые вложенные элементы. Анализатор XAML трактует свойство содержимого по-раз-
Глава 2. XAML 63 ному в зависимости от того, является ли оно свойством-коллекцией (и в этом случае реализует интерфейс IList или IDictionary). Так как свойство Panel.Children возвращает UIElementCollection, и поскольку UIElementCollection реализует IList, анализатор использует метод IList. Add О для добавления вложенного содержимого в сетку. Другими словами, когда анализатор XAML встречает приведенную выше разметку, он создает экземпляр каждого элемента и передает его Grid с помощью метода Grid. Children. Add(): txtQuestion = new TextBoxO ; gridl.Children.Add(txtQuestion); cmdAnswer = new Button (); gridl.Children.Add(cmdAnswer); txtAnswer = new TextBoxO ; gridl.Children.Add(txtAnswer); Что происходит дальше, полностью зависит от того, как элемент управления реализует свойство содержимого. Grid отображает все включенные в него элементы управления в невидимой компоновке строк и колонок, как будет показано в главе 3. Атрибут ContentProperty часто применяется в WPF. Он используется не только для контейнерных элементов управления (вроде Grid) и элементов, содержащих коллекцию визуальных элементов (таких как ListBox и TreeView), но также и для элементов управления, хранящих одиночное содержимое. Например, TextBox и Button способны содержать только один элемент или фрагмент текста, но они оба используют свойство содержимого для работы с вложенным содержимым: <TextBox Name="txtQuestion" ... > [Place question here.] </TextBox> <Button Name="cmdAnswer" ... > Ask the Eight Ball </Button> <TextBox Name="txtAnswer" . . . > [Answer will appear here.] </TextBox> В классе TextBox применяется атрибут ContentProperty для пометки свойства TextBox.Text. В классе Button использует атрибут ContentProperty для пометки свойства Button.Content. Анализатор XAML применяет указанный текст для установки этих свойств. Свойство TextBox.Text принимает только строковые значения. Однако Button. Content более интересно. Как будет показано в главе 6, свойство Content принимает любой элемент. Например, вот как выглядит кнопка, содержащая объект-фигуру: <Button Name="cmdAnswer" ... > <Rectangle Fill=,,Blue" Height=0" Width=,,100" /> </Button> Поскольку свойства Text и Content не используют коллекций, включать более одной части содержимого нельзя. Например, если вы попытаетесь вложить несколько элементов внутрь Button, то анализатор XAML сгенерирует исключение. Анализатор также сгенерирует исключение, если вы примените нетекстовое содержимое (такое как элемент Rectangle).
64 Глава 2. XAML На заметку! Как правило, все элементы управления, унаследованные от ContentControl, допускают единственный вложенный элемент Все элементы, унаследованные от itemsControl, позволяют применять коллекцию элементов, отображаемых на элемент управления некоторого типа (например, окно списка или дерево). Все элементы, унаследованные от Panel, являются контейнерами, используемыми для организации групп элементов управления Базовые классы ContentControl, ItemsControl и Panel используют атрибут ContentProperty. Специальные символы и пробелы Язык XAML ограничен правилами XML. Например, в XML особое внимание уделяется нескольким специальным символам вроде &, < и >. Если вы попытаетесь применить эти значения для установки содержимого элемента, то столкнетесь с проблемой, поскольку анализатор XAML предположит, что вы пытаетесь сделать что-то другое — например, создать вложенный элемент. Предположим, что требуется создать кнопку, которая содержит текст <Click Me>. Следующий код разметки работать не будет: <Button ... > <Click Me> </Button> Проблема в том, что код выглядит так, будто вы пытаетесь создать элемент по имени Click с атрибутом Me. Решение состоит в замене проблемных символов сущностными ссылками — специфическими кодами, которые анализатор XAML интерпретирует правильно. В табл. 2.1 перечислены символьные сущности, которые можно применять. Обратите внимание, что символьная сущность типа кавычки требуется только при установке значений с использованием атрибута, т.к. кавычка обозначает начало и конец значения атрибута. Таблица 2.1. Символьные сущности XML Специальный символ Символьная сущность Меньше (<) &lt; Больше (>) &gt; Амперсанд(&) &атр; Кавычка (") &quot; Ниже приведен правильный код разметки, в котором применяются соответствующие символьные сущности: <Button ... > &lt;Click Me&gt/ </Button> Когда анализатор XAML читает это, он правильно понимает, что вы хотите добавить текст <Click Me>, и передает строку с этим содержимым, дополняя ее угловыми скобками, свойству Button.Content. На заметку! Это ограничение — деталь XAML, которая не возникнет, если свойство Button. Content будет устанавливаться в коде. Конечно, в С# существует собственный специальный символ (обратный слэш), который должен быть защищен в строковых литералах по аналогичной причине.
Глава 2. XAML 65 Специальные символы — не единственная тонкость, с которой придется столкнуться в XAML. Другой проблемой является обработка пробелов. По умолчанию XML сокращает все пробелы, а это значит, что длинная строка пробелов, знаков табуляции и жестких переводов строки превращается в единственный пробел. Более того, пробел перед или после содержимого элемента будет полностью проигнорирован. Это можно наблюдать в примере EightBall. Текст на кнопке и два текстовых поля отделены от дескрипторов XAML жестким переводом строки и табуляцией, которые повышают читабельность разметки. Однако этот дополнительный пробел не появляется в пользовательском интерфейсе. Иногда это не то, что требуется. Например, в тексте кнопки могут понадобиться серии из нескольких пробелов. В таком случае в элементе должен использоваться атрибут xml:space="preserve". Атрибут xmlrspace — часть стандарта XML, являющаяся установкой в духе "все или ничего". Однажды включив его, вы предохраните все пробелы внутри элемента. Например, рассмотрим следующую разметку: <TextBox Name="txtQuestion" xml:space="preserve" ...> [There is a lot of space inside these quotation marks " " . ] </TextBox> В этом примере текст в текстовом поле будет включать жесткий перенос строки и знак табуляции перед текстом. Также он содержит серии пробелов внутри текста и перенос строки после текста. Если нужно только предохранить пробелы внутри, то придется применить менее читабельную разметку: <TextBox Name="txtQuestion" xml:space="preserve" ... >[There is a lot of space inside these quotation marks " ".]</TextBox> Трюк здесь состоит в том, что нужно убедиться в том, чтобы не было пробелов между открывающим символом > и содержимым или между содержимым и закрывающим символом <. Опять-таки, эта проблема касается только разметки XAML. При программной установке текста в текстовом поле будут использоваться все включенные пробелы. События До сих пор все атрибуты, которые вы видели, отображались на свойства. Однако атрибуты также могут быть использованы для присоединения обработчиков событий. Синтаксис при этом выглядит следующим образом: ИмяСобытия="ИмяМетодаОбработчикаСобытий" Например, элемент управления Button предоставляет событие Click. Присоединить обработчик событий можно так, как показано ниже: <Button . . . Click="cmdAnswer_Click"> Это предполагает наличие метода по имени cmdAnswer _ Click в классе отделенного кода. Обработчик событий должен иметь правильную сигнатуру (т.е. должен соответствовать делегату для события Click). Вот метод, который выполняет этот трюк: private void cmdAnswer_Click(object sender, RoutedEventArgs e) { this.Cursor = Cursors.Wait; // Значительная задержка... System.Threading.Thread.Sleep(TimeSpan.FromSeconds C));
66 Глава2.ХАМ1_ AnswerGenerator generator = new AnswerGenerator (); txtAnswer.Text = generator.GetRandomAnswer(txtQuestion.Text) ; this.Cursor = null; } Как вы могли заметить по сигнатуре этого обработчика событий, модель событий в WPF отличается от ранних версий .NET. Здесь поддерживается новая модель, которая полагается на маршрутизацию событий. Подробнее об этом речь пойдет в главе 5. Во многих ситуациях атрибуты используются для установки свойств и присоединения обработчиков событий в одном и том же элементе. WPF всегда работает в следующей последовательность: сначала устанавливается свойство Name (если оно есть), затем присоединяются любые обработчики событий и, наконец, устанавливаются свойства. Это значит, что любые обработчики событий, реагирующие на изменения свойств, будут запущены при первоначальной установке свойства. На заметку! Код (такой как обработчик события) допускается встраивать непосредственно в документ XAML, используя для этого элемент Code. Однако такая техника не является рекомендованной и не имеет какого-либо практического применения bWPF Этот подход не поддерживается в Visual Studio и книге не рассматривается При добавлении атрибута обработчика событий Visual Studio ассистирует с помощью средства IntelliSense. Как только введен символ равенства (например, после набора Click= в элементе <Button>), отображается раскрывающийся список со всеми подходящими обработчиками событий в классе отделенного кода (рис. 2.2). Если нужно создать новый обработчик для данного события, следует выбрать элемент <New Event Handler> (Новый обработчик событий) в начале списка. В качестве альтернативы можно присоединить и создать обработчики событий, используя вкладку Events (События) окна Properties (Свойства). Window 1 wMiiirl <- X П Ц Design /^ В XAML I ШВ® ф <TextBox VerticalAlignmentaB,,Stretch" HorizontalAlign»ent-,,Str—\ TextWrapping""Wrap" FontFamily"Verdana" FontSize- Grid^ow-^O" > [Place question here.] </TextBox> <Button VerticalAlignntent»wTop" HorizontalAlignment-"Left" Margin-0,0,0,20" Width—127" Height-,r23" Name-"cmdAf clic*"H Grii .af <New Event Handter> Азк the E: </Button> <TextBox Vcrtf J^^sEretch" HorizontalAligrunent*wStr TextWrapping""Wrapw ^ReadOnly'True* FontFamily«wVe Grid.Row-,'> (Answer will appear here.] </TextBox> Button (cmdAnswer) Window/Gnd/Button .—• ■' ■ ■ ■ — -——--—-—- _ Рис. 2.2. Присоединение события с помощью средства IntelliSense в Visual Studio Полный пример автоответчика После ознакомления с основами XAML теперь можно пройтись по определению окна, показанного на рис. 2.1. Ниже приведена полная разметка XAML.
Глава 2. XAML 67 <Window x:Class="EightBall.Windowl" xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Eight Ball Answer" Height=28" Width=12" > <Grid Name="gridl"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefmition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBox VerticalAlignment="Stretch" HonzontalAlignment="Stretch" Margin=0,10,13,10" Name="txtQuestion" TextWrapping="Wrap" FontFamily="Verdana" FontSize=4" Grid.Row="> [Place question here.] </TextBox> <Button VerticalAlignment="Top" HorizontalAlignment="Left" Margin=0,0,0,20" Width=27" Height=3" Name="cmdAnswer" Click="cmdAnswer_Click" Grid.Row="l"> Ask the Eight Ball </Button> <TextBox VerticalAlignment="Stretch" HonzontalAlignment="Stretch" Margin=0,10,13,10" Name="txtAnswer" TextWrapping="Wrap" IsReadOnly="True" FontFamily="Verdana" FontSize=4" Foreground="Green" Gnd.Row="> [Answer will appear here.] </TextBox> <Gnd.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset=.00" Color="Red" /> <GradientStop Offset=.50" Color="Indigo" /> <GradientStop Offset="l.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Васkground> </Gnd> </Window> Как упоминалось ранее, вряд ли вы захотите вручную вводить XAML-разметку для всего пользовательского интерфейса — это может оказаться невыносимо утомительно. Тем не менее, могут возникнуть веские причины для редактирования кода XAML с целью внесения изменений, которые трудно обеспечить в визуальном конструкторе. Кроме того, может понадобиться просмотреть код XAML, чтобы получить лучшее представление о работе окна. Использование типов из других пространств имен До сих пор демонстрировалось создание базового интерфейса в XAML с использованием классов, являющихся частью WPF. Однако XAML задуман как средство общего назначения для создания экземпляров объектов .NET, включая те, что находятся в пространствах имен, не относящихся к WPF, и те, которые вы создаете сами. Рассмотрение создания объектов, которые не предназначены для экранного отображения в окне XAML, может показаться странным, но существует немало ситуаций, когда это оправдано. Примером может служить случай, когда вы используете привязку данных и хотите нарисовать информацию из другого объекта, чтобы отобразить ее в
68 Глава 2. XAML элементе управления. Другой пример — когда требуется установить свойство объекта WPF с применением объекта, не относящегося к WPF. Например, WPF-элемент List Box можно заполнить объектами данных. List Box будет вызывать метод ToStringO, чтобы получить текст для отображения каждого элемента в списке. (Для более качественного отображения списка можно создать шаблон данных, который извлечет множество фрагментов информации и сформатирует их соответствующим образом. Эта техника рассматривается в главе 20.) Для того чтобы использовать класс, который не определен ни в одном из пространств имен WPF, понадобится отобразить пространство имен .NET на пространство имен XML. В XAML для этого предусмотрен специальный синтаксис: xmlns :Префикс="clr-namespace: ПространствоИмен; as зетЫу=ИмяСборки" Обычно это отображение пространства имен помещается в корневой элемент документа XAML — сразу после атрибутов, которые описывают пространства имен WPF и XAML. Выделенные курсивом части должны быть заполнены соответствующей информацией, как описано ниже. • Префикс — префикс XML, который будет использоваться для указания пространства имен в разметке XAML. Например, язык XAML использует префикс х. • ПространствоИмен — полностью квалифицированное название пространства имен .NET. • ИмяСборки — сборка, в которой объявлен тип, без расширения .dll. Проект должен ссылаться на эту сборку. Если используется сборка вашего проекта, этот параметр можно опустить. Например, ниже показано, как получить доступ к базовым типам пространства имен System и отобразить их на префикс sys: xmlns:sys="clr-namespace:System;assembly=mscorlib" А вот так можно получить доступ к типам, объявленным в пространстве имен MyProject текущего проекта, и отобразить их на префикс local: xmlns:local="clr-namespace:MyNamespace" Теперь для создания экземпляра класса в одном из этих пространств имен применяется префикс пространства имен: <local:MyObject ...></local:MyOb]ect> Совет. Помните, что можно использовать произвольный префикс пространства имен до тех пор, пока он согласован по всему документу XAML Однако префиксы sys и local обычно применяются при импорте пространства имен System и пространства имен для текущего проекта. Они используются на протяжении всей книги. В идеале каждый класс, который должен использоваться в XAML, будет иметь конструктор без аргументов. Если это так, то анализатор XAML сможет создавать соответствующий объект, устанавливать его свойства и присоединять любые обработчики событий, которые вы укажете. XAML не поддерживает параметризованных конструкторов, и все элементы в WPF включают конструкторы без аргументов. Вдобавок необходимо иметь возможность устанавливать все желаемые детали, используя общедоступные свойства. XAML не позволяет устанавливать общедоступные свойства или вызывать методы. Если класс, который планируется использовать, не имеет конструктора без аргументов, возможности некоторым образом ограничены. Если вы попытаетесь создать простой примитив (вроде строки, даты или числового типа), то сможете применить стро-
Глава 2. XAML 69 новое представление данных в качестве содержимого внутри дескриптора. Анализатор XAML затем использует конвертер типа для преобразования этой строки в соответствующий объект Ниже показан пример структуры DateTime: <sys:DateTime>10/30/2010 4:30 PM</sys:DateTime> Это работает, потому что класс DataTime применяет атрибут TypeConverter для привязки себя к DateTimeConverter. Конвертер DateTimeConverter распознает эту строку как корректный объект DateTime и преобразует его. При использовании этой техники нельзя применять атрибуты для установки любых свойств объекта. Если требуется создать класс, не имеющий конструктора без аргументов, и нет подходящего конвертера типов, которым можно было бы воспользоваться, значит, вам не повезло. На заметку! Некоторые разработчики преодолевают эти ограничения, создавая специальные классы-оболочки. Например, класс FileStream не включает конструктора без аргументов. Однако можно создать класс-оболочку, который его имеет. Этот класс-оболочка должен будет создать нужный объект FileStream в своем конструкторе, извлечь необходимую информацию и затем закрыть FileStream. Подобное решение не идеально, поскольку предполагает жесткое кодирование информации для конструктора класса, что усложняет обработку исключений. В большинстве случаев лучше будет манипулировать объектом с помощью небольшого кода обработки событий и полностью исключить его из XAML-разметки. В следующем примере все это собирается вместе. В нем префикс sys отображается на пространство имен System, после чего это пространство имен используется для создания объектов DateTime, которые применяются для заполнения списка. <Window x:Class="WindowsApplicationl.Windowl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" Width=00" Height=00" > <ListBox> <ListBoxItem> <sys:DateTime>10/13/2010 4:30 PM</sys:DateTime> </ListBoxItem> <ListBoxItem> <sys:DateTime>10/29/2010 12:30 PM</sys:DateTime> </ListBoxItem> <ListBoxItem> <sys:DateTime>10/30/2010 2:30 PM</sys:DateTime> </ListBoxItem> </ListBox> </Window> Загрузка и компиляция XAML Как вам уже известно, XAML и WPF — это две разные, хотя и взаимодополняющие технологии. В результате вполне возможно создать приложение WPF, в котором не используется XAML. В общем случае существуют три разных стиля кодирования, которые могут применяться при создании приложения WPF.
70 Глава 2. XAML • Только код. Это традиционный подход, используемый в Visual Studio для приложений Windows Forms. Пользовательский интерфейс в нем генерируется операторами кода. • Код и не компилированная разметка (XAML). Это специализированный подход, который имеет смысл в определенных сценариях, когда нужны исключительно динамичные пользовательские интерфейсы. При этом часть пользовательского интерфейса загружается из файла XAML во время выполнения с помощью класса XamlReader из пространства имен System.Windows.Markup. • Код и компилированная разметка (BAML). Это предпочтительный подход для WPF, поддерживаемый в Visual Studio. Для каждого окна создается шаблон XAML, и этот код XAML компилируется в BAML, после чего встраивается в конечную сборку. Во время выполнения скомпилированный BAML извлекается и используется для регенерации пользовательского интерфейса. В последующих разделах эти три модели рассматриваются более подробно. Только код Разработка на основе только кода — наименее распространенный (но полностью поддерживаемый) путь написания приложений WPF без применения какого-либо XAML- кода. Очевидным недостатком разработки на основе только кода является то, что этот вариант потенциально чрезвычайно утомителен. Элементы управления WPF не включают параметризованных конструкторов, поэтому даже добавление простой кнопки в окно требует нескольких строк кода. Одним потенциальным преимуществом разработки на основе только кода является ограниченное пространство для настройки. Например, можно сгенерировать форму, заполненную элементами управления вводом, на основе информации из записи базы данных, или же можно на основе какого-то условия принять решение, добавлять или подставлять элементы управления в зависимости от текущего пользователя. Все, что для этого потребуется — это логика проверки условия и ветвление. И напротив, когда используемые документы XAML встраиваются в сборку как фиксированные, неизменные ресурсы. На заметку! Несмотря на то что вы вряд ли будете создавать WPF-приложения на основе только кода, вы, скорее всего, воспользуетесь этим подходом для создания элемента управления WPF в некоторой точке, когда нужна адаптируемая порция пользовательского интерфейса. Ниже представлен код для скромного окна с единственной кнопкой и обработчиком событий (рис. 2.3). Когда создается окно, конструктор вызывает метод InitializeComponent (), который создает и конфигурирует кнопку и форму, а также присоединяет к ним обработчик событий. На заметку! Чтобы создать этот пример, вы должны кодировать класс Windowl с нуля (щелкните правой кнопкой мыши в Solution Explorer и выберите в контекстном меню пункт Add^Class (Добавить^Класс)). Выбирать пункт Add ^Window (Добавить^Окно) не следует, поскольку это добавит файл кода и шаблон XAML для окна, укомплектованный сгенерированным методом InitializeComponent(). using System.Windows; using System.Windows.Controls; using System.Windows.Markup; public class Windowl : Window { private Button buttonl;
Глава 2. XAML 71 public Windowl() { InitializeComponent (); } private void InitializeComponent () { // Сконфигурировать форму. this.Width = this.Height = 285; this.Left = this.Top = 100; this.Title = "Code-Only Window"; // Создать контейнер, содержащий кнопку. DockPanel panel = new DockPanelO ; // Создать кнопку. buttonl = new Button (); buttonl.Content = "Please click me."; buttonl.Margin = new Thickness C0); // Присоединить обработчик событий, buttonl.Click += buttonl_Click; // Поместить кнопку в панель. IAddChild container = panel; container.AddChild (buttonl) ; // Поместить панель в форму. container = this; . container.AddChild(panel); } private void buttonl_Click(object sender, RoutedEventArgs e) { buttonl.Content = "Thank you."; } } Концептуально класс Windowl в этом примере сильно напоминает форму из традиционного приложения Windows Forms. Он наследуется от базового класса Window и добавляет приватную переменную-член для каждого элемента управления. Для ясности этот класс выполняет свою работу по инициализации в выделенном методе InitializeComponent(). i Code-Only Window *£Ж Please click me. Рис. 2.3. Окно с одной кнопкой
72 Глава 2. XAML Для запуска этого приложения можете воспользоваться следующим методом Main(): public class Program : Application { [STAThreadO ] static void Main() { Program app = new Program(); app.MainWindow = new Windowl(); app.MainWindow.ShowDialog(); } } Код и не компилированный XAML Одним из наиболее интересных способов использования XAML является разбор его на лету с помощью XamlReader. Например, предположим, что вы начинаете со следующего содержимого в файле Windowl.xaml: <DockPanel xmlns="http://schemas.microsoft.com/winfx/200 6/xaml/presentation"> <Button Name="buttonl11 Margin=ll30">Please click me.</Button> </DockPanel> Во время выполнения можно загрузить это содержимое в активное окно для создания того же окна, что и на рис. 2.3. Ниже показан код, который делает это. using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.10; public class Windowl : Window { private Button buttonl; public Windowl () { InitializeComponent (); } public Windowl(string xamlFile) { // Сконфигурировать формы, this.Width = this.Height = 285; this.Left = this.Top = 100; this.Title = "Dynamically Loaded XAML"; // Получить содержимое XAML из внешнего файла. DependencyObject rootElement; using (FileStream fs = new FileStream(xamlFile, FileMode.Open)) { rootElement = (DependencyObject)XamlReader.Load (fs); } // Вставить разметку в это окно, this.Content = rootElement; // Найти элемент управления с соответствующим именем. buttonl = (Button)LogicalTreeHelper.FindLogicalNode (rootElement, "buttonl"); // Присоединить обработчик событий, buttonl.Click += buttonl_Click; } private void buttonl_Click(object sender, RoutedEventArgs e) { buttonl.Content = "Thank you."; }
Глава 2. XAML 73 Здесь конструктор получает имя файла XAML в качестве аргумента (в данном случае — Windowl.xaml). Он открывает FileStream и использует метод Load() класса XamlReader для преобразования содержимого этого файла в DependencyObject, являющийся базой, от которой наследуются элементы управления WPF. Этот объект DependencyObject может быть помещен внутрь контейнера любого типа (например, Panel), но в данном примере он используется как содержимое для всего окна. На заметку! В этом примере из файла XAML загружается элемент — объект DockPanel. В качестве альтернативы можно было бы загрузить все окно XAML (как в примере с автоответчиком). В данном случае объект, возвращенный XamlReader.Load(), потребуется привести к типу Window и затем вызвать его метод Show() или ShowDialogO для того, чтобы отобразить его. Эта техника используется в примере XAML 2009, приведенном ниже в этой главе. Чтобы манипулировать элементом, например, кнопкой в файле Windowl.xaml, необходимо найти соответствующий объект — элемент управления в динамически загруженном содержимом. Для этих целей предназначен элемент LogicalTreeHelper. Он позволяет производить поиск по всему дереву объектов — элементов управления, погружаясь на столько уровней, на сколько необходимо, пока не будет найден объект с указанным именем. Обработчик затем присоединяется к событию Button.Click. Другая альтернатива предполагает использование метода FrameworkElement. FindName(). В данном примере корневым элементом является объект DockPanel. Подобно всем элементам управления в окне WPF, объект DockPanel наследуется от FrameworkElement. Это значит, что следующий код: buttonl = (Button)LogicalTreeHelper.FindLogicalNode (rootElement, "buttonl"); может быть заменен эквивалентным кодом: FrameworkElement frameworkElement = (FrameworkElement)rootElement; buttonl = (Button)frameworkElement.FindName("buttonl"); В этом примере файл Windowl.xaml распространяется вместе с исполняемым файлом приложения, в той же папке. Однако, несмотря на то, что он не компилируется как часть приложения, его можно добавить в проект Visual Studio. Это упростит управление данным файлом и позволит проектировать пользовательский интерфейс с помощью Visual Studio (предполагается использование расширения файла .xaml, которое Visual Studio распознает как документ XAML). Если вы используете такой подход, удостоверьтесь, что этот файл XAML не компилируется и не встраивается в проект подобно традиционному файлу XAML. После добавления его к проекту выберите этот файл в Solution Explorer и с помощью окна Properties установите свойство Build Action (Действие сборки) в None, a Copy to Output Directory (Копировать в выходной каталог) — в Copy always (Копировать всегда). Очевидно, что динамическая загрузка XAML не будет столь же эффективной, как компиляция XAML в BAML с последующей загрузкой BAML во время выполнения, особенно в случае сложного пользовательского интерфейса. Тем не менее, он открывает ряд возможностей для построения динамических пользовательских интерфейсов. Например, можно было бы создать оператор общего назначения для опроса, который читает файл формы из веб-службы и затем отображает соответствующие элементы управления (метки, текстовые поля, флажки и т.п.). Файл формы может быть обычным документом XAML с дескрипторами WPF, который загружается в существующую форму с помощью XamlReader. Чтобы собрать результаты, как только форма опроса заполнена, понадобится просто перечислить все элементы управления вводом и собрать их содержимое. Другое преимущество подхода со несвязанными файлами XAML в готовом проекте состоит в том, что они позволяют использовать улучшения в стандарте XAML 2009, описанные далее в этой главе.
74 Глава 2. XAML Код и скомпилированный XAML Вы уже видели наиболее распространенный способ использования XAML в примере автоответчика, показанном на рис. 2.1 и исследуемом на протяжении всей этой главы. Это метод, применяемый Visual Studio, который обладает рядом преимуществ, уже затронутых ранее в главе. • Часть работы автоматизирована. Нет необходимости выполнять поиск идентификатора с помощью LogicalTreeHelper или привязывать в коде обработчики событий. • Чтение BAML-кода во время выполнения происходит быстрее, чем чтение XAML- кода. • Упрощается развертывание. Поскольку BAML-код встраивается в сборку как один или более ресурсов, его невозможно потерять. • Файлы XAML могут редактироваться в других программах, таких как инструменты графического дизайна. Это открывает возможность лучшей кооперации между программистами и дизайнерами. (Вы также получаете это преимущество, когда используете не компилированный XAML, как описано в предыдущем разделе.) В Visual Studio используется двухэтапный процесс компиляции приложений WPF. Первый этап — компиляция XAML-файлов в BAML. Например, если проект включает файл по имени Windowl.xaml, то компилятор создаст временный файл Windowl.baml и поместит его в подпапку obj\Debug (в папке проекта). В то же время для окна создается частичный класс с использованием выбранного языка. Например, если применяется С#, то компилятор создаст файл по имени Windowl.g.cs в папке obj\Debug. Здесь g означает generated (сгенерированный). Частичный класс включает следующие вещи. • Поля для всех элементов управления в окне. • Код, загружающий BAML из сборки и тем самым создающий дерево объектов. Это случается, когда конструктор вызывает InitializeComponent(). • Код, который назначает соответствующий объект элемента управления каждому полю и подключает все обработчики событий. Это происходит в методе по имени Connect (), который вызывается анализатором BAML при каждом нахождении именованного объекта. Частичный класс не включает кода для создания экземпляра и инициализации элементов управления, потому что эта задача выполняется механизмом WPF, когда BAML- код обрабатывается методом Application.LoadComponent(). На заметку! В процессе компиляции компилятор XAML должен создать частичный класс. Это возможно, только если используемый вами язык поддерживает модель .NET Code DOM. Языки С# и VB поддерживают Code DOM, но если используется язык от независимого поставщика, следует убедиться, что эта поддержка доступна, прежде чем создавать приложения со скомпилированным XAML. Ниже приведен слегка сокращенный файл Windowl.g.cs из примера автоответчика, показанного на рис. 2.1. public partial class Windowl : System.Windows.Window, System.Windows.Markup.IComponentConnector { // Поля элементов управления. internal System.Windows.Controls.TextBox txtQuestion;
Глава 2. XAML 75 internal System.Windows.Controls.Button cmdAnswer; internal System.Windows.Controls.TextBox txtAnswer; private bool _contentLoaded; // Загрузить BAML. public void InitializeComponent () { if (_contentLoaded) { return; } _contentLoaded = true; System.Uri resourceLocater = new System. Un ( "windowl .baml" , System.UriKind.RelativeOrAbsolute); System.Windows.Application.LoadComponent (this, resourceLocater) ; } // Подключить каждый элемент управления, void System.Windows.Markup.IComponentConnector.Connect (int connectionId, object target) { switch (connectionld) { case 1: txtQuestion = ((System.Windows.Controls.TextBox) (target)); return; case 2: cmdAnswer = ((System.Windows.Controls.Button)(target)); cmdAnswer.Click += new System.Windows.RoutedEventHandler ( cmdAnswer_Click); return; case 3: txtAnswer = ((System.Windows.Controls.TextBox) (target)); return; } this._contentLoaded = true; } } Когда завершается этап компиляции XAML в BAML, Visual Studio использует компилятор соответствующего языка, чтобы скомпилировать код и сгенерированные файлы частичного класса. В случае приложения С# эту задачу решает компилятор csc.exe. Скомпилированный код становится единой сборкой (EightBall.exe), и BAML для каждого окна встраивается как отдельный ресурс. Только XAML В предыдущих разделах было показано, как использовать XAML из приложения на основе кода. Разработчики для .NET будут заниматься этим большую часть времени. Однако также возможно использовать файл XAML без создания кода. Это называется несвязанный XAML-файл. Несвязанные файлы XAML могут открываться непосредственно в Internet Explorer. (Предполагается, что платформа .NET Framework установлена.) На заметку! Если файл XAML использует код, он не может быть открыт в браузере Internet Explorer. Однако можно построить браузерное приложение под названием XBAR в котором это ограничение преодолено. За подробным описанием обращайтесь к главе 24. К этому моменту создание несвязанного XAML-файла может показаться относительно бесполезным. В конце концов, какой смысл в пользовательском интерфейсе без управляющего им кода? Однако, изучая XAML, вы откроете несколько средств, которые
76 Глава 2. XAML полностью декларативны. К ним относится анимация, триггеры, привязка данных и ссылки (которые могут указывать на другие несвязанные файлы XAML). Используя эти средства, можно строить очень простые XAML-файлы без кода. Они не будут выглядеть как завершенные приложения, но позволят делать несколько больше, чем статические страницы HTML. Чтобы попробовать несвязанную страницу XAML, внесите в файл .xaml следующие изменения. • Удалите атрибут Class из корневого элемента. • Удалите любые атрибуты, которые присоединяют обработчики событий (такие как атрибут Button.Click). • Измените имя открывающего и закрывающего дескриптора с Window на Page. Браузер Internet Explorer может отображать только страницы, а не отдельные окна. После этого дважды щелкните на файле .xaml, чтобы загрузить Internet Explorer. На рис. 2.4 показана преобразованная страница EightBall.xaml, которая входит в загружаемый код примеров для этой главы. В текстовом поле можно набирать текст, но поскольку в приложении отсутствует файл с отделенным кодом, щелчок на кнопке ни к чему не приводит. Чтобы создать более полезное браузерное приложение, которое может содержать код, необходимо применять подход, описанный в главе 24. 4$ D:\Code\Pro WPF\Chapter02\EightBallBrowserPage\Windowl xaml - Windows Internet ЕхркХ.№Ив|ВН1 tm& ГТ-^! 1 * D:\Code\ProWPF\Cnapter02\EightBallBrowserPat»' 4 Xi] Google fi •• n D:\Code\Pro WPF\Chapter02\EightB... ' D ' W " ': * p^e w ..Tools*- Рис. 2.4. Страница XAML в браузере XAML 2009 Как упоминалось ранее в этой главе, в WPF 4 появился новый стандарт под названием XAML 2009. Однако пока он не внедрен повсеместно. Чтобы получить преимущества XAML 2009 в настоящее время, необходимо использовать несвязанные, не скомпилированные файлы XAML, что не удовлетворяет большинство разработчиков.
Глава 2. XAML 77 Даже если вы решите не использовать XAML 2009, стоит кратко познакомиться с его средствами. Дело в том, что в конечном итоге в следующей версии WPF язык XAML 2009 превратится в полностью интегрированный скомпилированный стандарт. В следующем разделе вы ознакомитесь с его наиболее важными изменениями, и все они будут проиллюстрированы примерами кода. Имейте в виду, что средство IntelliSense в Visual Studio пометит некоторые из них как ошибки времени проектирования, потому что пока производится проверка кода на предмет соответствия существующему стандарту XAML. Однако во время выполнения они будут работать должным образом. Автоматическая привязка событий В показанном ранее примере несвязанного XAML в коде требовалось вручную подключать обработчики событий, а это означает, что код должен обладать детальными сведениями о содержимом файла XAML (таком как имена всех элементов, инициирующих события, которые необходимо обрабатывать). Стандарт XAML 2009 предлагает частичное решение проблемы. Его анализатор может автоматически подключать обработчики событий, если соответствующие методы-обработчики определены в корневом классе. Например, рассмотрим следующую разметку: <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel> <Button Click="cmd_Click,,></Button> </StackPanel> </Window> Если передать этот документ методу XamlReader.LoadO, возникнет ошибка, потому что метод Window.cmd_Click() не существует. Но если создать собственный специальный класс, унаследованый от Window, скажем, XAML 2009Window, и применить показанную ниже разметку: <local:XAML 2009Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:NonCompiledXam1;assembly=NonCompiledXaml"> <StackPanel> <Button Click="cmd_Click"x/Button> </StackPanel> </local:XAML 2009Window> то анализатор сможет создать экземпляр класса XAML 2009Window и затем присоединит к событию Button.Click метод XAML 2009Window.cmd_Click() автоматически. Эта техника отлично работает с приватными методами, но если метод не существует (или не имеет правильной сигнатуры), генерируется исключение. Вместо загрузки XAML в конструкторе (как в предыдущем примере) класс XAML 2009Window использует собственный статический метод по имени LoadWindowFromXaml (). Такое проектное решение несколько лучше, потому что подчеркивает потребность в нетривиальном процессе, который необходим для создания объекта окна — в данном случае это открытие файла. При этом также можно организовать более ясную обработку исключений, если код не найдет или не получит доступа к файлу XAML. Причина в том, что генерировать исключение имеет больший смысл в методе, чем в конструкторе. Ниже показан полный код окна:
78 Глава 2. XAML public class XAML 2009Window : Window { public static XAML 2009Window LoadWindowFromXaml (string xamlFile) { // Получить содержимое XAML из внешнего файла. using (FileStream fs = new FileStream(xamlFile, FileMode.Open)) { XAML 2009Window window = (XAML 2009Window)XamlReader.Load(fs); return window; } } private void cmd_Click(object sender, RoutedEventArgs e) { MessageBox.Show("You clicked."); } } Для создания экземпляра этого окна необходимо вызвать статический метод LoadWindowFromXaml () в любом месте кода: Program арр = new Program(); app.MainWindow = XAML 2009Window.LoadWindowFromXaml("XAML 2009.xaml"); app.MainWindow.ShowDialog (); Возможно, вы уже заметили, что эта модель довольно похожа на встроенную модель Visual Studio, которая компилирует XAML. В обоих случаях весь код обработки событий помещается в специальный класс, унаследованный от элемента, который действительно нужен (обычно Window или Page). Ссылки В обычном языке XAML не существует простого способа ссылки одного элемента на другой. Лучшее решение состоит в привязке данных (как будет описано в главе 8), но для простых сценариев это слишком громоздко. В XAML 2009 задача упрощается за счет расширения разметки, которое специально предназначено для ссылок. В следующих двух фрагментах разметки показаны две ссылки, используемые для установки свойства Target объектов Label. Свойство Label.Target указывает на элемент управления вводом, который принимает фокус, когда пользователь нажимает горячую клавишу. В данном примере первое текстовое поле использует комбинацию <Alt+F>. Если пользователь нажимает эту комбинацию клавиш, фокус переходит к элементу управления txtFirstName, определенному следом. <Label Target="{x:Reference txtFirstName}">_FirstName</Label> <TextBox x:Name="txtFirstName"></TextBox> <Label Target="{x:Reference txtLastName}">_LastName</Label> <TextBox x :Name="txtLastName"x/TextBox> Встроенные типы Как уже известно, разметка XAML может обращаться почти к любому пространству имен, если сначала отобразить его на пространство имен XML. Многие новички в WPF удивляются, когда узнают о необходимости ручного отображения пространств имен для использования базовых типов из пространства System, таких как String, DateTime, TimeSpan, Boolean, Char, Decimal, Single, Double, Int32, Uri, Byte и т.д. Хотя это относительно небольшое препятствие, все же оно требует дополнительного шага с вашей стороны и привносит дополнительную путаницу: <sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib>A String</sys:String>
Глава 2. XAML 79 В XAML 2009 пространство имен XAML обеспечивает прямой доступ к этим типам данных, не требуя никаких излишних усилий: <x:String>A String</x:String> Можно также напрямую обращаться к обобщенным типам коллекций List и Dictionary. На заметку! При установке свойств элементов управления WPF данная проблема не возникает. Это объясняется тем, что конвертер значений берет строку и преобразует ее в соответствующий тип данных автоматически, как объяснялось ранее в этой главе. Однако бывают ситуации, когда конвертеры значений не работают, и нужны специфические типы данных. Одним из примеров может быть необходимость в применении простых типов данных для хранения ресурсов — объектов, которые можно многократно использовать в разметке и коде. Ресурсы рассматриваются в главе 10. Расширенное создание объектов Обычный язык XAML позволяет создавать почти любой тип — при условии, что у него имеется конструктор без аргументов. В XAML 2009 это ограничение снято и предлагаются два более мощных способа создания и инициализации объектов. Первый из них — возможность использования элемента <x:Arguments> для передачи аргументов конструктору. Например, предположим, что есть класс, не имеющий конструктора без аргументов: public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } } Создать его экземпляр в XAML 2009 можно следующим образом: <local:Person> <х:Arguments> <х:String>Joe</x:String> <х:String>McDowell</x:String> </х:Arguments> </local:Person> Второй подход предусматривает применение статического метода (того же либо другого класса), который создает экземпляр необходимого объекта. Этот шаблон называется фабричным методом. Пример фабричного метода содержит класс Guid из пространства имен System, представляющий глобально уникальный идентификатор. Создать экземпляр Guid с помощью ключевого слова new нельзя, но можно вызвать метод Guid.NewGuidO, который вернет новый экземпляр: Guid myGuid = Guid.NewGuidO ; В XAML 2009 аналогичный прием можно использовать в разметке. Секрет кроется в атрибуте x:FactoryMethod. Ниже показано, как создать разметку Guid, исходя из предположения, что префикс пространства имен sys отображен на пространство имен System: <sys:Guid x:FactoryMethod="Guid.NewGuid"></sys:Guid>
80 Глава 2. XAML XAML 2009 также позволяет создавать экземпляры обобщенных коллекций, что в обычном XAML невозможно. (Хотя существует обходной путь — унаследовать специальный класс коллекции для использования в качестве оболочки и создать его экземпляр в XAML. Однако это быстро засорит код излишними одноразовыми классами.) В XAML 2009 атрибут TypeArguments предоставляет возможность передачи аргументов обобщенному классу. Например, предположим, что требуется создать список объектов Person. Это может обеспечить примерно такой код: List<Person> people = new List<Person> () ; people.Add (new Person("Joe", "McDowell"); В XAML 2009 аналогичный результат дает следующая разметка: <x:List x:TypeArguments="Person"> <local:Person> <x:Arguments> <x:String>Joe</x:String> <x:String>McDowell</x:String> </x:Arguments> </local:Person> </x:List> Если в классе Person имеется конструктор по умолчанию без аргументов, то разметка может выглядеть так: <x:List х:TypeArguments="Person"> <local:Person FirstName="Joe" LastName="McDowell" /> </x:List> Резюме В этой главе вы ознакомились с содержимым простого файла XAML и необходимым синтаксисом. • Были рассмотрены ключевые ингредиенты XAML, такие как конвертеры типов, расширения разметки и присоединенные свойства. • Было показано, как привязывать класс отделенного кода, который может обрабатывать события, инициируемые элементами управления. • Был рассмотрен процесс компиляции, который превращает стандартное приложение WPF в скомпилированный исполняемый файл. Были описаны три варианта этого процесса: создание приложения WPF из одного только кода, создание приложения WPF без ничего кроме XAML и загрузка XAML вручную во время выполнения. • Были кратко описаны изменения, произошедшие в XAML 2009. Хотя детали разметки XAML особо глубоко не рассматривались, вы узнали достаточно, чтобы оценить его преимущества. Далее внимание переключается на саму технологию WPF, которая таит в себе некоторые интересные сюрпризы. В следующей главе будет показано, как организовать элементы управления в реалистичных окнах с использованием панелей компоновки WPF
ГЛАВА 3 Компоновка Половина всех усилий при проектировании пользовательского интерфейса уходит на организацию содержимого, чтобы она была привлекательной, практичной и гибкой. Но по-настоящему сложной задачей является адаптация компоновки элементов интерфейса к различным размерам окна. В WPF компоновка формируется с использованием разнообразных контейнеров. Каждый контейнер обладает собственной логикой компоновки — некоторые укладывают элементы в стопку, другие распределяют их по ячейкам сетки и т.д. Если вы программировали с помощью Windows Forms, то будете удивлены, что компоновку на основе координат в WPF использовать не рекомендуется. Вместо этого упор делается на создание более гибких компоновок, которые могут адаптироваться к изменяющемуся содержимому, разным языкам и широкому разнообразию размеров окон. Для большинства разработчиков переход на технологию WPF с ее новой системой компоновки становится большим сюрпризом — и первой реальной сложностью. В этой главе будет показано, как работает модель компоновки WPF и как использовать базовые контейнеры компоновки. Кроме того, будут продемонстрированы примеры распространенной компоновки — от базового диалогового окна к разделенному окну изменяемого размера. Что нового? В WPF 4 используется прежняя гибкая система компоновки, но с добавлением одного небольшого штриха, который может избавить от серьезной проблемы. Это средство называется округлением компоновки, и оно гарантирует, что контейнеры компоновки не будут пытаться размещать компоненты по дробным координатам, что приводит к размытым фигурам и изображениям. Более подробно это описано в разделе "Округление компоновки" настоящей главы. Понятие компоновки в WPF Модель компоновки WPF отражает существенные изменения подхода разработчиков к проектированию пользовательских интерфейсов Windows. Чтобы понять новую модель компоновки WPF, стоит посмотреть на то, что ей предшествовало. В .NET 1.0 технология Windows Forms предоставляла весьма примитивную систему компоновки. Элементы управления были фиксированы на месте по жестко закодированным координатам. Единственными удобствами были привязка (anchoring) и стыковка (docking) — два средства, которые позволяли элементам управления перемещаться и изменять свои размеры вместе с их контейнером. Привязка и стыковка были незаменимы для создания простых окон изменяемого размера, например, с привязкой кнопок ОК и Cancel (Отмена) к нижнему правому углу окна, либо когда нужно было заставить элемент TreeView разворачиваться для заполнения всей формы. Однако они не могли
82 Глава 3. Компоновка справиться с более сложными задачами компоновки. Например, привязка и стыковка не позволяли организовать пропорциональное изменение размеров двухпанельных окон (с равномерным разделением дополнительного пространства между двумя областями). Они также не слишком помогали в случае высоко динамичного содержимого, например, когда нужно было дать возможность метке расширяться, чтобы вместить больше текста, что приводило к перекрытию соседних элементов управления. В .NET 2.0 пробел в Windows Forms был восполнен, благодаря двум новым контейнерам компоновки: FlowLayoutPanel и TableLayoutPanel. С использованием этих элементов управления можно создавать более изощренные интерфейсы в стиле веб- приложений. Оба контейнера компоновки позволяли содержащимся в них элементам управления увеличиваться, расталкивая соседние элементы. Это облегчило задачу создания динамического содержимого, создания модульных интерфейсов и локализации приложений. Однако панели компоновки выглядели дополнением к основной системе компоновки Windows Forms, использовавшей фиксированные координаты. Панели компоновки были элегантным решением, но все-таки несколько чужеродным. В WPF появилась новая система компоновки, большое влияние на которую оказала разработка с помощью Windows Forms. Эта система возвращается к модели .NET 2.0 (координатная компоновка с необязательными потоковыми панелями компоновки), сделав потоковую (flow-based) компоновку стандартной и предложив лишь рудиментарную поддержку координатной компоновки. Преимущества подобного сдвига огромны. Разработчики могут теперь создавать не зависимые от разрешения и от размеров интерфейсы, которые масштабируются на разных мониторах, подгоняют себя при изменении содержимого и легко поддерживают перевод на другие языки. Однако прежде чем вы воспользуетесь преимуществом этих изменений, следует перестроить образ мышления относительно компоновки. Философия компоновки WPF Окно WPF может содержать только один элемент. Чтобы разместить более одного элемента и создать практичный пользовательский интерфейс, нужно поместить в окно контейнер и добавлять элементы в этот контейнер. На заметку! Это ограничение обусловлено тем фактом, что класс Window унаследован от ContentControl, который более подробно рассматривается в главе 6. В WPF компоновка определяется используемым контейнером. Хотя есть несколько контейнеров, среди которых можно выбирать, "идеальное" окно WPF следует описанным ниже ключевым принципам. • Элементы (такие как элементы управления) не должны иметь явно установленных размеров. Вместо этого они растут, чтобы уместить свое содержимое. Например, кнопка увеличивается при добавлении в нее текста. Можно ограничить элементы управления приемлемыми размерами, устанавливая максимальное и минимальное их значение. • Элементы не указывают свою позицию в экранных координатах. Вместо этого они упорядочиваются своим контейнером на основе размера, порядка и (необязательно) другой информации, специфичной для контейнера компоновки. Для добавления пробелов между элементами служит свойство Margin. На заметку! Жестко закодированные размеры позиции — зло, потому что они ограничивают возможности по локализации интерфейса и значительно затрудняют работу с динамическим содержимым.
Глава 3. Компоновка 83 • Контейнеры компоновки "разделяют" доступное пространство между своими дочерними элементами. Они пытаются обеспечить для каждого элемента его предпочтительный размер (на основе его содержимого), если только позволяет свободное пространство. Они могут также выделять дополнительное пространство одному или более дочерним элементам. • Контейнеры компоновки могут быть вложенными. Типичный пользовательский интерфейс начинается с Grid — наиболее развитого контейнера, и содержит другие контейнеры компоновки, которые организуют меньшие группы элементов, такие как текстовые поля с метками, элементы списка, значки в панели инструментов, колонка кнопок и т.д. Хотя из этих правил существуют исключения, они отражают общие цели проектирования WPF. Другими словами, если вы последуете этим руководствам при построении WPF-приложения, то получите лучший и более гибкий пользовательский интерфейс. Если же вы нарушаете эти правила, то получите пользовательский интерфейс, который не очень хорошо подходит для WPF и его будет значительно сложнее сопровождать. Процесс компоновки Компоновка WPF происходит в два этапа: этап измерения и этап расстановки. На этапе измерения контейнер выполняет проход в цикле по дочерним элементам и опрашивает их предпочтительные размеры. На этапе расстановки контейнер помещает дочерние элементы в соответствующие позиции. Разумеется, элемент не может всегда иметь свои предпочтительные размеры — иногда контейнер недостаточно велик, чтобы обеспечить это. В таком случае контейнер должен усекать такой элемент для того, чтобы уместить его в видимую область. Как вы убедитесь, этой ситуации часто можно избежать, устанавливая минимальный размер окна. На заметку! Контейнеры компоновки не поддерживают прокрутку. Вместо этого прокрутка обеспечивается специализированным элементом управления содержимым ScrollViewer, который может применяться почти где угодно Дополнительные сведения об этом элементе ищите в главе 6. Контейнеры компоновки Все контейнеры компоновки WPF являются панелями, которые унаследованы от абстрактного класса System.Windows. Controls.Panel (рис. 3.1). Класс Panel добавляет небольшой набор членов, включая три общедоступных свойства, описанные в табл. 3.1. На заметку! Класс Panel также имеет внутренний механизм, который можно использовать при создании собственного контейнера компоновки. Но важнее то, что можно переопределить методы MeasureOverrideO и ArrangeOverride(), унаследованные от FrameworkElement, для изменения способа обработки панелью этапов измерения и расстановки при организации дочерних элементов. Создание специальных панелей рассматривается в главе 18. DispatcherObject i DependencyObject Visual UlElement ~~r~ FrameworkElement I Panel Условные обозначения с Абстрактный Л класс J Конкретный класс Рис. 3.1. Иерархия класса Panel
84 Глава 3. Компоновка Таблица 3.1. Общедоступные свойства класса Panel Имя Описание Background Кисть, используемая для рисования фона панели. Чтобы принимать события мыши, это свойство должно быть установлено в отличное от null значение. (Если вы хотите принимать события мыши, но не желаете отображать сплошной фон, просто установите прозрачный цвет фона — Transparent.) Базовые кисти рассматриваются в главе 6, а более развитые кисти — в главе 12 Children Коллекция элементов, находящихся в панели. Это первый уровень элементов — другими словами, это элементы, которые сами могут содержать в себе другие элементы IsItemsHost Булевское значение, которое равно true, если панель используется для отображения элементов, ассоциированных с ItemsControl (вроде узлов в TreeView или элементов списка ListBox). Большую часть времени вы даже не будете знать о том, что элемент управления списком используется "за кулисами" панелью для управления компоновкой элементов. Однако эта деталь становится более важной, если вы хотите создать специальный список, который будет располагать свои дочерние элементы другим способом (например, ListBox, отображающий графические изображения). Данный прием применяется в главе 20 Сам по себе базовый класс Panel — это не что иное, как начальная точка для построения других более специализированных классов. WPF предлагает набор производных от Panel классов, которые можно использовать для организации компоновки. Наиболее основные из них перечислены в табл. 3.2. Как и все элементы управления WPF, а также большинство визуальных элементов, эти классы находятся в пространстве имен System.Windows.Controls. Таблица 3.2. Основные панели компоновки Имя Описание StackPanel Размещает элементы в горизонтальном или вертикальном стеке. Этот контейнер компоновки обычно используется в небольших разделах крупного и более сложного окна WrapPanel Размещает элементы в последовательностях строк с переносом. В горизонтальной ориентации WrapPanel располагает элементы в строке слева направо, затем переходит к следующей строке. В вертикальной ориентации WrapPanel располагает элементы сверху вниз, используя дополнительные колонки для дополнения оставшихся элементов DockPanel Выравнивает элементы по краю контейнера Grid Выстраивает элементы в строки и колонки невидимой таблицы. Это один из наиболее гибких и широко используемых контейнеров компоновки UniformGrid Помещает элементы в невидимую таблицу, устанавливая одинаковый размер для всех ячеек. Данный контейнер компоновки используется нечасто Canvas Позволяет элементам позиционироваться абсолютно — по фиксированным координатам. Этот контейнер компоновки более всего похож на традиционный компоновщик Windows Forms, но не предусматривает средств привязки и стыковки. В результате это неподходящий выбор для окон переменного размера, если только вы не собираетесь взвалить на свои плечи значительный объем работы
Глава 3. Компоновка 85 Наряду с этими центральными контейнерами существует еще несколько более специализированных панелей, которые встречаются во многих элементах управления. К ним относятся панели, предназначенные для хранения дочерних элементов определенного элемента управления, такого как TabPanel (вкладки в TabControl), ToolbarPanel (кнопки в Toolbar) и ToolbarOverflowPanel (команды в раскрывающемся меню Toolbar). Имеется еще VirtualizingStackPanel, список элементов управления с привязкой данных которого используется для существенного сокращения накладных расходов, а также InkCanvas, который подобен Canvas, но обладает поддержкой перьевого ввода на Tablet PC. (Например, в зависимости от выбранного режима, InkCanvas поддерживает рисование с указателем для выбора экранных элементов. Хотя это не очень удобно, но InkCanvas можно использовать и на обычном компьютере с мышью.) Элемент управления InkCanvas рассматривается в этой главе, a VirtualizingStackPanel — в главе 19. Другие специализированные панели будут описаны далее, когда речь пойдет о соответствующих элементах управления. Простая компоновка с помощью StackPanel Панель StackPanel — один из простейших контейнеров компоновки. Она просто укладывает свои дочерние элементы в одну строку или колонку. Например, рассмотрим следующее окно, которое содержит стек из четырех кнопок: <Window х:Class=MLayout.SimpleStack" xmlns=Mhttp://schemas.microsoft.com/winfx/2006/xaml/presentation11 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=MLayoutM Height=M223M Width=54" > <StackPanel> <Label>A Button Stack</Label> <Button>Button K/Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel> </Window> На рис. 3.2 показано полученное в результате окно. ■ т SimpleStack A Button Stack Button 1 Button 2 Button 3 Button 4 UalHI] Рис. 3.2. Панель StackPanel в действии
86 Глава 3. Компоновка Добавление контейнера компоновки в Visual Studio Этот пример сравнительно просто создается с использованием визуального конструктора Visual Studio. Начните с удаления корневого элемента Grid (если он есть). Затем перетащите в окно элемент stackPanel. После этого перетащите в окно другие элементы (метку и четыре кнопки), расположив их в желаемом порядке сверху вниз. Чтобы изменить порядок следования элементов в StackPanel, можно просто перетащить их в новые позиции. При создании пользовательского интерфейса в Visual Studio должны учитываться некоторые нюансы. Когда вы перетаскиваете элементы из панели инструментов в окно, Visual Studio добавляет ряд деталей в разметку. Среда Visual Studio автоматически назначает имя каждому новому элементу управления (что безвредно, но излишне). Также добавляются жестко закодированные значения Width и Height, а это ограничивает намного больше. Как уже говорилось ранее, явные размеры ограничивают гибкость пользовательского интерфейса. Во многих случаях лучше позволить элементам управления самостоятельно устанавливать свои размеры, подгоняя их к контейнеру. В данном примере фиксированные размеры вполне оправданы, т к для всех кнопок необходимо установить согласованную ширину. Однако более удачное решение состояло бы в том, чтобы позволить самой большой кнопке устанавливать свой размер самостоятельно, вмещая свое содержимое, а все остальные кнопки — растянуть до размеров большой, чтобы они соответствовали друг другу. (Такое решение, требующее применения Grid, описано далее в этой главе.) Но независимо от того, какой подход будет использоваться с кнопкой, почти наверняка понадобится избавиться от жестко закодированных величин Width и Height для StackPanel, чтобы она могла растягиваться и сжиматься, заполняя доступное пространство окна. По умолчанию панель StackPanel располагает элементы сверху вниз, устанавливая высоту каждого из них такой, которая необходима для отображения его содержимого. В данном примере это значит, что размер меток и кнопок устанавливается достаточно большим для спокойного размещения текста внутри них. Все элементы растягиваются на полную ширину StackPanel, которая равна ширине окна. Если вы расширите окно, StackPanel также расширится, и кнопки растянутся, чтобы заполнить ее. StackPanel может также использоваться для упорядочивания элементов в горизонтальном направлении за счет установки свойства Orientation: <StackPanel Orientation=llHorizontalll> Теперь элементы получают свою минимальную ширину (достаточную, чтобы уместить их текст) и растягиваются до полной высоты, чтобы заполнить содержащую их панель. В зависимости от текущего размера окна, это может привести к тому, что некоторые элементы не поместятся, как показано на рис. 3.3. г~ » • SimpleStack A Button Stack Button 1 l=>,ataJ| Button 2 Buttt } Рис. 3.3. Панель StackPanel с горизонтальной ориентацией
Глава 3. Компоновка 87 Ясно, что это не обеспечивает достаточной гибкости, необходимой реальному приложению. К счастью, с помощью свойств компоновки можно тонко настраивать способ работы StackPanel и других контейнеров компоновки. Свойства компоновки Хотя компоновка определяется контейнером, дочерние элементы тоже могут сказать свое слово. Панели компоновки взаимодействуют со своими дочерними элементами через небольшой набор свойств компоновки, перечисленных в табл. 3.3. Таблица 3.3. Свойства компоновки Наименование Описание HonzontalAlignment Определяет позиционирование дочернего элемента внутри контейнера компоновки, когда имеется дополнительное пространство по горизонтали. Доступные значения: Center, Left, Right или Stretch VerticalAlignment Определяет позиционирование дочернего элемента внутри контейнера компоновки, когда имеется дополнительное пространство по вертикали. Доступные значения: Center, Top, Bottom или Stretch Margin Добавляет некоторое пространство вокруг элемента. Свойство Margin — это экземпляр структуры System.Windows.Thickness, с отдельными компонентами для верхней, нижней, левой и правой граней MinWidth и Устанавливает минимальные размеры элемента. Если элемент слишком MinHeight велик, чтобы поместиться в его контейнер компоновки, он будет усечен MaxWidth и Устанавливает максимальные размеры элемента. Если контейнер MaxHeight имеет свободное пространство, элемент не будет увеличен сверх указанных пределов, даже если свойства HonzontalAlignment и VerticalAlignment установлены в Stretch Width и Height Явно устанавливают размеры элемента. Эта установка переопределяет значение Stretch для свойств HorizontalAlignment и VerticalAlignment. Однако данный размер не будет установлен, если выходит за пределы, заданные в MinWidth, MinHeight, MaxWidth и MaxHeight Все эти свойства унаследованы от базового класса FrameworkElement и потому поддерживаются всеми графическими элементами (виджетами), которые можно использовать в окне WPF. На заметку! Как известно из главы 2, различные контейнеры компоновки могут предоставлять присоединенные свойства своим дочерним элементам. Например, все дочерние элементы объекта Grid получают свойства Row и Column, позволяющие им выбирать ячейку, в которой они должны разместиться. Присоединенные свойства позволяют устанавливать информацию, специфичную для определенного контейнера компоновки. Однако свойства компоновки из табл. 3.3 носят достаточно общий характер, чтобы применяться ко многим панелям компоновки. Таким образом, эти свойства определены как часть базового класса FrameworkElement. Этот список свойств замечателен тем, чего он не содержит. Если вы ищете знакомые свойства позиционирования, вроде Top, Right и Location, вы не найдете их там. Причина в том, что большинство контейнеров компоновки (кроме Canvas) используют автоматическую компоновку и не позволяют явно позиционировать элементы.
88 Глава 3. Компоновка Выравнивание Чтобы понять, как работают эти свойства, еще раз взглянем на простую панель StackPanel, показанную на рис. 3.2. В этом примере — StackPanel с вертикальной ориентацией — свойство VerticalAlignment не имеет эффекта, потому что каждый элемент получает такую высоту, которая ему нужна, и не более. Однако свойство HorizontalAlignment является важным. Оно определяет место, где располагается каждый элемент в строке. Обычно HorizontalAlignment по умолчанию равно Left для меток и Stretch — для кнопок. Вот почему каждая кнопка целиком занимает ширину колонки. Эти детали можно изменять: <StackPanel> <Label HorizontalAlignment="Center">A Button Stack</Label> <Button HorizontalAlignment="Left">Button K/Button> <Button HorizontalAlignment="Right">Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel> На рис. 3.4 показан результат. Первые две кнопки получают минимальные размеры и соответствующим образом выровнены, в то время как две нижние кнопки растянуты на всю StackPanel. Изменив размер окна, вы увидите, что метка остается в середине, а первые две кнопки будут прижаты каждая к своей стороне. На заметку! StackPanel также имеет собственные свойства HorizontalAlignment и VerticalAlignment. По умолчанию оба они установлены в Stretch, и потому StackPanel заполняет свой контейнер полностью. В данном примере это значит, что StackPanel заполняет окно. Если используются другие установки, максимальный размер StackPanel будет определяться самым широким элементом управления, содержащимся в нем. Поля В текущей форме примера StackPanel присутствует очевидная проблема. Хорошо спроектированное окно должно содержать не только элементы; оно также содержит немного дополнительного пространства между элементами. Чтобы ввести это дополнительное пространство и сделать пример StackPanel менее сжатым, можно установить поля вокруг элементов управления. При установке полей допускается указание одинаковой ширины для всех сторон, как показано ниже: <Button Margin=M5">Button 3</Button> В качестве альтернативы можно установить разные поля для каждой стороны элемента управления в порядке левое, верхнее, правое, нижнее: <Button Margin=M5,10,5,10">Button 3</Button> В коде поля устанавливаются с применением структуры Thickness: cmd.Margin = new ThicknessE); Определение правильных полей вокруг элементов управления — отчасти искусство, потому что необходимо учитывать, каким образом установки полей для соседних элементов управления влияют друг на друга. Например, если есть две кнопки, уложенные одна на другую, и верхняя кнопка имеет нижнее поле размером 5, а нижняя кнопка — верхнее поле размером 5, то между двумя кнопками получится пространство в 10 единиц.
Глава 3. Компоновка 89 • ' SimpleStack ^ ... A Button Stack Button 2] Button 3 Button 4 • 3 SimpleStack :on 1 A Button Sta Button 3 Button 4 ^Lj^ggyi ck Butt: ... шшДХ I 1 Рис. З.4. Панель StackPanel с выровненными кнопками Рис. 3.5. Добавление полей между элементами В идеале можно сохранять разные установки полей насколько возможно согласованными и избегать разных значений для полей разных сторон. Например, в примере со StackPanel имеет смысл использовать одинаковые поля для кнопок и самой панели: <StackPanel Margin=M3M> <Label Margin=11 HorizontalAlignment=llCenter"> A Button Stack</Label> <Button Margin=M3M HorizontalAlignment=llLeft">Button K/Button> <Button Margin=M3M HorizontalAlignment="Rightll>Button 2</Button> <Button Margin=M3">Button 3</Button> <Button Margin=M3">Button 4</Button> </StackPanel> Таким образом, общее пространство между двумя кнопками (сумма полей двух кнопок) получается таким же, как общее пространство между кнопкой и краем окна (сумма поля кнопки и поля StackPanel). На рис. 3.5 показано наиболее приемлемое окно, а на рис. 3.6 — как его изменяют установки полей. StackPanel. Margin. Left Button 1. Margin.Top Button 1 Button 1. Margin. Bottom Button2.Margin.Top Button2 Button2. Margin. Right StackPanel.Margin.Right StackPanel. Margin. Bottom Рис. З.6. Комбинирование полей
90 Глава 3. Компоновка Минимальные, максимальные и явные размеры И, наконец, каждый элемент включает свойства Height и Width, которые позволяют установить явные размеры. Однако предпринимать такой шаг — не слишком хорошая идея. Вместо этого при необходимости используйте свойства минимальных и максимальных размеров, чтобы зафиксировать элемент управления в нужных пределах размеров. Совет. Подумайте дважды, прежде чем устанавливать явные размеры в WPF. В хорошо спроектированной компоновке подобная необходимость возникать не должна. Если вы добавляете информацию о размерах, то рискуете создать хрупкую компоновку, которая не сможет адаптироваться к изменениям (вроде разных языков и размеров окна) и будет усекать содержимое. Например, можно решить, что кнопки в панели StackPanel должны растягиваться для ее заполнения, но иметь ширину не более 200 и не менее 100 единиц. (По умолчанию кнопки начинаются с минимальной ширины в 75 единиц.) Ниже показана разметка, которая для этого понадобится: <StackPanel Margin=M3M> <Label Margin=M3M HorizontalAlignment="Center"> A Button Stack</Label> <Button Margin=M3M MaxWidth=,,200" MinWidth=00">Button K/Button> <Button Margin=M3M MaxWidth=M2 00M MinWidth=M100M>Button 2</Button> <Button Margin=M3" MaxWidth=M200M MinWidth=M100M>Button 3</Button> <Button Margin=M3M MaxWidth=M200M MinWidth=M100M>Button 4</Button> </StackPanel> Совет. Здесь может возникнуть вопрос: а нет ли более простого способа установки свойств, стандартизированных для нескольких элементов, наподобие полей кнопок в рассмотренном примере? Ответ: стили — средство, позволяющее повторно использовать установки свойств и даже применять их автоматически. Более подробно стили обсуждаются в главе 11. Когда панель StackPanel изменяет размеры кнопки, она принимает во внимание несколько единиц информации. • Минимальный размер. Каждая кнопка всегда будет не меньше минимального размера. • Максимальный размер. Каждая кнопка всегда будет меньше максимального размера (если только вы не установите неправильно максимальный размер меньше минимального). • Содержимое. Если содержимое внутри кнопки требует большей ширины, то StackPanel попытается увеличить кнопку. (Для определения размера, необходимого кнопке, можно проверить свойство DesiredSize, которое вернет минимальную ширину или ширину содержимого — в зависимости от того, что из них больше.) • Размер контейнера. Если минимальная ширина больше, чем ширина StackPanel, то часть кнопки будет усечена. В противном случае кнопке не позволено будет расти шире, чем позволит StackPanel, несмотря на то, что она не сможет вместить весь текст на своей поверхности. • Горизонтальное выравнивание. Поскольку кнопка использует значение HorizontalAlignment, равное Stretch (по умолчанию), панель StackPanel попытается увеличить кнопку, чтобы она заполнила всю ширину панели.
Глава 3. Компоновка 91 Сложность понимания этого процесса заключается в том, что минимальный и максимальный размеры устанавливают абсолютные пределы. Без этих пределов панель StackPanel пытается обеспечить желаемый размер кнопки (чтобы вместить ее содержимое) и настройки выравнивания. На рис. 3.7 в некоторой степени проясняется то, как это работает в StackPanel. Слева представлено окно в минимальном размере. Кнопки имеют размер по 100 единиц каждая, и окно не может быть сужено, чтобы сделать их меньше. Если вы попытаетесь сжать окно от этой точки, то правая часть каждой кнопки будет усечена. (Такую возможность можно предотвратить применением свойства MinWidth к самому окну, так что окно нельзя будет сузить меньше минимальной ширины.) При увеличении окна кнопки также растут, пока не достигнут своего максимума в 200 единиц. Если после этого продолжать увеличивать окно, то с каждой стороны от кнопок будет добавляться дополнительное пространство (как показано на рисунке справа). а A Button Stack Button 1 Button 3 Button 4 • J SimpleStack I о ,0 I^I^l \ 1 WW—i A Button Stack ton 1 Button 2 Button 3 Button 4 Рис. З.7. Ограничение изменения размеров кнопок На заметку! В некоторых ситуациях может использоваться код, проверяющий, насколько велик элемент в окне. Свойства Height и Width в этом не помогут, т.к. отражают желаемые установки размера, которые могут не соответствовать действительному визуализируемому размеру. В идеальном сценарии элементам позволено менять размеры так, чтобы уместить свое содержимое, и тогда свойства Height и Width вообще устанавливать не надо. Узнать действительный размер, используемый при визуализации элемента, можно через свойства ActualHeight и ActualWidth. Однако помните, что эти значения могут меняться при изменении размера окна или содержимого элементов. Окна с автоматически устанавливаемыми размерами В данном примере присутствует один элемент с жестко закодированным размером: окно верхнего уровня, которое содержит в себе панель StackPanel (и все остальное внутри). По ряду причин жестко кодировать размеры окна по-прежнему имеет смысл • Во многих случаях требуется сделать окно меньше, чем диктует желаемый размер его дочерних элементов. Например, если окно включает контейнер с прокручиваемым текстом, нужно будет ограничить размер этого контейнера, чтобы прокрутка стала возможной. Не следует делать это окно чрезмерно большим, чтобы отпала необходимость в прокрутке, чего требует контейнер. (Прокрутка более подробно рассматривается в главе 6.)
92 Глава 3. Компоновка • Минимальные размеры окна могут быть удобны, но при этом не обеспечивать наиболее привлекательные пропорции. Другие размеры окна просто лучше выглядят • Автоматическое изменение размеров окна не ограничено размером области отображения на мониторе. В результате окно с автоматически установленным размером может оказаться слишком большим для просмотра. Однако окна с автоматически устанавливаемым размером вполне допустимы, и они имеют смысл, когда конструируется простое окно с динамическим содержимым. Чтобы включить автоматическую установку размеров окна, удалите свойства Height и Width и установите свойство Window. SizeToContent в WidthAndHeight. Окно сделает себя достаточно большим, чтобы уместить все содержимое. Можно также позволить окну изменять свой размер только в одном измерении, используя для свойства SizeToContent значение Width или Height. Элемент Border Border не является одной из панелей компоновки, но это удобный элемент, который часто будет использоваться вместе с ними. По этой причине имеет смысл ознакомиться с ним сейчас, пока мы не двинулись дальше. Класс Border предельно прост. Он принимает единственную порцию вложенного содержимого (которым часто является панель компоновки) и добавляет фон или рамку вокруг него. Для работы с Border понадобятся свойства, перечисленные в табл. 3.4. Таблица 3.4. Свойства класса Border Имя Описание Background С помощью объекта Brush устанавливает фон, который появляется под содержимым. Можно использовать сплошной цвет либо что-нибудь более экзотическое BorderBrush и Устанавливают цвет рамки, который появляется на границе объекта BorderThickness Border, и толщину рамки. Для отображения рамки потребуется установить оба свойства CornerRadius Позволяет скруглить углы рамки. Чем больше значение CornerRadius, тем заметнее эффект Padding Добавляет пространство между рамкой и содержимым внутри нее. (В отличие от этого, поля добавляют пространство вне рамки.) Ниже показана разметка для простой, слегка скругленной рамки вокруг группы кнопок в StackPanel: <Border Margin=M5M Padding=M5M Background="LightYellow11 BorderBrush=MSteelBlueM BorderThickness=,5,3,5" CornerRadius=M3M VerticalAlignment="Topll> <StackPanel> <Button Margin=M3M>One</Button> <Button Margin=M3M>Two</Button> <Button Margin=M3M>Three</Button> </StackPanel> </Border> Результат можно видеть на рис. 3.8. В главе 6 приводятся дополнительные сведения о кистях и цветах, которые можно использовать для установки BorderBrush и Background.
■ SimpleBorder ! Ш Ш ШШшШ Глава З. Компоновка 93 1,,.,. \CHZ |( • 1 One Two Three Рис. З.8. Базовая рамка На заметку! Формально Border — это декоратор, т.е. разновидность элемента, которая обычно используется для добавления некоторого рода графического оформления объекта. Все декораторы наследуются от класса System.Windows.Controls.Decorator. Большинство декораторов создано специально для оформления определенного рода элементов управления. Например, Button использует декоратор ButtonChrome, чтобы создать оригинальные скругленные углы и фон с тенью, в то время как ListBox использует декоратор ListBoxChrome. Существуют два более общих декоратора, которые полезны при построении пользовательских интерфейсов: Border обсуждается в этой главе, a Viewbox — в главе 12. WrapPanel и DockPanel Очевидно, что одна лишь панель StackPanel не может помочь в создании реалистичного пользовательского интерфейса. Чтобы довершить картину, панель StackPanel должна работать с другими более развитыми контейнерами компоновки. Только так получится создать полноценное окно. Наиболее изощренный контейнер компоновки — это Grid, который рассматривается далее в этой главе. Но сначала стоит взглянуть на WrapPanel и DockPanel — два простых контейнера компоновки, предлагаемых WPF. Они дополняют StackPanel другим поведением компоновки. WrapPanel Панель WrapPanel располагает элементы управления в доступном пространстве — по одной строке или колонке за раз. По умолчанию свойство WrapPanel.Orientation установлено в Horizontal; элементы управления располагаются слева направо, затем — в следующих строках. Установка значения Vertical для свойства WrapPanel. Orientation приводит к размещению элементов в нескольких колонках. Совет. Подобно StackPanel, панель WrapPanel действительно предназначена для управления мелкими деталями пользовательского интерфейса, а не компоновкой всего окна. Например, WrapPanel можно использовать для удержания вместе кнопок в элементе управления, подобном панели инструментов. Ниже приведен пример, в котором определяется последовательность кнопок с разными выравниваниями, помещенных в WrapPanel:
94 Глава 3. Компоновка <WrapPanel Margin=,,3"> <Button VerticalAlignment="Top">Top Button</Button> <Button MinHeight=0">Tall Button 2</Button> <Button VerticalAlignment="Bottom">Bottom Button</Button> <Button>Stretch Button</Button> <Button VerticalAlignment="Center,,>Centered Button</Button> </WrapPanel> На рис. 3.9 показано, что кнопки переносятся для заполнения текущего размера WrapPanel (определяемого размером окна, содержащего его). Как демонстрирует этот пример, WrapPanel в горизонтальном режиме создает серии воображаемых строк, высота каждой из которых определяется высотой самого крупного содержащегося в ней элемента. Другие элементы управления могут быть растянуты для заполнения строки или выровнены в соответствии со значением свойства VerticalAlignment. В примере, представленном слева на рис. 3.9, все кнопки выстроены в одну строку, причем некоторые растянуты, а другие выровнены по этой строке. В примере справа несколько кнопок выталкиваются на вторую строку. Поскольку вторая строка не включает слишком высоких кнопок, высота строки равна минимальной высоте кнопок. В результате не важно, какое значение VerticalAlignment используют кнопки в этой строке. .youtPanels 1^1'°> ШШ Top Button; £utton2 Bottom Button :—] Stretch Button |Centerea Button • ' LayoutPanels Tall Button 2 Bottom Bu Centered В [Bottom Button]Stretch Button 3uttoni Рис. З.9. Перенос кнопок На заметку! WrapPanel — единственная панель, которая не может дублироваться за счет хитроумного использования элемента Grid. DockPanel Панель DockPanel обеспечивает более интересный вариант компоновки. Эта панель растягивает элементы управления вдоль одной из внешних границ. Простейший способ представить это — вообразить панель инструментов, которая присутствует в верхней части многих Windows-приложений. Такие панели инструментов пристыковываются к верхней части окна. Как и в случае StackPanel, пристыкованные элементы должны выбрать один аспект компоновки. Например, если вы пристыковать кнопку к верхней части DockPanel, она растянется на всю ширину DockPanel, но получит высоту, которая ей понадобится (на основе своего содержимого и свойства MinHeight). С другой стороны, если пристыковать кнопку к левой стороне контейнера, ее высота будет растянута для заполнения контейнера, но ширина будет установлена по необходимости. Возникает закономерный вопрос: каким образом дочерние элементы выбирают сторону для пристыковки? Ответ: через присоединенное свойство по имени Dock, которое может быть установлено в Left, Right, Top или Bottom. Каждый элемент, помещаемый внутри DockPanel, автоматически получает это свойство.
Глава 3. Компоновка 95 Ниже приведен пример, который помещает по одной кнопке на каждую сторону DockPanel: <DockPanel LastChildFill="True"> <Button DockPanel.Dock="Top">Top Button</Button> <Button DockPanel.Dock="Bottom">Bottom Button</Button> <Button DockPanel.Dock="Left">Left Button</Button> <Button DockPanel.Dock="Right">Right Button</Button> <Button>Remaining Space</Button> </DockPanel> В этом примере также свойство LastChildFill устанавливается в true, что указывает DockPanel о необходимости отдать оставшееся пространство последнему элементу. Результат показан на рис. 3.10. SmpleDock i_5H ©.. _е" Button Top Button Remaining Space Right Button Рис. 3.10. Пристыковка к каждой стороне Ясно, что при такой пристыковке элементов управления важен порядок. В данном примере верхняя и нижняя кнопки получают всю ширину DockPanel, поскольку они пристыкованы первыми. Когда затем стыкуются левая и правая кнопки, они помещаются между первыми двумя. Если поступить наоборот, то левая и правая кнопки получат полную высоту сторон панели, а верхняя и нижняя станут уже, потому что им придется размещаться между боковыми кнопками. Можно пристыковать несколько элементов к одной стороне. В этом случае элементы просто выстраиваются вдоль этой стороны в том порядке, в котором они объявлены в разметке. И, если вам не нравится поведение в отношении растяжения и промежуточных пробелов, можете подкорректировать свойства Margin, HorizontalAlignment и VerticalAlignment, как делали это со StackPanel. Ниже для целей иллюстрации приведена модифицированная версия предыдущего примера. <DockPanel LastChildFill="True"> <Button DockPanel.Dock="Top">A Stretched Top Button</Button> <Button DockPanel.Dock="Top" HorizontalAlignment="Center"> A Centered Top Button</Button> <Button DockPanel.Dock="Top" HorizontalAlignment="Left"> A Left-Aligned Top Button</Button> <Button DockPanel.Dock="Bottom">Bottom Button</Button> <Button DockPanel.Dock="Left">Left Button</Button> <Button DockPanel.Dock="Right">Right Button</Button> <Button>Remaining Space</Button> </DockPanel>
96 Глава 3. Компоновка Поведение в отношении стыковки остается прежним. Сначала стыкуются верхние кнопки, затем стыкуется нижняя кнопка и, наконец, оставшееся пространство делится между боковыми кнопками, а последняя кнопка размещается в середине. На рис. 3.11 можно видеть полученное в результате окно. • SimpleDodc A Stretched Top Button |A Centered Top Button [A Left-Aligned Top Button Left Button Remaining Space Right Button Bottom Button ь, J Рис. 3.11. Стыковка нескольких элементов к верхней части окна Вложение контейнеров компоновки Панели StackPanel, WrapPanel и DockPanel редко используются сами по себе. Вместо этого они применяются для формирования частей интерфейса. Например, панель DockPanel можно использовать для размещения разных контейнеров StackPanel и WrapPanel в соответствующих областях окна. Например, предположим, что необходимо создать стандартное диалоговое окно с кнопками ОК и Cancel (Отмена) в нижнем правом углу, расположив большую область содержимого в остальной части окна. Существует несколько способов смоделировать этот интерфейс в WPF, но простейший вариант, при котором применяются описанные ранее панели, выглядит следующим образом. 1. Создайте горизонтальную панель StackPanel для размещения рядом кнопок ОК и Cancel. 2. Поместите панель StackPanel в DockPanel и пристыкуйте ее к нижней части окна. 3. Установите свойство DockPanel.LastChildFill в true, чтобы можно было использовать остаток окна для заполнения прочим содержимым. Сюда можно добавить другой элемент управления компоновкой либо просто обычный элемент управления Text Box (как в примере). 4. Установите значения полей, чтобы распределить пустое пространство. Ниже показана итоговая разметка: <DockPanel LastChildFill="True"> <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin=0/10/2/10" Padding=">0K</Button>
Глава 3. Компоновка 97 <Button Margin=/10,10,10" Padding=">Cancel</Button> </StackPanel> <TextBox DockPanel.Dock="Top" Margin=0">This is a test.</TextBox> </DockPanel> В этом примере с помощью свойства Padding добавляется некоторое минимальное пространство между рамкой кнопки и ее внутренним содержимым ("ОК" или "Cancel"). На рис. 3.12 можно видеть полученное в результате диалоговое окно. * BasicDialogBox iSV^J ->ч| This is a test. СК Cancel Рис. 3.12. Базовое диалоговое окно На первый взгляд может показаться, что все это требует больше работы, чем точное размещение с использованием координат в традиционном приложении Windows Forms. Во многих случаях так оно и есть. Однако более высокие временные затраты на установку компенсируются легкостью, с которой можно в будущем изменять пользовательский интерфейс. Например, если вы решите, что кнопки ОК и Cancel должны размещаться по центру нижней части окна, достаточно просто изменить выравнивание содержащей их панели StackPanel: <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Center" ... > Такой дизайн — простое окно с центрированными кнопками — уже демонстрирует результат, который был невозможен в Windows Forms из .NET 1.x (по крайней мере, невозможен без написания кода) и который требовал специализированных контейнеров компоновки в Windows Forms из .NET 2.0. И если вы когда-либо видели код для визуального конструктора, сгенерированный процессом сериализации Windows Forms, то согласились бы, что используемая здесь разметка яснее, проще и компактнее. Добавив стиль к этому окну (глава 11), можно еще более усовершенствовать его и удалить другие излишние детали (вроде установки полей), чтобы создать по-настоящему адаптируемый пользовательский интерфейс. Совет. При наличии дерева элементов с плотными вложениями очень легко потерять представление об общей структуре. В Visual Studio доступно удобное средство, показывающее древовидное представление элементов и позволяющее выбирать в нем нужный элемент для просмотра или модификации. Этим средством является окно Document Outline (Эскиз документа), которое открывается выбором пункта меню View=>Other Windows^Document Outline (Вид1^Другие окна1^Эскиз документа).
98 Глава 3. Компоновка Grid Элемент управления Grid — это наиболее мощный контейнер компоновки в WPF. Большая часть того, что можно достичь с помощью других элементов управления компоновкой, также возможно и в Grid. Контейнер Grid является идеальным инструментом для разбиения окна на меньшие области, которыми можно управлять с помощью других панелей. Фактически Grid настолько удобен, что при добавлении в Visual Studio нового документа XAML для окна автоматически добавляются дескрипторы Grid в качестве контейнера первого уровня, вложенного внутрь корневого элемента Window. Grid располагает элементы в невидимой сетке строк и колонок. Хотя в отдельную ячейку этой сетки можно поместить более одного элемента (и тогда они перекрываются), обычно имеет смысл помещать в каждую ячейку по одному элементу. Конечно, этот элемент сам может быть другим контейнером компоновки, который организует собственную группу содержащихся в нем элементов управления. Совет. Хотя Grid задуман как невидимый элемент, можно установить свойство Grid. ShowGridLines в true и получить наглядное представление о нем. Это средство на самом деле не предназначено для украшения окна. В действительности это средство для облегчения отладки, которое предназначено для того, чтобы наглядно показать, как Grid разделяет пространство на отдельные области. Благодаря ему, появляется возможность точно контролировать то, как Grid выбирает ширину колонок и высоту строк. Создание компоновки на основе Grid — двухшаговый процесс. Сначала выбирается необходимое количество колонок и строк. Затем каждому содержащемуся элементу назначается соответствующая строка и колонка, тем самым помещая его в правильное место. Колонки и строки создаются путем заполнения объектами коллекции Grid. ColumnDef initions и Grid.RowDef initions. Например, если вы решите, что требуется две строки и три колонки, то используйте следующие дескрипторы: <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDef initionX/RowDef inition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefmition></ColumnDefinition> <ColumnDef initionx/Column Definition> <ColumnDef initionX/Column Definition> </Grid.ColumnDefinitions> </Grid> В этом примере демонстрируется, что указывать какую-либо информацию в элементах RowDef in it ion или ColumnDef in it ion не обязательно. Если вы оставите их пустыми (как показано здесь), то Grid поровну разделит пространство между всеми строками и колонками. В данном примере каждая ячейка будет одного и того же размера, который зависит от размера включающего окна. Для помещения индивидуальных элементов в ячейку используются присоединенные свойства Row и Column. Оба эти свойства принимают числовое значение индекса, начинающееся с 0. Например, вот как можно создать частично заполненную кнопками сетку:
Глава 3. Компоновка 99 <Grid ShowGridLines="True"> <Button Grid.Row=" Grid.Column=">Top Left</Button> <Button Grid.Row=" Grid.Column="l">Middle Left</Button> <Button Gnd.Row=" 1" Grid.Column=">Bottom Right</Button> <Bu'tton Grid.Row="l" Grid.Column="l">Bottom Middle</Button> </Grid> Каждый элемент должен быть помещен в свою ячейку явно. Это позволяет помещать в одну ячейку более одного элемента (что редко имеет смысл) или же оставлять определенные ячейки пустыми (что часто бывает полезным). Это также означает возможность объявления элементов в любом порядке, как это сделано с последними двумя кнопками в этом примере. Однако более ясной разметка получится, если определять элементы управления строку за строкой, а в каждой строке — слева направо. Существует одно исключение. Если не указать значение для свойства Grid.Row, то оно предполагается равным 0. То же самое касается и свойства Grid.Column. Таким образом, если опущены оба атрибута элемента, он помещается в первую ячейку Grid. На заметку! Контейнер Grid помещает элементы в предопределенные строки и колонки. Это отличает его от таких контейнеров компоновки, как WrapPanel и StackPanel, создающих неявные строки и колонки в процессе размещения дочерних элементов. Чтобы создать сетку, состоящую из более чем одной строки и одной колонки, необходимо определить строки и колонки явно, используя объекты RowDefinition и ColumnDefinition. На рис. 3.13 показано, как эта простая сетка выглядит в разных размерах. Обратите внимание, что свойство ShowGridLines установлено в true, так что можно видеть границы между колонками и строками. Как и можно было бы ожидать, Grid предоставляет базовый набор свойств компоновки, перечисленных в табл. 3.3. Это значит, что можно добавлять поля вокруг содержимого ячейки, изменять режим установки размеров, чтобы элемент не рос, заполняя ячейку целиком, а также выравнивать элемент по одному из граней ячейки. Если вы заставите элемент иметь размер, превышающий тот, что может уместить ячейка, часть содержимого будет отсечена. • SimpleGnd т Top Left Middle Left Bottom Middle» Bottom Right • SimpleGnd Top Left Middle Left Bottom Middle -> ._.... Bottom Right | Рис. 3.13. Простая сетка
100 Глава 3. Компоновка Использование Grid в Visual Studio При использовании Grid на поверхности проектирования Visual Studio вы обнаружите, что он работает несколько иначе, чем другие контейнеры компоновки. При перетаскивании элемента на Grid среда Visual Studio позволяет поместить его в точную позицию. Visual Studio выполняет подобный фокус, устанавливая свойство Margin элемента. При установке полей Visual Studio использует ближайший угол. Например, если ближайшим к элементу является верхний левый угол Grid, то Visual Studio устанавливает верхнее и левое поля для позиционирования элемента (оставляя правое и нижнее поля равными 0). Если вы перетаскиваете элемент ниже, приближая его к нижнему левому углу, то Visual Studio устанавливает вместо этого нижнее и левое поля и устанавливает свойство VerticalAlignment в Bottom. Это очевидно влияет на то, как перемещается элемент при изменении размера Grid. Процесс установки полей в Visual Studio выглядит достаточно прямолинейным, но в большинстве случаев он приводит не к тому результату, который необходим. Обычно требуется более гибкая потоковая компоновка, которая позволяет некоторым элементам расширяться динамически, "расталкивая" соседей. В этом сценарии вы сочтете жесткое кодирование позиции свойством Margin совершенно негибким. Проблема усугубляется при добавлении множества элементов, потому что Visual Studio не добавляет автоматически новых ячеек. В результате все такие элементы помещаются в одну и ту же ячейку. Разные элементы могут выравниваться по разным углам Grid, что заставит их перемещаться друг относительно друга (и даже перекрывать друг друга) при изменении размеров окна. Однажды поняв, как работает Grid, вы сможете исправлять эти проблемы. Первый трюк заключается в конфигурировании Grid перед добавлением элементов за счет определения новых строк и колонок. (Коллекции RowDefinitions и ColumnDefinitions можно редактировать с использованием окна Properties (Свойства).) Однажды настроив Grid, вы можете перетаскивать в него нужные элементы и конфигурировать их настройки полей и выравнивание в окне Properties либо редактируя XAML-разметку вручную. Тонкая настройка строк и колонок Если бы Grid был просто коллекцией строк и колонок пропорциональных размеров, от него было бы мало толку. К счастью, он не таков. Чтобы открыть полный потенциал Grid, можно изменять способы установки размеров каждой строки и колонки. Элемент Grid поддерживает следующие стратегии изменения размеров. • Абсолютные размеры. Выбирается точный размер с использованием независимых от устройства единиц измерения. Это наименее удобная стратегия, поскольку она недостаточно гибка, чтобы справиться с изменением размеров содержимого, изменением размеров контейнера или локализацией. • Автоматические размеры. Каждая строка и колонка получает в точности то пространство, которое нужно, и не более. Это один из наиболее удобных режимов изменения размеров. • Пропорциональные размеры. Пространство разделяется между группой строк и колонок. Это стандартная установка для всех строк и колонок. Например, на рис. 3.13 вы увидите, что все ячейки увеличиваются пропорционально при расширении Grid. Для максимальной гибкости можно смешивать и сочетать эти разные режимы изменения размеров. Например, часто удобно создать несколько автоматически изменяющих размер строк и затем позволить одной или двум остальным строкам поделить между собой оставшееся пространство через пропорциональную установку размеров.
Глава 3. Компоновка 101 Режим изменения размеров устанавливается с помощью свойства Width объекта ColumnDef inition или свойства Height объекта RowDef inition, присваивая ему некоторое число или строку. Например, ниже показано, как установить абсолютную ширину в 100 независимых от устройства единиц: <ColumnDefinition Width=00"></ColumnDefinition> Чтобы использовать пропорциональное изменение размеров, указывается значение Auto: <ColumnDefinition Width="Autо"></ColumnDefinition> И, наконец, для активизации пропорционального изменения размеров задается звездочка (*): <ColumnDefinition Width="*"></ColumnDefinition> Этот синтаксис пришел из мира Интернета, где он применяется на страницах HTML с фреймами. Если вы используете смесь пропорциональной установки размеров с другими режимами, то пропорционально изменяемая строка или колонка получит все оставшееся пространство. Чтобы разделить оставшееся пространство неравными частями, можно назначить вес (weight), который должен указываться перед звездочкой. Например, если есть две строки пропорционального размера, и требуется, чтобы высота первой была равна половине высоты второй, необходимо разделить оставшееся пространство следующим образом: <RowDefinition Height="*"></RowDefinition> <RowDefinition Height=*"></RowDefinition> Это сообщит Grid о том, что высота второй строки должна быть вдвое больше высоты первой строки. Для разделения дополнительного пространства можно указывать любые числа. На заметку! Легко организовать программное взаимодействие между объектами ColumnDef inition и RowDef inition. Нужно просто знать, что свойства Width и Height — это объекты типа GetLength. Чтобы создать GridLength, представляющий определенный размер, просто передайте соответствующее значение конструктору GridLength. Для создания объекта GridLength, представляющего пропорциональный размер (*), необходимо передать число конструктору GridLength и значение GridUnitType.Start в качестве второго аргумента конструктора. Для обозначения автоматического изменения размера используется статическое свойство GridLength.Auto. С помощью этих режимов установки размеров можно продублировать тот же пример диалогового окна, показанного на рис. 3.12, используя вместо DockPanel контейнер Grid верхнего уровня для разделения окна на две строки. Ниже показана разметка, которая для этого понадобится: <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDeflnition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <TextBox Margin=0" Grid.Row=">This is a test.</TextBox> <StackPanel Grid.Row=" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin=0,10,2,10" Padding=">OK</Button> <Button Margin=,10,10,10" Padding=">Cancel</Button> </StackPanel> </Grid>
102 Глава 3. Компоновка Совет. В этом элементе Grid не объявлены какие-либо колонки. Такое сокращение можно применять, если Grid использует только одну колонку, размер которой устанавливается пропорционально (так что заполняет всю ширину Grid). Этот код разметки немного длиннее, но обладает тем преимуществом, что объявляет элементы управления в порядке их появления, что облегчает его понимание. Выбор такого подхода — просто вопрос персональных предпочтений. При желании вложенную панель StackPanel можно заменить элементом Grid с одной строкой и одной колонкой. На заметку! С помощью вложенных контейнеров Grid можно создать практически любой интерфейс. (Единственное исключение — строки с переносом колонок, использующие WrapPanel.) Однако когда вы имеете дело с небольшими разделами пользовательского интерфейса или расположением небольшого количества элементов, то часто проще применить более специализированные контейнеры StackPanel и DockPanel. Округление компоновки Без округления компоновки щая от разрешения система измерений. Хотя это обеспечивает гибкость для работы с различным оборудованием, иногда оно привносит некоторые сложности. Одной из них является тот факт, что элементы могут оказаться выровненными по межпиксельным границам. Другими словами, элементы будут позиционированы по дробным координатам, которые не совпадают с линией физических пикселей. Это П можете случиться в результате указания нецелых размеров для контейнеров компоновки. Однако подобная ситуация может возникнуть и тогда, когда она не ожидается, например, при создании Grid с пропорциональными размерами. Например, предположим, что есть контейнер Grid из двух столбцов, имеющий общую ширину в 200 пикселей. Если распределить ширину между столбцами поровну, каждый получит по 100 пикселей. Но если общая ширина составляет, скажем, 175 пикселей, то разделение между ними не столь ясно, и каждый столбец получает по 87,5 пикселя. Это значит, что второй столбец будет слегка смещен относительно обычных границ пикселей. Обычно это не представляет проблемы, но если столбец содержит один из элементов формы, рамку или графическое изображение, то содержимое может оказаться размытым, потому что WPF использует сглаживание в отношении того, что иначе имело бы резкие грани по границам пикселей. Проблема проиллюстрирована на рис. 3.14. Здесь показана увеличенная часть окна, которое содержит два контейнера Grid. В верхнем контейнере Grid не используется округление компоновки, в результате чего четкие границы прямоугольника внутри становятся размытыми при определенных размерах окна. Существует простое решение этой проблемы. Просто установите свойство Use Lay out Rounding контейнера компоновки в true: <Grid UseLayoutRounding="True"> После этого WPF будет обеспечивать размещение всего содержимого контейнера компоновки четко по ближайшим границам пикселей, исключая размытие. С округлением компоновки Рис. 3.14. Размытие границ при пропорциональном распределении размеров
Глава 3. Компоновка 103 Объединение строк и колонок Вы уже видели, как помещаются элементы в ячейки с использованием присоединенных свойств Row и Column. Можно также использовать еще два присоединенных свойства, чтобы растянуть элемент на несколько ячеек: RowSpan и ColumnSpan. Эти свойства принимают количество строк или колонок, которые должен занять элемент. Например, следующая кнопка займет все место, доступное в первой и второй ячейках первой строки: <Button Grid.Row=" Grid.Column=" Grid.RowSpan=">Span Button</Button> А эта кнопка растянется всего на четыре ячейки, охватив две колонки и две строки: <Button Grid.Row=" Grid.Column=" Grid.RowSpan=" Grid.ColumnSpan="> Span Button</Button> Объединение нескольких строк и колонок позволяет достичь некоторых интересных эффектов, и особенно удобно, когда требуется уместить в табличную структуру элементы, которые меньше или больше существующих ячеек. Используя объединение колонок, пример простого диалогового окна на рис. 3.12 можно переписать, оставив единственный Grid. Этот контейнер Grid делит окно на три колонки, растягивая текстовое поле на все три, и использует последние две колонки для выравнивания кнопок О К и Cancel (Отмена). <Grid ShowGridLines="True"> <Grid.RowDefinitions> » <RowDefmition Height="*"></RowDef1nition> <RowDefmition He1ght="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefmition Width="*"></ColumnDefinition> <ColumnDefmition Width=MAuto"></ColumnDefinition> <ColumnDefmition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBox Margin=0u Grid.Row=" Grid.Column=" Grid.ColumnSpan="> This is a test.</TextBox> <Button Margin=0,10,2,10" Padding=" Grid.Row=" Grid.Column="l">OK</Button> <Button Margin=,10,10,10" Padding=" Grid.Row="l" Grid.Column=">Cancel</Button> </Grid> Большинство разработчиков согласится с утверждением, что такая компоновка непонятна. Ширины колонок определяются размером двух кнопок окна, что затрудняет добавление нового содержимого к существующей структуре Grid. Для внесения даже минимального дополнения к этому окну, скорее всего, понадобится создавать новый набор колонок. Как видите, при выборе контейнера компоновки для окна нужно не просто добиться корректного поведения компоновки, а необходимо также получить структуру компоновки, которую легко сопровождать и расширять в будущем. Хорошее эмпирическое правило заключается в использовании меньших контейнеров компоновки, подобных StackPanel для одноразовых задач компоновки, таких как организация группы кнопок. С другой стороны, если требуется применить согласованную структуру к более чем одной области окна (как с колонкой текстового поля, показанной ниже, на рис. 3.22), то в таком случае Grid — незаменимый инструмент для стандартизации компоновки.
104 Глава 3. Компоновка Разделенные окна Каждый пользователь Windows встречался с разделительными полосами — перемещаемыми разделителями, которые отделяют одну часть окна от другой. Например, в проводнике Windows слева находится список папок, а справа — список файлов. Перетаскивая разделительную полосу, можно устанавливать пропорции между этими двумя панелями в окне. В WPF полосы разделителей представлены классом GridSplitter и являются средствами Grid. Добавляя GridSplitter к Grid, вы предоставляете пользователю возможность изменения размеров строк и колонок. На рис. 3.15 показано окно, в котором GridSplitter находится между двумя колонками. Перетаскивая полосу разделителя, пользователь может менять относительные ширины обеих колонок. SolitWindow jo |&_Циа^] Left Left 4 Right Right ■ SplitWindow Left Left i v :1u: @.b£id| S Right i 1 : j Right ~ ,. .,' л.~. ) Рис. 3.15. Перемещение полосы разделителя Большинство программистов считают GridSplitter наиболее интуитивно понятной частью WPF. Чтобы разобраться, как использовать его для получения требуемого эффекта, нужно лишь немного поэкспериментировать. Ниже предлагается несколько подсказок. • GridSplitter должен быть помещен в ячейку Grid. Его можно поместить в ячейку с существующим содержимым — тогда понадобится настроить установки полей, чтобы они не перекрывались. Лучший подход заключается в резервировании специальной колонки или строки для GridSplitter, со значениями Height или Width, равными Auto. • GridSplitter всегда изменяет размер всей строки или колонки (в не отдельной ячейки). Чтобы сделать внешний вид GridSplitter соответствующим такому поведению, необходимо растянуть GridSplitter по всей строке или колонке, а не ограничиваться единственной ячейкой. Для этого используются свойства Row Span и ColumnSpan, которые рассматривались ранее. Например, GridSplitter на рис. 3.15 имеет значение RowSpan, равное 2. В результате он растягивается на всю колонку. Если вы не добавите эту установку, он появится только в верхней строке (где помещен), даже несмотря из. то, что перемещение разделительной полосы изменило бы размер всей колонки.
Глава 3. Компоновка 105 • Изначально GridSplitter настолько мал, что его не видно. Чтобы сделать его удобным, понадобится указать его минимальный размер. В случае вертикальной разделяющей полосы (вроде показанной на рис. 3.15) нужно установить VerticalAlignment в Stretch (чтобы он заполнил всю высоту доступной области), a Width — в фиксированный размер (например, 10 независимых от устройства единиц). В случае горизонтальной разделительной полосы следует установить HorizontalAlignment в Stretch, a Height — в фиксированный размер. • Выравнивание GridSplitter также определяет, будет ли разделительная полоса горизонтальной (используемой для изменения размеров строк) или вертикальной (для изменения размеров колонок). В случае горизонтальной разделительной полосы необходимо установить VerticalAlignment в Center (что принято по умолчанию), указав тем самым, что перетаскивание разделителя изменит размеры строк, находящихся выше и ниже. В случае вертикальной разделительной полосы (как на рис. 3.13) понадобится установить HorizontalAlignment в Center, чтобы изменять размеры соседних колонок. На заметку! Изменить поведение установки размеров можно через свойства ResizeDirection и ResizeBehavior объекта GridSplitter. Однако проще поставить это поведение в зависимость от установок выравнивания, что и принято по умолчанию. Еще не запутались? Чтобы закрепить эти правила, стоит взглянуть на реальный код разметки примера, показанного на рис. 3.15. В следующем листинге детали GridSplitter выделены полужирным. <Grid> <Grid.RowDefinitions> <RowDef mitionx/RowDef inition> <RowDef mitionx /RowDef in it ion> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefmition MinWidth=00"></ColumnDefinition> <ColumnDefinition Width="Auto"X/ColmnnDefinition> <ColumnDefmition MinWidth=0"x/ColumnDefinition> </Grid.ColumnDefinitions> <Button Grid.Row=" Grid.Column=" Margin=">Left</Button> <Button Grid.Row=" Grid.Column=" Margin=">Right</Button> <Button Grid.Row="l" Grid.Column=" Margin=">Left</Button> <Button Grid.Row="l" Grid.Column=" Margin=">Right</Button> <GridSplitter Grid.Row=" Grid.Column="l" Grid.RowSpan=" Width=" VerticalAlignment="Stretch" HorizontalAlignment="Center" Shows Preview=" False" X/GridSplitter> </Grid> Совет. Для создания правильного элемента GridSplitter не забудьте присвоить значения свойствам VerticalAlignment, HorizontalAlignment и Width (или Height). Эта разметка включает одну дополнительную деталь. Когда объявляется GridPlitter, свойство ShowPreview устанавливается в false. В результате, когда полоса разделителя перетаскивается от одной стороны к другой, колонки немедленно изменяют свой размер. Но если установить ShowPreview в true, то при перетаскивании отображается лишь серая тень, следующая за курсором мыши, которая показывает, куда попадет разделитель после отпускания кнопки мыши. Вплоть до этого момента колонки изменять размеры не будут. После получения фокуса элементом GridSplitter для изменения размера можно также использовать клавиши со стрелками.
106 Глава 3. Компоновка ShowPreview — не единственное свойство GridSplitter, которое доступно для установки. Можно также изменить свойство Draglncrement, если полоса разделителя должна перемещаться "шагами" (например, по 10 единиц за раз). Для управления минимально и максимально допустимыми размерами колонок просто устанавливаются соответствующие свойства в разделе ColumnDef initions, как было показано в предыдущем примере. Совет. Есть возможность изменить заливку GridSplitter, чтобы она не выглядела просто серым прямоугольником. Трюк заключается в использовании свойства Background, которое принимает значения простых цветов и более сложных кистей. Обычно Grid содержит не более одного GridSplitter. Тме не менее, можно вкладывать один Grid в другой, и при этом каждый из них будет иметь собственный GridSplitter. Это позволяет создавать окна, которые разделены на две области (например, на левую и правую панель), одна из которых (скажем, правая), в свою очередь, также разделена на два раздела (на верхний и нижний с изменяемыми размерами). Пример показан на рис. 3.16. DoubleSplitWindow Top Left Bottom Left Top Right Bottom Right Рис. 3.16. Окна с двумя разделителями Создать такое окно довольно просто, хотя управление тремя контейнерами Grid, которые здесь присутствуют, требует некоторых усилий: общий Grid, вложенный Grid слева и вложенный Grid справа. Единственный трюк состоит в том, чтобы установить GridSplitter в правильную ячейку и задать ему правильное выравнивание. Ниже показана полная разметка. <'-- Это Grid для целого окна. --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <!-- Это вложенный Grid слева. Он не делится разделителем. --> » DoubleSplitWindow Го* _ef: Bottom _ef: Top Right 1 Bottom Right
Глава 3. Компоновка 107 <Grid Grid.Column=" VerticalAlignment="Stretch"> <Grid.RowDefinitions> <RowDef mitionx/RowDef inition> <RowDefinition></RowDeflnition> </Grid.RowDefinitions> <Button Margin=" Grid.Row=">Top Left</Button> <Button Margin=" Grid.Row="l">Bottom Left</Button> </Grid> < '-- Это вертикальный разделитель, находящийся между двумя вложенными (правым и левым) Grid. --> <GridSplitter Grid.Column="l" Width=" НогizontalAlignment="Center" VerticalAlignment="Stretch" ShowsPreview="False"></GridSplitter> <'-- Это вложенный Grid справа. --> <Grid Grid.Column="> <Grid.RowDefinitions> <RowDef mitionx /RowDef inition> <RowDefmition Height="Auto"></RowDefinition> < RowDef mitionx /RowDef 1 nit ion> </Grid.RowDefinitions> <Button Grid.Row=" Margin=">Top Right</Button> <Button Grid.Row=" Margin=">Bottom Right</Button> < '-- Это горизонтальный разделитель, отделяющий верхнюю область от нижней. --> <GridSplitter Grid.Row="l" Height=" VerticalAlignment="Center" HorizontalAlignment="Stretch" Shows Preview="False"x/GridSplitter> </Grid> </Grid> Совет. Помните, что если Grid имеет всего одну строку или колонку, раздел RowDef mition может быть опущен. Кроме того, элементы, которые не имеют явно установленной позиции строки, предполагают значение Grid.Row, равное 0, и помещаются в первой строке. То же самое справедливо и в отношении элементов, для которых не указано Grid.Column Группы с общими размерами Как уже было указано, Grid содержит коллекцию строк и колонок, размер которых устанавливается явно, пропорционально или на основе размеров их дочерних элементов. Существует только один способ изменить размер строки или колонки — приравнять его размеру другой строки или колонки. Это выполняется с помощью средства, которое называется группы с общими размерами (shared size groups). Цель таких групп — поддержание согласованности между различными частями пользовательского интерфейса. Например, размер одной колонки может быть установлен в соответствии с ее содержимым, а размер другой колонки — в точности равным размеру первой. Однако реальное преимущество групп с общими размерами заключается в обеспечении одинаковых пропорций различным элементам управления Grid. Чтобы понять, как это работает, рассмотрим пример, показанный на рис. 3.17. Это окно оснащено двумя объектами Grid — один в верхней части окна (с тремя колонками) и один в его нижней части (с двумя колонками). Размер левой крайней колонки первого Grid устанавливается пропорционально ее содержимому (длинной текстовой строке). Левая крайняя колонка второго Grid имеет в точности ту же ширину, хотя меньшее
108 Глава 3. Компоновка содержимое. Дело в том, что они входят в одну размерную группу. Независимо от того, какое содержимое вы поместите в первую колонку первого Grid, первая колонка второго Grid останется синхронизированной. [ S-wedSizeGroup A very long bit of text ; More text ; A text box Some text in between the two grids- Short A text box Рис. 3.17. Два элемента Grid, разделяющие одно определение колонки Как демонстрирует этот пример, колонки с общими размерами могут принадлежать к разным элементам Grid. В этом примере верхний Grid имеет на одну колонку больше и потому оставшееся пространство в нем распределяется иначе. Аналогично колонки с общими размерами могут занимать разные позиции, так что можно создать отношение между первой колонкой одного Grid и второй колонкой другого. И очевидно, что колонки при этом могут иметь совершенно разное содержимое. Когда применяется группа с общими размерами, это все равно, как если бы создавалось одно определение колонки (или строки), используемое в более чем одном месте. Это не просто однонаправленная копия одной колонки в другую. В этом можно убедиться, изменив в предыдущем примере содержимое разделенной колонки второго Grid. Теперь колонка в первом Grid будет удлинена для сохранения соответствия (рис. 3.18). • SharedStzeGroup а . 3 A very long bit of text More text ■ A text box Some text in between the two grids... An even longer bit of text over here ; A text box Рис. 3.18. Колонки, разделяющие общий размер, остаются синхронизированными
Глава 3. Компоновка 109 Можно даже добавить GridSplitter к одному из объектов Grid. Когда пользователь будет изменять размер колонки в одном Grid, то соответствующая разделенная колонка из второго Grid также будет синхронно менять свой размер. Создать группы с общими размерами просто. Понадобится лишь установить свойство SharedSizeGroup в обеих колонках, используя строку соответствия. В текущем примере обе колонки используют группу по имени Text Label. <Grid Margin=" Background="LightYellow" ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefmition Width="Auto" SharedSizeGroup="TextLabel"></ColumnDefinition> <ColumnDefmition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Label Margin=">A very long bit of text</Label> <Label Grid.Column="l" Margin=">More text</Label> <TextBox Grid.Column=" Margin=">A text box</TextBox> </Grid> <Grid Margin=" Background="LightYellow" ShowGridLines="True"> <Grid. ColumnDef mitions> <ColumnDefinition Width="Auto" SharedSizeGroup="TextLabel"></ColumnDefinition> <ColumnDefmition></ColumnDeflnition> </Grid.ColumnDefinitions> <Label Margin=">Short</Label> <TextBox Grid.Column="l" Margin=">A text box</TextBox> </Grid> Осталось упомянуть еще одну деталь. Группы с общими размерами не являются глобальными для всего приложения, потому что более одного окна могут непреднамеренно использовать одно и то же имя. Можно предположить, что группы с общими размерами ограничены текущим окном, но на самом деле платформа WPF еще более строга в этом отношении. Чтобы разделить группу, необходимо явно установить присоединенное свойство Grid.IsSharedSizeScope в true в контейнере высшего уровня, содержащем объекты Grid, который имеет колонки с общими размерами. В текущем примере верхний и нижний Grid входят в другой Grid, предназначенный для этой цели, хотя столь же просто можно применить другой контейнер, такой как DockPanel или StackPanel. Ниже показана разметка Grid верхнего уровня. <Grid Grid.IsSharedSizeScope="True" Margin="> <Grid.RowDefinitions> <RowDef initionx/RowDef inition> <RowDefmition Height="Auto"></RowDefinition> <RowDef in ltionx/RowDef inition> </Grid.RowDefinitions> <Grid Grid.Row=" Margin=" Background="LightYellow" ShowGridLines="True"> </Grid> <Label Grid.Row="l" >Some text in between the two grids ...</Label> <Grid Grid.Row=" Margin=" Background="LightYellow" ShowGridLines="True"> </Grid> </Grid> Совет. Для синхронизации отдельных Grid с заголовками колонок можно было бы использовать группу с общими размерами. Ширина каждой колонки может быть затем определена ее содержимым, которое разделит заголовок. Допускается даже поместить GridSplitter в заголовок, и тогда пользователь сможет перетаскивать его для изменения размера заголовка и всей лежащей ниже колонки.
110 Глава 3. Компоновка UniformGrid Существует элемент типа сетки, который идет вразрез со всеми правилами, изученными до сих пор — это UniformGrid. В отличие от Grid, элемент UniformGrid не требует (и даже не поддерживает) предопределенных колонок и строк. Вместо этого вы просто задаете значения свойствам Rows и Columns для установки его размеров. Каждая ячейка всегда имеет одинаковый размер, потому что доступное пространство делится поровну. И, наконец, элементы помещаются в соответствующую ячейку на основе порядка их определения. Нет никаких присоединенных свойств Row и Column, и нет никаких пустых ячеек. Ниже приведен пример наполнения UniformGrid четырьмя кнопками: <UniformGrid Rows=" Columns="> <Button>Top Left</Button> <Button>Top Right</Button> <Button>Bottom Left</Button> <Button>Bottom Right</Button> </UniformGrid> UniformGrid используется намного реже, чем Grid. Элемент Grid — это инструмент общего назначения для создания компоновки окон, от наиболее простых до самых сложных. UniformGrid намного более специализированный контейнер компоновки, который в первую очередь предназначен для размещения элементов в жесткой сетке (например, для построения игрового поля для ряда игр). Многие программисты WPF никогда не пользуются UniformGrid. Координатная компоновка с помощью Canvas Единственный контейнер компоновки, который пока еще не рассматривался — это Canvas. Он позволяет размещать элементы, используя точные координаты, что, вообще говоря, является плохим выбором при проектировании развитых управляемых данными форм и стандартных диалоговых окон, но ценным инструментом, когда требуется построить нечто другое (вроде поверхности рисования для инструмента построения диаграмм). Canvas также является наиболее легковесным из контейнеров компоновки. Это объясняется тем, что он не включает в себя никакой сложной логики компоновки, согласовывающей размерные предпочтения своих дочерних элементов. Вместо этого он просто располагает их в указанных позициях с точными размерами, которые нужны. Для позиционирования элемента в контейнере Canvas устанавливаются присоединенные свойства Canvas.Left и Canvas.Top. Свойство Canvas.Left задает количество единиц измерения между левой гранью элемента и левой границей Canvas. Свойство Canvas.Top устанавливает количество единиц измерения между вершиной элемента и левой границей Canvas. Как всегда, эти значения выражаются в независимых от устройства единицах измерения, которые соответствуют обычным пикселям, когда системная установка DPI составляет 96 dpi. На заметку! В качестве альтернативы вместо Canvas.Left можно использовать Canvas. Right, чтобы расположить элемент относительно правого края Canvas, и Canvas.Bottom вместо Canvas.Top — чтобы расположить его относительно низа. Одновременно использовать Canvas.Right и Canvas.Left или Canvas.Top и Canvas.Bottom нельзя. Дополнительно можно устанавливать размер элемента явно, используя его свойства Width и Height. Это чаще применяется для Canvas, чем с другими панелями, потому что Canvas не имеет собственной логики компоновки. (К тому же вы часто будете ис-
Глава 3. Компоновка 111 пользовать Canvas, когда понадобится точный контроль над расположением комбинации элементов.) Если свойства Width и Height не устанавливаются, элемент получит желательный для него размер; другими словами, он станет достаточно большим, чтобы уместить свое содержимое. Ниже приведен пример простого контейнера Canvas, включающего четыре кнопки. <Canvas> <Button Canvas.Left=0" Canvas.Top=0">A0,10)</Button> <Button Canvas.Left=20" Canvas.Top=0">A20,30)</Button> <Button Canvas.Left=0" Canvas.Top="80" Width=0" Height=0"> F0,80)</Button> <Button Canvas.Left=0" Canvas.Top=20" Width=00" Height=0"> G0,120)</Button> </Canvas> На рис. 3.19 показан результат. Если вы измените размеры окна, то Canvas растянется для заполнения всего доступного пространства, но ни один из элементов управления на его поверхности не изменит своего положения и размеров. Контейнер Canvas не имеет средства привязки или стыковки, которые доступны в координатных компоновках Windows Forms. Отчасти это объясняется легковесностью Canvas. Другая причина в том, чтобы предотвратить использование Canvas для целей, для которых он не предназначен (например, для компоновки стандартного пользовательского интерфейса). Подобно любому другому контейнеру компоновки, Canvas может вкладываться внутрь пользовательского интерфейса. Это значит, что Canvas можно использовать для рисования более детализированного содержимого в одной части окна и применять более стандартные панели WPF для остальных элементов. Совет. Если вы используете Canvas рядом с другими элементами, можно установить его свойство ClipToBounds в true. В результате элементы внутри Canvas, которые выходят за его пределы, будут усечены на гранях Canvas. (Это предотвратит перекрытие других элементов в окне.) Все прочие контейнеры компоновки всегда усекают свои дочерние элементы, выходящие за их границы, независимо от установки ClipToBounds. Z-порядок При наличии более одного перекрывающегося элемента с помощью присоединенного свойства Canvas.ZIndex можно управлять их расположением. Обычно все добавляемые элементы имеют одинаковый ZIndex — 0. Элементы с одинаковым ZIndex отображаются в том порядке, в каком они представлены в коллекции Canvas.Children, который основан на порядке их определения в разметке XAML. Элементы, объявленные позже в разметке, такие как кнопка G0,120), отображаются поверх элементов, объявленных ранее, вроде кнопки A20,30). За счет увеличения ZIndex любой элемент можно передвинуть на более высокий уровень. Это объясняется тем, что элементы с большими ZIndex всегда появляются поверх элементов с меньшими ZIndex. Используя этот подход, можно поменять уровни в компоновке из предыдущего примера на противоположные: Рис. 3.19. Явно позиционированные кнопки в Canvas
112 Глава 3. Компоновка <Button Canvas.Left=0" Canvas.Top="80" Canvas.ZIndex="l" Width=0" Height=0"> F0,80)</Button> <Button Canvas.Left=0" Canvas.Top=20" Width=00" Height=0"> G0,120)</Button> На заметку! Действительные значения, которые используется для свойства Canvas.ZIndex, не важны. Важно отношение значений ZIndex разных элементов между собой. Для ZIndex можно указывать любое положительное или отрицательное целое число. Свойство ZIndex в частности удобно, если нужно изменить позицию элемента программно. Просто вызовите Canvas. Set ZIndex () и передайте ему элемент, который необходимо модифицировать, и новое значение ZIndex. К сожалению, не предусмотрено метода BringToFront () или SendToBackO, так что на вас возлагается задача отслеживать максимальное и минимальное значения ZIndex, если планируется реализовать это поведение. InkCanvas В WPF также имеется элемент InkCanvas, который подобен Canvas в одних отношениях и совершенно отличается в других. Подобно Canvas, элемент InkCanvas определяет четыре присоединенных свойства, которые можно применить к дочерним элементам для координатного позиционирования (Top, Left, Bottom и Right). Однако лежащий в его основе механизм существенно отличается. Фактически InkCanvas не наследуется от Canvas, и даже не наследуется от базового класса Panel. Вместо этого он наследуется непосредственно от FrameworkElement. Главное предназначение InkCanvas заключается в обеспечении перьевого ввода. Перо (stylus) — это подобное карандашу устройство ввода, используемое в планшетных ПК. Однако InkCanvas работает с мышью точно так же, как и с пером. Поэтому пользователь может рисовать линии или выбирать и манипулировать элементами в InkCanvas с применением мыши. InkCanvas в действительности содержит две коллекции дочернего содержимого. Уже знакомая коллекция Children содержит произвольные элементы — как и Canvas. Каждый элемент может быть позиционирован на основе свойств Top, Left, Bottom и Right. Коллекция Strokes содержит объекты System.Windows.Ink.Stroke, представляющие графический ввод, который рисует пользователь в InkCanvas. Каждая нарисованная линия или кривая становится отдельным объектом Stroke. Благодаря этим двум коллекциям, InkCanvas можно использовать для того, чтобы позволить пользователю аннотировать содержимое (хранящееся в коллекции Children) пометками (хранящимися в коллекции Strokes). Например, на рис. 3.20 показан элемент InkCanvas, содержащий изображение, аннотированное дополнительными пометками. Ниже приведена разметка InkCanvas из этого примера, которая определяет изображение: <InkCanvas Name="inkCanvas" Background="LightYellow" EditingMode=,,Ink"> <Image Source="office.jpg" InkCanvas.Top=0" InkCanvas.Left=0" Width= 8 7" Height=19"x/Image> </InkCanvas> Пометки нарисованы пользователем во время выполнения. InkCanvas может применяться несколькими существенно отличающимися способами, в зависимости от значения, которое устанавливается для свойства InkCanvas. EditingMode. Возможные варианты этого значения перечислены в табл. 3.5.
Глава 3. Компоновка 113 [ 111Т ЦКИ11 Рис. 3.20. Добавление пометок в InkCanvas Таблица 3.5. Значения перечисления InkCanvasEditingMode Имя Описание Ink GestureOnly InkAndGesture EraseByStroke EraseByPoint Select None InkCanvas позволяет пользователю рисовать аннотации. Это режим по умолчанию. Когда пользователь рисует мышью или пером, появляются штрихи InkCanvas не позволяет пользователю рисовать аннотации, но привлекает внимание к некоторым предопределенным жестам (gesture), таким как перемещение пера в одном направлении или подчеркивание содержимого. Полный список жестов определен в перечислении System.Windows.Ink. ApplicationGesture InkCanvas позволяет пользователю рисовать штриховые аннотации и также распознает предопределенные жесты InkCanvas удаляет штрих при щелчке. Если у пользователя есть перо, он может переключиться в этот режим, используя его обратный конец. (Определить текущий режим можно, проверив значение доступного только для чтения свойства ActiveEditingMode, а для изменения режима, используемого обратным концом пера, необходимо модифицировать свойство EditingModelnverted.) InkCanvas удаляет часть штриха (точку штриха) при щелчке на соответствующей его части InkCanvas позволяет пользователю выбирать элементы, хранящиеся в коллекции Children. Чтобы выбрать элемент, пользователь должен щелкнуть на нем или обвести "лассо" выбора вокруг него. Как только элемент выбран, его можно перемещать, изменять размер или удалять InkCanvas игнорирует ввод с помощью мыши или пера InkCanvas инициирует события при изменении режима редактирования (ActiveEditingModeChanged), обнаружении жеста в режимах GestureOnly или InkAndGesture (Gesture), рисовании штриха (StrokeCollected), стирании штриха (StrokeErasing и StrokeErased), а также при выборе элемента или изменении его в режиме Select (SelectionChanging, SelectionChanged, SelectionMoving,
114 Глава 3. Компоновка SelectionMoved, SelectionResizing и SelectionResized). События, оканчивающиеся на ing, представляют действие, которое начинается, но может быть отменено установкой свойства Cancel объекта EventArgs. В режиме Select элемент InkCanvas предоставляет довольно удобную поверхность проектирования для перетаскивания содержимого и различных манипуляций им. На рис. 3.21 показан элемент управления Button в InkCanvas, когда он был выбран (слева) и затем перемещен и увеличен (справа). plelnkCanvas EditingMode: Select • .-h e о Рис. 3.21. Перемещение и изменение размеров элемента в InkCanvas Как бы ни был интересен режим Select, он не совсем подходит для построения рисунков или диаграмм. Вы увидите лучший пример того, как создается поверхность рисования, в главе 14. Примеры компоновки Итак, исследованиям интерфейсов контейнеров компоновки WPF было уделено достаточное время. Теперь стоит взглянуть на несколько завершенных примеров компоновки. Это даст лучшее представление о том, как работают различные концепции компоновки WPF (такие как размер по содержимому, растягивание и вложение) в реальных окнах приложений. Колонка настроек Контейнеры компоновки, подобные Grid, значительно упрощают задачу создания общей структуры окна. Например, рассмотрим окно с настройками, показанное на рис. 3.22. Это окно располагает свои индивидуальные компоненты — метки, текстовые поля и кнопки — в табличной структуре. Создание этой таблицы начинается с определения строк и колонок сетки. Строки достаточно просты — размер каждой просто определяется по высоте содержимого. Это значит, что вся строка получит высоту самого большого элемента, которым в данном случае является кнопка Browse (Обзор) из третьей колонки. <Grid Margin=,3,10,3"> <Grid.RowDefinitions> <RowDefmition Height="Auto"></RowDefinition> <RowDefmition Height="Auto"></RowDefinition> <RowDefmition Height="Auto"></RowDefinition> <RowDefmition Height="Auto"></RowDefinition> </Grid.RowDefinitions>
Глава 3. Компоновка 115 1 TextBoxColumn Home: сД Network: e:\Shared Browse Web: c:\ i Browse j Secondary: c:\ i Browse Рис. 3.22. Настройки папки в колонке Далее необходимо создать колонки. Размер первой и последней колонки определяется так, чтобы уместить их содержимое (текст метки и кнопку Browse соответственно). Средняя колонка получает все оставшееся пространство, а это значит, что она будет расти при увеличении размера окна, предоставляя больше места, чтобы видеть выбранную папку. (Если хотите ограничить ее ширину, можете указать свойство MaxWidth при определении колонки, как это делается с индивидуальными элементами.) <Gnd.ColumnDef initions> <ColumnDefmition Width= <ColumnDefmition Width= <ColumnDefinition Width= </Gnd.ColumnDef initions> Совет. Контейнер Grid требует некоторого минимального пространства — достаточного, чтобы уместить полный текст метки, кнопку просмотра и несколько пикселей в средней колонке, отобразив текстовое поле. Если установить размеры включающего окна меньше этих, то некоторое содержимое будет усечено. Как всегда, чтобы предотвратить такую ситуацию, имеет смысл использовать свойства окна MinWidth и MinHeight. При наличии базовой структуры остается просто разместить элементы в правильных ячейках. Однако также потребуется тщательно продумать поля и выравнивание. Каждый элемент нуждается в базовом поле (подходящим значением для него будет 3 единицы), чтобы создать небольшой отступ от края окна. Вдобавок метка и текстовое поле должны быть центрированы по вертикали, потому что их высота меньше, чем у кнопки Browse. И, наконец, текстовое поле должно использовать режим автоматической установки размера, растягиваясь для того, чтобы уместить всю колонку. Ниже показана разметка, которая понадобится для определения первой строки сетки. <Label Grid.Row=" Grid.Column=" Margin=" VerticalAlignment="Center">Home:</Label> <TextBox Grid.Row=" Grid.Column="l" Margin=" Height="Auto" VerticalAlignment="Center"></TextBox> <Button Grid.Row=" Grid.Column=" Margin=" Padding=">Browse</Button> "Auto"></ColumnDefinition> " *"></ColumnDef inition> "Auto"></ColumnDefinition> </Grid>
116 Глава 3. Компоновка Эту разметку можно повторить, добавив все строки, при этом просто увеличивая значение атрибута Grid.Row. Один факт, который не сразу очевиден, связан с тем, насколько гибким является это окно благодаря использованию элемента управления Grid. Ни один из индивидуальных элементов — метки, текстовые поля и кнопки — не имеют жестко закодированных позиций и размеров. В результате сетку легко модифицировать, просто изменяя элементы ColumnDef inition. Более того, если вы добавите строку, которая имеет более длинный текст метки (что потребует расширения первой колонки), вся сетка будет откорректирована автоматически, сохраняя согласованность, в том числе и для добавленных строк. Если понадобится добавить элементы между существующими строками, такие как разделительные линии между разными частями окна, можно сохранить те же колонки, но использовать свойство ColumnSpan для растяжения единственного элемента на большую область. Динамическое содержимое Как демонстрирует показанная выше колонка настроек, окна, использующие контейнеры компоновки WPF, легко поддаются изменениям и адаптации по мере развития приложения. И преимущество этой гибкости проявляется не только во время проектирования. Это также ценное приобретение, если нужно отобразить содержимое, изменяющееся динамически. Примером может служить локализованный текст — текст, который отображается в пользовательском интерфейсе и нуждается в переводе на разные языки для разных географических регионов. В приложениях старого стиля, опирающихся на координатные системы, изменение текста может разрушить внешний вид окна — в частности, потому, что краткие предложения английского языка становятся существенно длиннее на многих других языках. Даже если элементам позволено изменять свои размеры, чтобы вместить больший текст, это может нарушить общий баланс окна. На рис. 3.23 показано, как можно избежать этих неприятностей, если разумно применять контейнеры компоновки WPF: В этом примере пользовательский интерфейс имеет опции краткого и длинного текста. Когда используется длинный текст, кнопки, содержащие текст, изменяют свой размер автоматически, расталкивая соседнее содержимое. И поскольку кнопки измененного размера разделяют один и тот же контейнер компоновки (в данном случае — колонку таблицы), весь раздел пользовательского интерфейса изменяет свой размер. В результате получается, что кнопки сохраняют согласованный размер — размер самой большой из них. This »s a test that demonstrates how buttons adapt themselves to fit the content they contain when they aren't explicitly sized This behavior makes localization much easier I Ctos* 1 I ' I Рис. 3.23. Самонастраивающееся окно ШшЩ Layout This is a test that demonstrates how buttons adapt themselves to fit the N**1 content they contain when they aren t explicitly sized. This behavior makes Show Long Text localization much easier. Go to tne Next Window ->
Глава 3. Компоновка 117 Чтобы заставить это работать, окно оснащено таблицей из двух колонок и двух строк. Колонка слева принимает кнопки изменяемого размера, а колонка справа — текстовое поле. Нижняя строка используется для кнопки Close (Закрыть). Она находится в той же таблице, поэтому изменяет свой размер вместе с верхней строкой. Ниже показана полная разметка: <Grid> <Grid.RowDefinitions> <RowDefmition Height="*"></RowDefinition> <RowDefmition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefmition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <StackPanel Grid.Row=" Grid.Column="> <Button Name="cmdPrev" Margin=0,10,10,3">Prev</Button> <Button Name="cmdNext" Margin=0,3,10,3">Next</Button> <CheckBox Name="chkLongText" Margin=0,10,10,10" Checked="chkLongText_Checked" Unchecked="chkLongText_Unchecked"> Show Long Text</CheckBox> </StackPanel> <TextBox Grid.Row=" Grid.Column="l" Margin=,10,10,10" TextWrapping="WrapWithOverflow" Grid.RowSpan=">This is a test that demonstrates how buttons adapt themselves to fit the content they contain when they aren't explicitly sized. This behavior makes localization much easier.</TextBox> <Button Grid.Row="l" Grid.Column=" Name="crndClose" Margin=0,3,10,10">Close</Button> </Grid> Модульный пользовательский интерфейс Многие контейнеры компоновки успешно "заливают" содержимое в доступное пространство — так поступают StackPanel, DockPanel и WrapPanel. Одно из преимуществ этого подхода заключается в том, что он позволяет строить действительно модульные интерфейсы. Другими словами, можно подключать разные панели с соответствующими разделами пользовательского интерфейса, которые должны отображаться, и пропускать те, которые в данный момент не нужны. Все приложение будет должным образом подстраиваться, подобно портальному сайту в Интернете. На рис. 3.24 показан пример. Здесь в WrapPanel помещается несколько отдельных панелей. Пользователь может выбрать те панели, которые должны быть видимыми, используя флажки в верхней части окна. На заметку! Хотя для панели компоновки можно установить фон, определить границу вокруг нее нельзя. В этом примере данное ограничение преодолено за счет помещения каждой панели в оболочку элемента Border, очерчивающего точные размеры. Поскольку другие панели скрыты, оставшиеся реорганизуют себя, заполняя доступное пространство (и порядок, в котором они объявлены). На рис. 3.25 показана другая организация панелей. Чтобы скрыть или показать индивидуальные панели, нужен небольшой фрагмент кода, обрабатывающего щелчки на флажках. Хотя модель обработки событий WPF пока еще детально не рассматривалась (этой теме посвящена глава 5), забегая вперед, скажем, что трюк состоит в установке свойства Visibility: panel.Visibility = Visibility.Collapsed;
118 Глава 3. Компоновка ModularCcntent | у Pan«U J Pan*2 •/ Pane*3 / Pan* Pagel Pag«2 Thu rs a test of a text box that contains wrapped text Рис. 3.24. Серии панелей в WrapPanel ModularContent . ■ Pane»2 Pagel . Th« is a test of a text box that contains wrapped text ' Рис. 3.25. Сокрытие некоторых панелей Свойство Visibility — это часть базового класса UIElement, и потому поддерживается почти всеми объектами, которые помещаются в окно WPF. Оно принимает одно из трех значений перечисления System.Windows.Visibility, описанных в табл. 3.6. Таблица 3.6. Значения перечисления Visibility Значение Описание visible Элемент появляется в окне в нормальном виде Collapsed Элемент не отображается и не занимает места Hidden Элемент не отображается, но место за ним резервируется. (Другими словами, там, где он должен появиться, отображается пустое пространство.) Эта установка удобна, если вы хотите скрывать и показывать элементы, не меняя компоновки и относительного положения элементов в остальной части окна На заметку! Свойство Visibility можно использовать для динамической подгонки вариантов интерфейса. Например, можно создать сворачиваемую панель, отображаемую сбоку окна. Все, что потребуется сделать для этого — поместить содержимое этой панели в какой-то контейнер компоновки и соответствующим образом устанавливать его свойство Visibility. Остальное содержимое будет автоматически реорганизовано, чтобы заполнить доступное пространство. Резюме В этой главе был представлен детальный обзор новой модели компоновки WPF и показано, как размещать элементы в стеках, сетках и других структурах. Мы построили более сложные компоновки, используя вложенные комбинации контейнеров компоновки, добавив GridSplitter для создания разделенных окон изменяемого размера. Особое внимание было уделено причинам, вызвавшим все эти значительные изменения, а именно — преимуществам, которые получаются при поддержке, расширении и локализации пользовательского интерфейса. История с компоновкой далека от завершения. В следующих главах будет демонстрироваться множество новых примеров, в которых контейнеры компоновки применя-
Глава 3. Компоновка 119 ются для организации групп элементов. Также будет рассказано о нескольких дополнительных средствах, позволяющих организовать содержимое окна. • Специализированные контейнеры. Border, ScrollViewer и Expander предоставляют возможность создания содержимого, имеющего рамки, допускающего прокрутку и которое может быть свернуто. В отличие от панелей компоновки, эти контейнеры могут содержать только один фрагмент содержимого. Однако их легко использовать в сочетании с панелями компоновки, чтобы получить нужный эффект. В главе 6 эти контейнеры демонстрируются в действии. • Контейнер Viewbox. Нужен способ изменения размера графического содержимого (такого как графические изображения и векторная графика)? Viewbox — это еще один специализированный контейнер, который поможет в этом, обладая встроенным масштабированием. Первое знакомство с Viewbox произойдет в главе 12. • Компоновка текста. В WPF доступны инструменты для компоновки крупных блоков стилизованного текста. Можно использовать плавающие фигуры и списки, применять выравнивание, колонки и изощренную технологию переносов, чтобы получить замечательно изящный результат. В главе 28 показано, как это делается.
ГЛАВА 4 Свойства зависимости Каждый программист, работающий с .NET, знаком со свойствами (property) и событиями (event), которые являются основными компонентами объектной абстракции .NET. Почти никто не предполагал, что WPF — технология пользовательских интерфейсов — изменит какой-либо из этих основополагающих компонентов. Однако именно это и произошло. В данной главе вы узнаете, как WPF заменяет обычные свойства .NET высокоуровневым компонентом — свойствами зависимости (dependency property). Свойства зависимости более эффективно используют память и поддерживают дополнительные возможности: уведомления об изменениях и наследование значений свойств (возможность распространить стандартные значения вниз по дереву элементов). Они являются также основой для ряда ключевых возможностей WPF, например, анимации, привязки данных и стилей. К счастью, изменился только внутренний механизм, и свойства зависимости можно считывать и устанавливать точно так же, как и традиционные свойства .NET. На последующих страницах вы подробно ознакомитесь со свойствами зависимости. Вы научитесь определять их, регистрировать и использовать. Вы также узнаете, какие возможности они поддерживают и какие задачи решают. На заметку! Для освоения свойств зависимости необходим большой объем теоретических сведений, а вам это может показаться ни к чему (по крайней мере сейчас). Если вам не терпится приступить к созданию приложений, то можете перейти к последующим главам и вернуться сюда тогда, когда захотите более глубоко разобраться в механизме работы WPF и создавать свойства зависимости самостоятельно. Свойства зависимости Свойства зависимости являются совершенно новой, значительно более полезной, реализацией свойств. Без них вы не сможете работать с основными средствами WPF, такими как анимация, привязка данных и стили. Большинство свойств у элементов WPF являются свойствами зависимости. Во всех примерах, которые были приведены до настоящего момента, вы использовали свойства зависимости, даже не подозревая об этом. Это объясняется тем, что свойства зависимости разработаны таким образом, чтобы с ними можно было работать как с обычными свойствами. И все же свойства зависимости не являются обычными свойствами. Лучше всего представлять себе эти свойства как обычные (определяемые в .NET обычным образом), но с дополнительным набором возможностей WPF. В концептуальном отношении поведение свойств зависимости не отличается от поведения обычных свойств, однако pea-
Глава 4. Свойства зависимости 121 лизованы они по-другому. Причина проста: производительность. Если бы разработчики WPF просто внесли дополнительные возможности в систему свойств .NET, то им пришлось бы создать сложный и громоздкий слой для вашего кода. Рядовые свойства не могут поддерживать все характеристики свойств зависимости, не перегружая при этом систему. Свойства зависимости являются специфическим детищем WPF. Однако в библиотеках WPF они всегда заключены в оболочки обычных процедур свойств .NET. Это позволяет использовать их обычным образом даже в том коде, который не имеет понятия о системе свойств зависимости WPF. На первый взгляд странно, что новая технология упакована в старую, однако только так WPF может изменить такой фундаментальный ингредиент, как свойства, не нарушая структуру остального мира .NET Определение свойства зависимости Свойства зависимости приходится создавать гораздо реже, чем использовать. Тем не менее, существует множество причин, по которым вам придется создавать собственные свойства зависимости. Очевидно, они будут являться ключевым ингредиентом при создании пользовательского элемента WPF Но они понадобятся и тогда, когда необходимо добавить привязку данных, анимацию или какую-то другую возможность WPF во фрагмент кода, который иначе не смог бы поддерживать их. Создать свойство зависимости не очень сложно, хотя к синтаксису нужно привыкнуть. Он полностью отличается от синтаксиса обычного свойства .NET На заметку! Свойства зависимости можно добавлять только к объектам зависимости — классам, порожденных от DependencyObject. К счастью, большинство ключевых компонентов инфраструктуры WPF косвенно порождены от DependencyObject. Наиболее очевидным примером такого порождения являются элементы. Сначала нужно определить объект, который будет представлять свойство. Это экземпляр класса DependencyProperty. Информация о свойстве должна быть доступна постоянно и, возможно, даже другим классам (как обычно для элементов WPF). По этой причине объект DependencyProperty следует определить как статическое поле в связанном классе. Например, класс FrameworkElement определяет свойство Margin, доступное всем элементам. Конечно, Margin — это свойство зависимости. Это означает, что оно определяется в классе FrameworkElement следующим образом: public class FrameworkElement : UIElement, ... { public static readonly DependencyProperty MarginProperty; } Принято соглашение, что поле, представляющее свойство зависимости, имеет имя обычного свойства с добавлением слова Property в конце. Таким образом можно отделить определение свойства зависимости от имени самого свойства. Поле определено с ключевым словом readonly — это означает, что его значение можно задать только в статическом конструкторе для класса FrameworkElement, но это уже следующий шаг. Регистрация свойства зависимости Определение объекта DependencyProperty является лишь первым шагом. Чтобы его можно было задействовать, необходимо зарегистрировать свойство зависимости в
122 Глава 4. Свойства зависимости WPF. Это нужно сделать до использования данного свойства в коде, поэтому определение должно быть выполнено в статическом конструкторе связанного класса. WPF гарантирует, что объекты DependencyProperty не будут создаваться напрямую, так как класс DependencyObject не имеет общедоступного конструктора. Экземпляр DependencyObject может быть создан только посредством статического метода DependencyProperty.Register (). WPF также гарантирует невозможность изменения объектов DependencyProperty после их создания, т.к. все члены DependencyProperty доступны только для чтения, а их значения должны быть заданы в виде аргументов в методе Register (). В следующем фрагменте кода приведен пример создания DependencyProperty. Здесь класс FrameworkElement использует статический конструктор для инициализации MarginProperty: static FrameworkElement () { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata( new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure); MarginProperty = DependencyProperty.Register("Margin", typeof(Thickness), typeof(FrameworkElement), metadata, new ValidateValueCallback(FrameworkElement.IsMarginValid)); } Регистрация свойства зависимости осуществляется в два этапа. Сначала создается объект FrameworkPropertyMetadata, который указывает, какие службы вы хотите использовать со свойством зависимости (например, поддержку привязки данных, анимацию и ведение журнала). Затем свойство регистрируется, для чего вызывается метод DependencyProperty.Register(). Здесь нужно определить несколько ключевых ингредиентов: • имя свойства (в данном примере это Margin); • тип данных, используемый свойством (в данном примере это структура Thickness); • тип, которому принадлежит это свойство (в данном примере это класс FrameworkElement); • объект FrameworkPropertyMetadata с дополнительными параметрами свойства (необязательно); • обратный вызов, при котором производится проверка правильности свойства (необязательно). С первыми тремя ингредиентами все вроде бы ясно. Более интересными являются объект FrameworkPropertyMetadata и обратный вызов проверки. Объект FrameworkPropertyMetadata используется для настройки дополнительных возможностей создаваемого свойства зависимости. Большая часть свойств класса FrameworkPropertyMetadata представляет собой обычные логические флаги, которые устанавливаются для активации этих возможностей (по умолчанию все эти флаги имеют значения false). Но некоторые из них являются обратными вызовами, которые указывают на пользовательские методы, созданные для выполнения конкретных задач. Одно из таких свойств — FrameworkPropertyMetadata.DefaultValue —устанавливает стандартное значение, которое WPF будет применять при первоначальной инициализации свойства. В табл. 4.1 приведены все свойства FrameworkPropertyMetadata.
Глава 4. Свойства зависимости 123 Таблица 4.1. Свойства класса FrameworkPropertyMetadata Имя Описание Af fectsArrange, Af fectsMeasure, AffectsParentArrange и Af fectsParentMeasure Af fectsRender BindsTwoWayByDefault Inherits IsAmmationProhibited IsNotDataBindable Journal SubPropertiesDoNotAffectRender DefaultUpdateSourceTrigger DefaultValue CoerceValueCallback PropertyChangedCallback Если имеет значение true, то свойство зависимости может влиять на расположение смежных элементов (или родительского элемента) во время этапа измерения и этапа расстановки в операции компоновки. Например, свойство зависимости Margin заносит в Af fectsMeasure значение true — это означает, что при изменении полей элементов контейнер компоновки должен повторить этап измерения, чтобы определить новое размещение элементов Если имеет значение true, то свойство зависимости может влиять на внешний вид элемента, что требует перерисовки элемента Если имеет значение true, то свойство зависимости будет использовать не одностороннюю (по умолчанию), а двухстороннюю привязку данных. Однако при создании привязки можно явно указать ее поведение Если имеет значение true, то значение свойства зависимости распространяется по дереву элементов и может наследоваться вложенными элементами. Например, наследуемым свойством зависимости является Font: если указать его для элемента самого высокого уровня, то оно наследуется вложенными элементами, если не будет явно перекрыто собственными параметрами шрифта Если имеет значение true, то свойство зависимости нельзя использовать в анимации Если имеет значение true, то значение свойства зависимости нельзя устанавливать в выражении привязки Если имеет значение true, то в страничном приложении значение свойства зависимости будет сохранено в журнале (история посещенных страниц) Если имеет значение true, то WPF не будет выполнять перерисовку объекта при изменении одного из его под- свойств (свойства свойства) Устанавливает стандартное значение для свойства Binding.UpdateSourceTrigger, когда это свойство используется в выражении привязки. Свойство UpdateSourceTrigger определяет момент применения изменений привязанного значения. Свойство UpdateSourceTrigger можно установить вручную при создании привязки Устанавливает стандартное значение для свойства зависимости Обеспечивает обратный вызов, который пытается "исправить" значение свойства перед его проверкой Обеспечивает обратный вызов, который выполняется при изменении значения свойства
124 Глава 4. Свойства зависимости В последующих разделах обратные вызовы для проверки правильности и некоторые параметры метаданных будут рассмотрены более подробно. Кроме того, далее в книге будут встречаться примеры с демонстрацией их работы. Но вначале нужно разобраться, как обеспечить точно такой же доступ к свойству зависимости, как и к обычному свойству .NET. Добавление оболочки свойства На завершающем этапе создания свойства зависимости его нужно оформить в виде традиционного свойства .NET. Однако процедуры обычного свойства извлекают или задают значение приватного поля, а процедуры свойства WPF используют методы GetValueO и SetValueO, определенные в классе DependencyObject. Например: public Thickness Margin { set { SetValue(MarginProperty, value); } get { return (Thickness)GetValue(MarginProperty); } } При создании оболочки свойства необходимо включить только вызов методов SetValueO и GetValueO, как в предыдущем примере. Не нужно добавлять какой-то дополнительный код для проверки значений, генерации событий и т.п. Это связано с тем, что другие средства WPF могут обходить оболочку свойства и напрямую обращаться к методам SetValue () и GetValue (). (В качестве примера можно привести синтаксический анализ скомпилированного XAML-файла во время выполнения.) Методы SetValueO и GetValueO являются общедоступными. На заметку! Оболочка свойства не предназначена для проверки правильности данных или генерации события. Однако в WPF есть возможность выполнить такой код — это обратные вызовы свойства зависимости. Проверку следует выполнять в DependencyProperty. ValidateValueCallback, как было показано в предыдущем примере, а генерация событий — в FrameworkPropertyMetadata.PropertyChangedCallback, как будет показано в следующем разделе. Теперь у вас есть полностью готовое свойство зависимости, которое можно задавать подобно любому другому свойству .NET с помощью оболочки свойства: myElement.Margin = new Thickness E); Здесь есть одна особенность. Свойства зависимости подчиняются строгим правилам предшествования для определения текущих значений. Даже если вы не устанавливаете непосредственно свойство зависимости, оно уже может иметь значение: возможно, оно было присвоено во время привязки данных, определении стиля или анимации, или было унаследовано через дерево элементов. (О правилах предшествования речь пойдет в следующем разделе.) Однако если установить значение напрямую, оно перекроет существующее значение. Через некоторое время после этого вам может понадобиться удалить локально заданное значение, т.е. чтобы значение свойства было определено так, как если бы оно не было явно установлено. Понятно, что это невозможно сделать, присваивая новое значение. Придется воспользоваться другим методом, унаследованным от DependencyObject — методом ClearValue(). Вот как он работает: myElement .ClearValue (FrameworkElement .MarginProperty);
Глава 4. Свойства зависимости 125 Как WPF использует свойства зависимости На страницах этой книги вы увидите, что свойства зависимости необходимы самым разным средствам WPF. Тем не менее, все эти средства имеют две ключевых возможности, поддерживаемых каждым свойством зависимости — это уведомление об изменении и динамическое разрешение значений. Как ни странно, свойства зависимости не генерируют автоматически события, чтобы дать знать об изменении значения свойства. Вместо этого они запускают защищенный метод OnPropertyChangedCallback(). Он передает информацию двум службам WPF (привязка данных и триггеры) и вызывает метод PropertyChangedCallback, если он определен. Другими словами, если вы хотите выполнить действие в случае изменения свойства, у вас есть два варианта: можно создать привязку, которая использует значение свойства (см. главу 8), или написать триггер, который автоматически изменяет другое свойство или запускает анимацию (см. главу 11). Однако свойства зависимости не дают обобщенный способ запуска некоторого кода в ответ на изменение свойства. На заметку! При работе с созданным вами элементом управления можно воспользоваться механизмом обратного вызова свойства, чтобы реагировать на изменения свойства и даже генерировать событие. Многие обычные элементы управления используют этот прием для свойств, которые соответствуют информации, заданной пользователем. Например, элемент TextBox имеет событие TextChanged, a ScrollBar — событие ValueChanged. Элемент управления может реализовывать подобную функцию с помощью объекта PropertyChangedCallback, однако по соображениям производительности эта возможность в свойствах зависимости закрыта от обычного доступа. Второй возможностью, которая определяет характер работы свойств зависимости, является динамическое разрешение значения. Это означает, что при извлечении значения из свойства зависимости WPF учитывает несколько факторов. Такое поведение и объясняет название этих свойств — по сути, свойство зависимости зависит от нескольких поставщиков свойств, каждый из которых имеет свой уровень приоритета. При извлечении значения из свойства система свойств WPF выполняет ряд действий, которые дают окончательное значение. Сначала она определяет базовое значение свойства, учитывая следующие факторы, перечисленные в порядке возрастания приоритета. 1. Значение по умолчанию (задается объектом FrameworkPropertyMetadata). 2. Унаследованное значение (если установлен флаг FrameworkPropertyMetadata. Inherits, и где-то выше в иерархии элементу было присвоено значение). 3. Значение из стиля темы (см. главу 18). 4. Значение из стиля проекта (см. главу 11). 5. Локальное значение (то есть значение, заданное непосредственно в этом объекте с помощью кода или XAML). Как показывает этот список, при непосредственном присваивании значения переопределяется целая иерархия значений. Иначе значение берется из ближайшего применимого элемента выше в списке. На заметку! Одно из преимуществ этой системы состоит в ее значительной экономичности. Если значение свойства не было задано локально, WPF извлечет его значение из стиля, другого элемента, либо стандартного значения. При этом не требуется выделять память для хранения значения. Оценить эту экономичность можно, если добавить на форму несколько кнопок. Каждая кнопка имеет десятки свойств, которые вообще не занимают память, если заданы посредством одного из этих механизмов.
126 Глава 4. Свойства зависимости WPF придерживается приведенного выше списка, чтобы определить базовое значение свойства зависимости. Однако это базовое значение не обязательно является конечным значением, которое выбирается из свойства. Это связано с тем, что WPF рассматривает несколько других поставщиков, которые могут изменить значение свойства. Ниже описан четырехшаговый процесс, с помощью которого WPF определяет значение свойства. 1. Определяется базовое значение (как описано выше). 2. Если свойство задается выражением, производится вычисление этого выражения. На данный момент WPF поддерживает два типа выражений: привязка данных (см. главу 8) и ресурсы (см. главу 10). 3. Если данное свойство предназначено для анимации, применяется эта анимация. 4. Выполняется метод CoerceValueCallback для "корректировки" значения. (Применение этой техники описано ниже, в разделе "Проверка свойств".) По сути, свойства зависимости жестко связаны с небольшим набором служб WPF Если бы в данной инфраструктуре этого не было, они могли бы породить излишнюю сложность и добавить значительные накладные расходы. Совет. В будущих версиях WPF к свойствам зависимости будут добавлены дополнительные службы. При разработке пользовательских элементов (об этом речь пойдет в главе 18) вы, скорее всего, будете использовать свойства зависимости для большинства (если не всех) их общедоступных свойств. Совместно используемые свойства зависимости Некоторые классы совместно используют одно и то же свойство зависимости, даже если они имеют отдельные иерархии классов. Например, TextBlock. Font Family и Control.FontFamily указывают на одно и то же статическое свойство зависимости, которое определено в свойстве TextElement.FontFamilyProperty класса TextElement. Статический конструктор TextElement регистрирует свойство, а статические конструкторы TextBlock и Control просто повторно используют его, вызывая метод DependencyProperty. AddOwneг(): TextBlock.FontFamilyProperty = TextElement.FontFmamilyProperty.AddOwner(typeof(TextBlock)); Такую технологию можно применять при создании собственных пользовательских классов (если нужное свойство еще не определено в базовом классе — иначе вы получите его готовым). Можно также использовать перегрузку метода AddOwner (), что позволит определить обратный вызов проверки и новый объект FrameworkPropertyMetadata, который будет применяться только к этому новому использованию свойства зависимости. Повторное использование свойств зависимости может привести в WPF к некоторым странным побочным эффектам, особенно в стилях. Например, если применить стиль для автоматического задания свойства TextBlock.FontFamily, то это повлияет и на свойство Control.FontFamily, поскольку "за кулисами" оба класса используют одно и то же свойство зависимости. Действие этого феномена будет продемонстрировано в главе 12. Прикрепляемые свойства зависимости В главе 2 было рассказано о специальном типе свойства зависимости, называемом прикрепляемым свойством. Прикрепляемое свойство (attached property) — это свойство зависимости, которым управляет система свойств WPF. Его отличительной чертой яв-
Глава 4. Свойства зависимости 127 ляется тот факт, что прикрепляемое свойство применяется к классу, отличному от того, в котором оно определено. Наиболее характерный пример прикрепляемых свойств можно найти в контейнерах компоновки, описанных в главе 4. Например, класс Grid определяет прикрепляемые свойства Row и Column, которые задаются для содержащихся элементов и показывают их расположение. Точно так же класс DockPanel определяет прикрепляемое свойство Dock, a Canvas — прикрепляемые свойства Left, Right, Top и Bottom. Для определения прикрепляемого свойства используется метод RegisterAttachedO, aHeRegister(). Вот пример регистрации свойства Grid.Row: FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata ( 0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged)); Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof (int), typeof(Grid) , metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative)); Как и при использовании обычного свойства зависимости, здесь также можно определить объект FrameworkPropertyMetadata и ValidateValueCallback. При создании прикрепляемого свойства оболочка свойства .NET не определяется. Это связано с тем, что прикрепляемые свойства могут быть заданы в любом объекте зависимости. Например, свойство Grid.Row может быть задано в объекте Grid (если один объект Grid вложен в другой) или в каком-то другом элементе. Вообще-то свойство Grid.Row может быть задано в элементе, даже если это не экземпляр Grid — и даже если в дереве объектов вообще нет ни одного объекта Grid. Вместо применения оболочки свойства .NET для прикрепляемых свойств требуется пара статических методов, которые могут быть вызваны для установки и получения значения свойства. Эти методы используют знакомые вам методы SetValueO и GetValueO (унаследованные от класса DependencyObject). Статические методы должны иметь имена наподобие БеЬИмяСвойстваО и СеЬИмяСвойства{). Ниже показаны статические методы, реализующие прикрепляемое свойство Grid.Row. public static int GetRow(UIElement element) { if (element == null) { throw new ArgumentNullException (...); } return (int)element.GetValue(Grid.RowProperty); } public static void SetRow(UIElement element, int value) { if (element == null) { throw new ArgumentNullException (...); } element.SetValue(Grid.RowProperty, value); } А вот пример, позиционирующий элемент в первой строке сетки с помощью кода: Grid.SetRow(txtElement, 0); Но метод SetValueO или GetValueO можно вызвать напрямую, в обход статических методов: txtElement.SetValue(Grid.RowProperty, 0) /
128 Глава 4. Свойства зависимости Метод SetValue () имеет одну странную особенность. Хотя XAML не позволяет применять его, в коде можно использовать перегруженную версию метода SetValue (), чтобы прикрепить значение к любому свойству зависимости, далее если это свойство не определено как прикрепляемое. Например, вполне допустимым является следующий код: ComboBox comboBox = new ComboBox(); comboBox.SetValue(PasswordBox.PasswordCharProperty, "*") ; Здесь значение свойства PasswordBox. PasswordChar задается для объекта ComboBox, хотя PasswordBox.PasswordCharProperty зарегистрировано как обычное свойство зависимости, а не как прикрепляемое свойство. Это действие не изменит способ работы ComboBox — ведь код внутри ComboBox не будет искать значение свойства, о существовании которого ничего не известно — однако вы можете в своем коде работать со значением PasswordChar. Этот трюк применяется нечасто, но он позволяет глубже понять принцип работы системы свойств WPF и демонстрирует ее великолепную расширяемость. Он показывает также, что хотя прикрепляемые свойства регистрируются не как обычные свойства зависимости, а с помощью другого метода, WPF не проводит между ними различий. Единственным отличием является поведение синтаксического анализатора XAML. Если не зарегистрировать свойство как прикрепляемое, вы не сможете менять его значение в остальных элементах разметки. Проверка свойств При определении любого свойства необходимо учитывать возможность неверного задания его значения. Работая с обычными свойствами .NET, можно попытаться перехватить этот момент в методе установки значения. Но в случае свойств зависимости этот способ неприменим, т.к. свойство можно установить напрямую, с помощью метода SetValue () из системы свойств WPF. Вместо этого в WPF предусмотрены два способа защиты от неверно установленных значений: • ValidateValueCallback. Этот обратный вызов может принимать или отбрасывать новые значения. Обычно он применяется для обнаружения очевидных ошибок, которые нарушают ограничения свойства. Его можно передать в качестве аргумента при вызове метода DependencyProperty.Register (). • CoerceValueCallback. Этот обратный вызов может изменять введенные значения на более приемлемые. Обычно он применяется для обработки конфликтов между значениями свойств зависимости, установленных для одного и того же объекта. Такие значения могут быть верны порознь, но противоречить друг другу. Для использования этого обратного вызова передайте его в качестве аргумента конструктора при создании объекта FrameworkPropertyMetadata, который затем передается методу DependencyProperty.Register(). Вот как работают все эти части, когда приложение пытается установить значение свойства зависимости: 1. Вначале метод CoerceValueCallback получает возможность изменить полученное значение (обычно чтобы оно не противоречило значениям других свойств) или возвратить значение DependencyProperty.UnsetValue, которое вообще запрещает применение значения.
Глава 4. Свойства зависимости 129 2. Затем запускается метод ValidateValueCallback. Он возвращает true, что означает принятие значения как верного, или false, что означает отказ от применения значения. В отличие от CoerceValueCallback, метод ValidateValueCallback не имеет доступа к самому объекту, в котором выполняется попытка изменения свойства — то есть он не может анализировать значения других свойств. 3. И, наконец, если оба предыдущих этапа закончились успешно, запускается метод PropertyChangedCallback. В это время можно сгенерировать событие изменения, если нужно обеспечить уведомление других классов. Обратный вызов проверки Как уже было сказано, метод DependencyProperty. Register () принимает необязательный параметр с обратным вызовом проверки: MarginProperty = DependencyProperty.Register("Margin", typeof(Thickness), typeof(FrameworkElement), metadata, new ValidateValueCallback (FrameworkElement. IsMarginValid) ) ; Его можно использовать для выполнения проверки, которая обычно помещается в процедуру установки значения свойства. Этот обратный вызов должен указывать на метод, который принимает объект в качестве параметра и возвращает логическое значение. Значение true означает, что объект верен, a false — что неверен, и его следует отбросить. Проверка свойства FrameworkElement. Mar gin не представляет особого интереса, т.к. она зависит от внутреннего метода Thickness.IsValid(). Этот метод проверяет верность объекта Thickness в текущем контексте (когда он представляет краевое поле). Например, можно создать полностью корректный объект Thickness, который все-таки не годится для установки ширины поля — допустим, из-за отрицательных размеров. И если объект Thickness не может представлять краевое поле, то свойство IsMarginValid возвращает false: private static bool IsMarginValid(object value) { Thickness thicknessl = (Thickness) value; return thicknessl.IsValid(true, false, true, false); } У обратных вызовов проверки есть одно ограничение: они являются статическими методами и поэтому не имеют доступа к проверяемому объекту. В вашем распоряжении имеется лишь применяемое значение. Конечно, это облегчает их повторное использование, но делает невозможным создание процедуры проверки, которая должна учитывать другие свойства. Классический пример — элемент со свойствами Maximum и Minimum. Понятно, что нельзя присваивать Maximum значение, которое меньше Minimum. Но такую проверку невозможно выполнить в обратном вызове проверки, т.к. при каждом вызове доступно только одно свойство. На заметку! Эту проблему рекомендуется решать с помощью приведения значений. Приведение — это шаг, который выполняется перед проверкой и позволяет изменить значение, чтобы оно стало более приемлемым (например, увеличить значение Maximum, чтобы оно стало не меньше Minimum), или вообще запретить изменение. Шаг приведения выполняется с помощью другого обратного вызова, прикрепленного к объекту FrameworkPropertyMetadata, но этот объект будет описан в следующем разделе.
130 Глава 4. Свойства зависимости Обратный вызов приведения Метод CoerceValueCallback вызывается с помощью объекта FrameworkProperty Metadata. Вот, например: FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum); DependencyProperty.Register("Maximum", typeof(double), typeof(RangeBase), metadata); Метод CoerceValueCallback позволяет обрабатывать зависящие друг от друга свойства. К примеру, у объектов ScrollBar имеются свойства Maximum, Minimum и Value; все они унаследованы от класса RangeBase. Один из способов сохранить их согласованность — использование приведения свойств. Например, при установке значения Maximum должно быть выполнено такое приведение, чтобы это значение было не меньше Minimum: private static object CoerceMaximum(DependencyObject d, object value) { RangeBase basel = (RangeBase)d; if (((double) value) < basel.Minimum) { return basel.Minimum; } return value; } Другими словами, если значение, применяемое к свойству Maximum, меньше, чем Minimum, то используется значение Minimum, а не применяемое значение. Обратите внимание: методу CoerceValueCallback передаются два параметра: применяемое значение и объект, к которому оно применяется. Аналогичное приведение можно выполнить при установке значения Value: оно не должно выходить за границы, определяемые значениями Minimum и Maximum, и это можно проверить с помощью следующего кода: internal static object ConstrainToRange(DependencyObject d, object value) { double newValue = (double)value; RangeBase basel = (RangeBase) d; double minimum = basel.Minimum; if (newValue < minimum) { return minimum; } double maximum = basel.Maximum; if (newValue > maximum) { return maximum; } return newValue; } Свойство Minimum вообще не выполняет приведение значения. Вместо этого при его изменении выполняется метод PropertyChangedCallback, который запускает приведение значений Maximum и Value:
Глава 4. Свойства зависимости 131 private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { RangeBase basel = (RangeBase)d; basel.CoerceMaximum(RangeBase.MaximumProperty); basel.CoerceValue(RangeBase.ValueProperty); } Аналогично, при установке и приведении значения Maximum дополнительно выполняется приведение свойства Value: private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { RangeBase basel = (RangeBase)d; basel.CoerceValue(RangeBase.ValueProperty); basel.OnMaximumChanged((double) e.OldValue, (double)e.NewValue); } В результате получается, что при применении конфликтующих значений наибольший приоритет имеет Minimum, затем Maximum (с возможной корректировкой по значению Minimum), а затем — Value (с возможной корректировкой по значениям Maximum и Minimum). Цель этой несколько запутанной последовательности действий состоит в обеспечении того, что свойства объекта ScrollBar можно задавать в произвольном порядке без возникновения ошибки. Это важно на этапе инициализации, когда создается окно для документа XAML. Все управляющие элементы WPF гарантируют, что их свойства можно устанавливать в любой последовательности, и это никак не повлияет на их поведение. Если внимательно просмотреть приведенный выше код, то могут возникнуть вопросы. Например, рассмотрим следующий код: ScrollBar bar = new ScrollBar (); bar.Value = IOC- bar. Minimum = 1; bar.Maximum = 200; Сразу после создания объекта ScrollBar он имеет параметры Value = О, Minimum = О и Maximum = 1. После выполнения второй строки значение Value приводится к 1 (т.к. по умолчанию свойство Maximum имеет значение 1). Но в четвертой строке кода происходит интересное явление. При изменении свойства Maximum оно запускает приведение свойств Miniтити Value. Но это приведение действует на значения, указанные первоначально. То есть локальное значение 100 все еще хранится где-то в системе свойств зависимости WPF, и теперь, когда оно стало допустимым, его можно применить к свойству Value. Значит, в результате выполнения этой одной строки изменились два свойства. Вот более подробный протокол происходящего: ScrollBar bar = new ScrollBar (); bar.Value = 100; // (Сейчас bar.Value возратит 1.) bar.Minimum = 1; // (bar.Value все так же возвращает 1.) bar.Maximum = 200; // (А теперь bar.Value возвратит 100.)
132 Глава 4. Свойства зависимости Это поведение не зависит от времени задания свойства Maximum. Например, если при загрузке окна задать значение Value равным 100, а потом при щелчке пользователя на какой-то кнопке установить значение Maximum, то в этот момент значение Value восстановится до величины 100. (Единственный способ предотвратить это состоит в установке другого значения или удалении локального значения, которое было применено с помощью метода ClearValueO,наследуемого всеми элементами от класса Dependency Object.) Это поведение обусловлено системой разрешения свойств WPF, о которой уже было рассказано выше. WPF хранит точное локальное значение, установленное внутри, но она вычисляет, чему должно быть равно это значение (с учетом приведения и некоторых других соображений), при чтении свойства. На заметку! Программисты со стажем, которые работали с Windows Forms, могут помнить интерфейс ISupportlnitialize, который применялся для решения аналогичных задач при инициализации свойств: тогда последовательность изменений свойства оформлялась в виде пакетного процесса. В принципе, ISupportlnitialize можно использовать и с WPF (и синтаксический анализатор XAML не возражает против этого), но лишь немногие элементы WPF задействуют эту технику. Вместо нее подобные задачи рекомендуется решать с помощью приведения значений. Приведение удобнее по целому ряду причин Например, в отличие от ISupportlnitialize, оно решает и другие задачи, которые могут возникнуть при применении неправильного значения с помощью привязки данных или анимации. Резюме В этой главе мы детально рассмотрели свойства зависимости WPF. Сначала было показано, как определяются и регистрируются свойства зависимости, а затем — как они подключаются к остальным службам WPF. В следующей главе будет рассмотрена еще одна возможность WPF, которая расширяет базовую часть традиционной инфраструктуры .NET — маршрутизируемые события. Совет. Один из лучших способов изучить механизм WPF — просмотр кода для базовых элементов WPF, таких как Button, UIElement и FrameworkElement. Одним из наиболее удобных инструментов для этого является Reflector, доступный по адресу http://www.red-gate.com/ products/reflector. Он позволяет увидеть определения свойств зависимости, просмотреть код инициализирующего их статического конструктора и даже узнать, как они используются в коде класса. Этот инструмент позволяет также получить аналогичную низкоуровневую информацию о маршрутизируемых событиях, которые будут описаны в следующей главе.
ГЛАВА 5 Маршрутизируемые события В предыдущей главе была описана созданная в WPF новая система свойств зависимости, которая усовершенствовала традиционные свойства .NET, повысив их производительность и интегрировав новые возможности, такие как привязка данных и анимация. В данной главе вы познакомитесь со вторым усовершенствованием: заменой обычных событий .NET на высокоуровневые маршрутизируемые события (routed event). Маршрутизируемые события — это события с большими транспортными возможностями: они могут туннелироваться вниз и распространяться пузырьками наверх по дереву элементов и по пути запускать обработчики событий. Маршрутизируемые события позволяют обработать событие в одном элементе (например, в метке), хотя оно возникло в другом (например, в изображении внутри этой метки). Как и в случае свойств зависимости, маршрутизируемые события можно употреблять и традиционным способом — подключив обработчик событий с нужной сигнатурой — но все равно необходимо понимать принципы их работы, чтобы задействовать все их возможности. В данной главе вы познакомитесь с системой событий WPF и научитесь запускать и обрабатывать маршрутизируемые события. После освоения основ вы рассмотрите семейство событий, которые предоставляют элементы WPF: события для инициализации, щелчков мышью, нажатий клавиш и мультипозиционных сенсорных экранов. Что нового? Свойства зависимости и маршрутизируемые события работают в WPF 4 так же, как и в предыдущих версиях. Но в WPF 4 появилась совершенно новая возможность: захват данных, вводимых с сенсорных устройств нового поколения, которые поддерживают несколько касаний (например, планшетных компьютеров с усовершенствованными сенсорными экранами). Эти устройства будут рассмотрены ниже в данной главе, в разделе "Сенсорный многопозиционный ввод". Знакомство с маршрутизируемыми событиями Каждый разработчик, работающий в .NET, знаком с понятием события: это сообщение, которое посылается объектом (например, элементом WPF) для уведомления кода о том, что произошло что-то важное. WPF дополняет модель событий .NET новой концепцией маршрутизации событий. Маршрутизация позволяет событию возникать в одном элементе, а генерироваться в другом: например, щелчок на кнопке панели инструментов генерируется в панели инструментов, а затем в содержащем эту панель окне, и только тогда передается на обработку коду.
134 Глава 5. Маршрутизируемые события Маршрутизация событий дает возможность писать лаконичный и* понятный код, который может обрабатывать события в наиболее удобном для этого месте. Она необходима также для работы с моделью содержимого WPF, позволяющей создавать простые элементы (например, кнопки) из десятков отдельных ингредиентов, каждый из которых имеет свой собственный набор событий. Определение, регистрация и упаковка маршрутизируемых событий Модель событий WPF очень похожа на модель свойств WPF. Как и свойства зависимости, маршрутизируемые события представляются статическими полями, доступными только для чтения, которые регистрируются в статическом конструкторе и оформляются в виде стандартного определения события .NET. Например, WPF-класс Button предлагает знакомое событие Click, являющееся потомком абстрактного класса ButtonBase. Ниже показано, как определяется и регистрируется это событие. public abstract class ButtonBase : ContentControl, ... { // Определение события public static readonly RoutedEvent ClickEvent; // Регистрация события static ButtonBase () { ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent( "Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof (ButtonBase)); } // Традиционная оболочка события public event RoutedEventHandler Click { add { base.AddHandler(ButtonBase.ClickEvent, value); } remove { base.RemoveHandler(ButtonBase.ClickEvent, value); } } } Свойства зависимости регистрируются посредством метода DependencyProperty. Register (), а для регистрации маршрутизируемых событий предназначен метод Eve nt Man age r. RegisterRoutedEvent (). При регистрации события нужно указать имя события, тип маршрутизации (об этом чуть позже), делегат, определяющий синтаксис обработчика события (в данном примере это RoutedEventHandler), и класс, которому принадлежит событие (в данном примере ButtonBase). Как правило, маршрутизируемые события упаковываются в обычные события .NET, чтобы сделать их доступными для всех языков .NET. Оболочка события добавляет и удаляет зарегистрированные вызывающие объекты с помощью методов AddHandlerO и RemoveHandler (), которые определены в базовом классе FrameworkElement и наследуются каждым элементом WPF.
Глава 5. Маршрутизируемые события 135 Совместное использование маршрутизируемых событий Как и в случае свойств зависимости, определение маршрутизируемых событий можно совместно использовать несколькими классами. К примеру, событие MouseUp используют два базовых класса: UIElement (начальная точка для обычных элементов WPF) HContentElement (начальная точка для элементов контента — отдельных частей содержимого, которые могут помещаться в документе потока). Событие MouseUp определено в классе System.Windows.Input.Mouse. Классы UIElement и ContentElement просто используют его с помощью метода RoutedEvent.AddOwner(): UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement)); Генерация маршрутизируемого события Конечно, как и любое событие, определяющий класс должен где-то сгенерировать маршрутизируемое событие. Г^е именно — это уже детали реализации. Однако следует помнить, что ваше событие не возбуждается через традиционную оболочку событий .NET. Вместо этого используется метод RaiseEvent (), наследуемый каждым элементом от класса UIElement. Ниже представлен соответствующий код класса ButtonBase: RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this); base.RaiseEvent(e) ; Метод RaiseEvent () отвечает за генерацию события для каждого вызывающего объекта, который был зарегистрирован с помощью метода AddHandler (). Поскольку этот метод является общедоступным, вызывающим объектам предоставляется выбор: они могут зарегистрироваться напрямую, с помощью метода AddHandler (), либо воспользоваться оболочкой события. (В следующем разделе продемонстрированы оба подхода.) В любом случае они будут уведомлены о вызове метода RaiseEvent (). Все события WPF придерживаются знакомого вам условия о сигнатурах событий, существующего в .NET. Первый параметр каждого обработчика события содержит ссылку на объект, который сгенерировал событие (отправитель). Второй параметр — объект EventArgs, объединяющий все дополнительные детали, которые могут понадобиться. Например, событие MouseUp предоставляет объект MouseEventArgs, который показывает, какая кнопка мыши была нажата при возникновении события: private void img_MouseUp(object sender, MouseButtonEventArgs e) { } В приложениях Windows Forms для многих событий обычно применялся базовый класс EventArg, если им не требовалось передавать дополнительную информацию. В приложениях WPF ситуация иная, поскольку в них поддерживается модель маршрутизируемых событий. Если событию не нужно посылать какую-либо дополнительную информацию, то в WPF оно использует класс RoutedEventArgs, который содержит некоторые сведения о маршрутизации события. Если событию нужно передать дополнительную информацию, оно использует более специализированный объект, порожденный от RoutedEventArgs (как MouseButtonEventArgs в предыдущем примере). Поскольку каждый класс аргумента события WPF порожден от RoutedEventArgs, каждый обработчик события WPF имеет доступ к информации о маршрутизации события. Обработка маршрутизируемого события Как было сказано в главе 2, прикрепить обработчик события можно несколькими способами. Чаще всего для этой цели добавляется атрибут события в разметку XAML.
136 Глава 5. Маршрутизируемые события Данный атрибут события получает имя события, которое нужно обрабатывать, а его значением является имя метода обработчика события. Вот пример, в котором этот синтаксис применяется для прикрепления обработчика imgMouseUp к событию MouseUp элемента Image: <Image Source="happyface.jpg" Stretch="None" Name="img" MouseUp="img_MouseUp" /> Обычно (хотя и не обязательно) имя метода обработчика события имеет вид ИмяЭлементаИмяСобытия. Если элемент не имеет определенного имени (возможно, потому, что с ним не нужно взаимодействовать в любом другом месте кода), попробуйте использовать имя, которое он мог бы иметь: <Button Click="cmdOK Click">OK</Button> Совет. Может возникнуть желание прикрепить событие к высокоуровневому методу, выполняющему задачу. Однако вы получите большую гибкость при наличии дополнительного уровня кода для обработки событий. Например, щелчок на кнопке cmdUpdate не вызовет непосредственно метод UpdateDatabaseO. Вместо этого будет вызван обработчик события — например, cmdUpdate_Click() —который может вызвать метод UpdateDatabaseO, а уже тот сделает всю работу. Этот принцип позволяет изменить местонахождение кода базы данных, заменить кнопку обновления другим элементом управления, привязать несколько элементов управления к одному и тому же процессу — и все это при полной возможности изменять в последующем пользовательский интерфейс. Если необходим более простой способ работы с действиями, которые могут запускаться из нескольких разных мест в пользовательском интерфейсе (кнопки панели инструментов, команды меню и т.д.), понадобится добавить средство команд WPF, описанное в главе 9. Событие можно соединить и с кодом. Вот эквивалент приведенного выше кода разметки XAML: img.MouseUp += new MouseButtonEventHandler(img_MouseUp); Этот код создает объект делегата, имеющий правильную сигнатуру для события (в данном случае это экземпляр делегата MouseButtonEventHandler) и указывающий на метод imgMouseUp (). Затем он добавляет делегат в список зарегистрированных обработчиков для события img.MouseUp. Язык С# разрешает применять более лаконичный синтаксис, явным образом создающий подходящий объект делегата: img.MouseUp += img_MouseUp; Подход с использованием кода полезен тогда, когда нужно динамически создать элемент управления и прикрепить обработчик события в некоторый момент существования окна. Для сравнения скажем, что события, захватываемые в XAML, всегда присоединяются при первом создании экземпляра объекта окна. Этот подход позволяет также упростить и рационализировать код XAML, что исключительно полезно, если предполагается совместно использовать его не с программистами, а, скажем, с художниками- дизайнерами. Недостатком является большой объем шаблонного кода, который загромождает кодовые файлы. Подход, продемонстрированный в предыдущем коде, основан на оболочке события, которая вызывает метод UIElement.AddHandler(), как показано в предыдущем разделе. Вы можете связать событие напрямую, самостоятельно вызвав метод UIElement. AddHandler(), например: img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
Глава 5. Маршрутизируемые события 137 При использовании этого подхода всегда приходится создавать подходящий тип делегата (например, MouseButtonEventHandler). Нельзя создать объект делегата неявно, как при захвате события через оболочку свойства, поскольку метод UIElement. AddHandler () поддерживает все события WPF и не знает, какой тип делегата вы хотите использовать. Некоторые разработчики предпочитают использовать имя класса, в котором определено событие, а не имя класса, сгенерировавшего событие. Ниже показан эквивалентный синтаксис, наглядно демонстрирующий определение события Mouse Up Event в классе UIElement. img.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp)); На заметку! Выбор подхода зависит от ваших предпочтений. Хотя у второго подхода есть недостаток: он не дает ясного представления о том, что класс Image обеспечивает событие MouseUpEvent. Такой код можно неправильно понять и предположить, что он прикрепляет обработчик, предназначенный для обработки MouseUpEvent во вложенном элементе. Об этой технологии мы поговорим в разделе "Прикрепляемые события" далее в этой главе. Если понадобится открепить обработчик события, то это можно сделать только в коде — например, с помощью операции -=: lmg.MouseUp -= img_MouseUp; Либо можно использовать метод UIElement.RemoveHandler(): lmg.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp)); Технически возможно прикрепить один и тот же обработчик к одному и тому же событию более одного раза. Обычно это происходит из-за ошибки при кодировании. (В этом случае обработчик события будет запущен несколько раз.) После удаления обработчика события, который был подключен дважды, событие все-таки запустит этот обработчик, но только один раз. Маршрутизация событий Как было сказано в предыдущей главе, многие элементы управления в WPF являются элементами управления содержимым, которые могут иметь разный тип и разный объем вложенного содержимого. Например, можно собрать графическую кнопку из отдельных графических элементов, создать метку, которая будет совмещать текст и рисунки, или поместить содержимое в специальный контейнер, чтобы его можно было прокручивать или сворачивать. И такой процесс "вкладывания" можно повторять столько раз, сколько уровней нужно получить. При этом возникает интересный вопрос. Например, предположим, что имеется метка, в которой имеется панель StackPanel, содержащая два текстовых блока и изображение: <Label BorderBrush="Black" BorderThickness="l"> <StackPanel> <TextBlock Margin="> Image and picture label </TextBlock> <Image Source="happyface.jpg" Stretch="None" /> <TextBlock Margin="> Courtesy of the StackPanel </TextBlock> </StackPanel> </Label>
138 Глава 5. Маршрутизируемые события Как вам уже известно, каждый ингредиент, помещаемый в окно WPF, так или иначе является наследником класса UIElement, включая Label, StackPanel, TextBlock и Image. Класс UIElement определяет несколько ключевых событий. Например, каждый класс, являющийся потомком UIElement, обеспечивает события MouseUp и MouseDown. А теперь подумайте, что произойдет при щелчке на изображении в такой метке. Понятно, что при этом возникнут события Image.MouseDown и Image.MouseUp. А если вам нужно обрабатывать все щелчки на метке одинаковым образом? То есть неважно, где щелкнул пользователь: на изображении, на тексте или на пустом месте в области метки. В любом из этих случаев нужно реагировать на щелчок с помощью одного и того же кода. Понятно, что к событиям MouseDown и MouseUp каждого элемента можно привязать один и тот же обработчик, однако это может загромоздить код и усложнить сопровождение разметки. WPF предлагает более удобное решение с помощью модели маршрутизируемых событий. Маршрутизируемые события бывают трех видов: • Прямые (direct) события подобны обычным событиям .NET. Они возникают в одном элементе и не передаются в другой. Например, прямым является событие MouseEnter, которое возникает, когда указатель мыши наводится на элемент. • Пузырьковые (bubbling) события поднимаются по иерархии содержания. Например, пузырьковым событием является MouseDown. Оно возникает в элементе, на котором был произведен щелчок, потом передается от этого элемента к родителю, затем к родителю этого родителя, и т.д., пока WPF не достигнет вершины дерева элементов. • Туннелируемые (tunneling) события опускаются по иерархии содержания. Они позволяют предварительно просматривать (и, возможно, останавливать) событие, прежде чем оно дойдет до подходящего элемента управления. Например, PreviewKeyDown позволяет перехватить нажатие клавиши, сначала на уровне окна, а затем в более специфических контейнерах, вплоть до элемента, содержавшего фокус в момент нажатия клавиши. При регистрации маршрутизируемого события с помощью метода EventManager. RegisterEventO ему передается значение из перечисления RoutingStrategy, которое задает необходимое поведение для события. Поскольку события MouseUp и MouseDown являются пузырьковыми событиями, вы уже можете определить, что произойдет в примере с составной меткой. При щелчке на довольном смайлике событие MouseDown возникнет в следующем порядке: 1. Image.MouseDown 2. StackPanel.MouseDown 3. Label.MouseDown После того как событие MouseDown возникнет в метке, оно передается следующему элементу управления (в данном случае это сетка Grid для разметки вмещающего окна), а затем его родителю (окно). Окно находится на самом верху иерархии содержания и в самом конце в последовательности пузырькового распространения события. Здесь последний шанс обработать пузырьковое событие наподобие MouseDown. Если пользователь отпускает кнопку мыши, в такой же последовательности возникает событие MouseUp. На заметку! В главе 24 вы научитесь создавать страничные WPF-приложения. В них контейнером самого верхнего уровня является не окно, а экземпляр класса Page.
Глава 5. Маршрутизируемые события 139 Пузырьковые события не обязательно обрабатывать в одном месте: например, ничто не мешает обрабатывать события MouseDown и MouseUp на каждом уровне. Однако, как правило, для каждой задачи выбирается наиболее подходящая маршрутизация событий. Класс RoutedEventArgs При обработке пузырькового события параметр отправителя содержит ссылку на последнее звено в цепочке. Например, если событие перед обработкой всплывает от изображения до метки, то параметр отправителя будет ссылаться на объект метки. В некоторых случаях требуется знать, где первоначально произошло событие. Эту информацию, а также другие подробности, можно получить из свойств класса RoutedEventArgs (которые перечислены в табл. 5.1). Поскольку все классы аргументов событий WPF являются наследниками RoutedEventArgs, эти свойства доступны в любом обработчике события. Таблица 5.1. Свойства класса RoutedEventArgs Имя Описание Source OriginalSource RoutedEvent Handled Указывает, какой объект сгенерировал событие. Если речь идет о событии клавиатуры, то это элемент управления, имевший фокус ввода в момент возникновения события (например, когда была нажата клавиша). В случае события мыши это самый верхний элемент под указателем мыши в момент возникновения события (например, когда был произведен щелчок кнопкой мыши) Указывает, какой объект первоначально сгенерировал событие. Как правило, совпадает с Source. Однако в некоторых случаях OriginalSource спускается глубже по дереву объектов, чтобы дойти до внутреннего элемента, являющегося частью элемента более высокого уровня. Например, если вы щелкнете кнопкой мыши близко к границе окна, то получите объект Window в качестве источника события и Border в качестве первоначального источника. Это объясняется тем, что window состоит из отдельных меньших элементов. Чтобы разобраться с этой сборной моделью более детально (и узнать, как ее можно изменить), обратитесь к главе 17, в которой рассказывается о шаблонах элементов управления Предоставляет объект RoutedEvent для события, сгенерированного вашим обработчиком события (например, статический объект UIElement.MouseUpEvent). Эта информация бывает полезна при обработке разных событий одним и тем же обработчиком Позволяет остановить процесс пузырькового распространения или тун- нелирования события. Если элемента управления заносит в свойство Handled значение true, событие прекращает продвижение и не будет возникать в любых других элементах. (В разделе "Обработка заблокированного события" будет описан один способ обхода этого ограничения.) Пузырьковые события На рис. 5.1 показано простое окно, которое демонстрирует пузырьковое распространение события. Если щелкнуть на какой-либо части метки, события будут возникать в порядке, перечисленном на текстовой панели ниже. На рис. 5.1 приведен вид этого окна сразу после щелчка пользователя на изображении внутри метки. Событие MouseUp проходит пять уровней и останавливается на пользовательской форме BubbledLabelClick.
140 Глава 5. Маршрутизируемые события Чтобы получить эту форму, нужно связать изображение и каждый элемент, стоящий над ним в иерархии элементов, с одним и тем же обработчиком события — методом SomethingClicked(). Вот как это делается в XAML: <Window х:Class="RoutedEvents.BubbledLabelClick" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="BubbledLabelClick" Height=59" Width=29" MouseUp="SomethingClieked" > <Grid Margin=" MouseUp="SomethingClicked"> <Grid.RowDefinitions> <RowDefinition Height="Auto"x/RowDefinition> <RowDefinition Height="*"x/RowDef inition> <RowDefinition Height="Auto"x/RowDefinition> <RowDefinition Height="Auto"x/RowDefinition> </Grid.RowDefinitions> <Label Margin=" Grid.Row=" HorizontalAlignment="Left" Background="AliceBlue" BorderBrush="Black" BorderThickness="l" МоиseUp="SomethingCliсked"> <StackPanel MouseUp="SomethingClieked"> <TextBlock Margin=" MouseUp="SomethingClicked"> Image and picture label</TextBlock> <Image Source="happyface.jpg" Stretch="None" MouseUp="SomethingClicked" /> <TextBlock Margin=" MouseUp="SomethingClicked"> Courtesy of the StackPanel</TextBlock> </StackPanel> </Label> <ListBox Grid.Row="l" Margin=" Name="lstMessages"x/ListBox> <CheckBox Grid.Row=" Margin=" Name="chkHandle"> Handle first event</CheckBox> <Button Grid.Row=" Margin=" Padding=" HorizontalAlignment="Right" Name="cmdClear" Click="cmdClear_Click">Clear List</Button> </Grid> </Window> Метод SomethingClicked() просто проверяет свойства объекта RoutedEventArgs и добавляет сообщение на текстовую панель: protected int eventCounter = 0; private void SomethingClicked(object sender, RoutedEventArgs e) ' { eventCounter++; string message = "#" + eventCounter.ToString() + ":\r\n" + " Sender: " + sender.ToString() + "\r\n" + " Source: " + e.Source + "\r\n" + " Original Source: " + e .OnginalSource; IstMessages.Items.Add(message); e.Handled = (bool)chkHandle.IsChecked; } На заметку! С технической точки зрения событие MouseUp предоставляет объект MouseButton EventArgs с дополнительной информацией о состоянии мыши в момент возникновения события. Однако класс MouseButtonEventArgs является наследником MouseEventArgs, который в свою очередь является наследником класса RoutedventArgs. Это позволяет использовать его при объявлении обработчика события (как показано здесь), если дополнительная информация о мыши не требуется.
Глава 5. Маршрутизируемые события 141 В этом примере есть еще один момент Если установить флажок chkHandle, метод SomethingClickedO присвоит свойству RoutedEventArgs.Handled значение true, что останавливает последовательность пузырькового распространения события сразу при его возникновении. Поэтому вы увидите в списке только первое событие, как показано на рис. 5.2. На заметку! Здесь нужно дополнительное приведение, т.к. свойство CheckBox.IsChecked является логическим значением, которое может принимать значение null (bool?, а не bool). Значение null представляет неопределенное состояние флажка, которое означает, что он и не установлен, и не сброшен. Эта особенность не используется в данном примере, поэтому достаточно простого приведения. Поскольку метод SomethingClickedO обрабатывает событие MouseUp, которое возникает в объекте Window, щелчки можно перехватывать в текстовой панели и на пустой поверхности окна. Однако событие MouseUp не возникает при щелчке на кнопке Clear (которая удаляет из текстовой панели все записи). Это связано с тем, что кнопке соответствует интересный фрагмент кода, который блокирует событие MouseUp и генерирует событие более высокого уровня Click. Одновременно флагу Handled присваивается значение true, что блокирует дальнейшее продвижение события MouseUp. Совет. В отличие от элементов управления Windows Forms, большинство элементов WPF не имеют события Click. Вместо этого у них есть более простые события MouseDown и MouseUp. Событие Click зарезервировано для кнопочных элементов управления ■. BubbledLabeClick Image and picture label Courtesy of the StackPanel •1: Sender. System Windows Controls.Image Source System.Windows.ControlsImage Original Source: Systerr..Windows.Controls.Image #2: Sender. System.Windows.ControlsStackPanel Source: System Windows.Contrcls.Image Original Source: System.Windows.Contrors.Image Sender System Windows Controls.LabeJ j Source System Windows Controls Image Original Source: System.Windows.ControlsImage *4 Sender System Windows.Controls.Grid Source: System.Windows.ControlsImage Original Source: System.Windows.Controlsimage #5: Sender: RouiedEvents BubbledlabelClick Source: System WindowsControls-Image Original Source: System.Windows.Controts.Image \ ] Handle first event Clear List Рис. 5.1. Пузырьковое распространение события после щелчка на изображении ■ BubbledLabelCiick ^i-ьЖШ Image and picture label •J Courtesy of the StackPanel = . Sender System.Windows.Controls Image Source: System Windows ControlsJmage Original Source: System .Windows.ControlsJmage V,;Hand|e first event Рис. 5.2. Пометка события как обработанного
142 Глава 5. Маршрутизируемые события Обработка заблокированного события Интересно, что существует способ получать события, которые отмечены как обработанные. Вместо прикрепления обработчика события посредством XAML следует использовать рассмотренный ранее метод AddHandler (). Этот метод имеет перегруженный вариант, который принимает логическое значение в третьем параметре. Если задать его равным true, вы получите событие, даже если для него был установлен флаг Handled: cmdClear.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp), true); Такое решение редко бывает удачным. Кнопка предназначена для блокирования события MouseUp по очень простой причине: чтобы избежать путаницы. Ведь в Windows принято, что "щелкнуть" на кнопке можно и с помощью клавиатуры, да еще несколькими способами. Если вы ошибочно будете обрабатывать в элементе Button событие MouseUp, а не события Click, то ваш код будет реагировать только на щелчки мышью, но не на эквивалентные клавиатурные действия. Прикрепляемые события Рассмотренная декоративная метка является довольно простым примером пузырькового распространения события, поскольку все элементы поддерживают событие MouseUp. Но многие элементы управления обладают собственными специальными событиями. Одним из таких примеров является кнопка: она добавляет событие Click, которое не определено ни в одном базовом классе. Здесь возникает интересный момент. Предположим, что стек кнопок упакован в элемент StackPanel, и необходимо обработать все щелчки на кнопках в одном обработчике события. Конечно, можно прикрепить события Click каждой кнопки к одному и тому же обработчику события. Однако событие Click поддерживает пузырьковое распространение событий, и это позволяет решить задачу более изящным способом. Все щелчки на кнопках можно обработать, реагируя на событие Click на более высоком уровне (например, на уровне элемента StackPanel). К сожалению, следующий — вроде бы очевидный — код работать не будет: <StackPanel Click="DoSomething" Margin="> <Button Name="cmdl">Command K/Button> <Button Name="cmd2">Command 2</Button> <Button Name="cmd3">Command 3</Button> </StackPanel> t Дело в том, что StackPanel не содержит событие Click, поэтому такой код вызовет ошибку во время синтаксического анализа XAML. Для решения этой задачи нужно использовать другой синтаксис с применением прикрепленных событий в виде ИмяКласса.ИмяСобытия. Вот исправленный вариант: <StackPanel Button.Click="DoSomething" Margin="> <Button Name="cmdl">Command K/Button> <Button Name="cmd2">Command 2</Button> <Button Name="cmd3">Command 3</Button> </StackPanel> Теперь обработчик события получит управление при щелчках на всех упакованных кнопках.
Глава 5. Маршрутизируемые события 143 На заметку! Событие Click определено в классе ButtonBase и наследуется классом Button. Если прикрепить обработчик события к ButtonBase.Click, то этот обработчик события будет использоваться при щелчке на любом элементе управления, порожденном от ButtonBase (включая классы Button, RadioButton и CheckBox). Но если прикрепить обработчик события к Button.Click, то он будет использоваться только для объектов Button. Прикрепляемое событие можно подключить и в коде, но тогда вместо операции += придется использовать метод UIElement.AddHandler(). Вот пример (здесь предполагается, что элемент StackPanel имеет имя pnlButtons): pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething)); Если несколько возможностей определить в обработчике события DoSomethingO, какая кнопка сгенерировала событие. Можно сравнить ее текст (возможны проблемы с локализацией) или ее имя (ненадежно, так как на этапе создания приложения невозможно перехватить ошибочно введенные имена). Лучше всего задать с помощью XAML у каждой кнопки свойство Name — тогда можно обратиться к соответствующему объекту посредством поля в классе окна и сравнить эту ссылку с отправителем события. Вот пример: private void DoSomething(object sender, RoutedEventArgs e) { if (sender == cmdl) { ... } else if (sender == cmd2) { ... } else if (sender == cmd3) { ... } } Существует еще один вариант: вместе с кнопкой отправить порцию информации, которую можно использовать в коде. Например, для каждой кнопки можно задать свойство Tag: <StackPanel Click="DoSomething" Margin="> <Button Name="cmdl" Tag="The first button.">Command K/Button> <Button Name="cmd2" Tag="The second button.">Command 2</Button> <Button Name="cmd3" Tag="The third button.">Command 3</Button> </StackPanel> После этого можно обращаться к свойству Tag в коде: private void DoSomething(object sender, RoutedEventArgs e) { object tag = ((FrameworkElement)sender).Tag; MessageBox.Show((string)tag); } Туннелируемые события Туннелируемые события работают так же, как и пузырьковые, но в обратном направлении. Например, если бы событие MouseUp было туннельным (а это не так), то при щелчке на изображении в примере с меткой событие MouseUp возникло бы сначала в окне, затем в элементе Grid, затем в StackPanel и так далее до достижения источника, т.е. изображения в метке. Туннелируемые события легко распознать: они начинаются на слово Preview. Более того, WPF обычно определяет события попарно. Это означает, что если имеется пузырьковое событие MouseUp, то, скорее всего, существует и туннелируемое событие PreviewMouseUp. Туннелируемые событие всегда возникает перед пузырьковым событием, как показано на рис. 5.3.
144 Глава 5. Маршрутизируемые события Туннельное событие PreviewMouseUp Корневой элемент (окно) S Промежуточный элемент Промежуточный элемент Источник события С Здесь ) ( ВОЗНИКЛО Л N последним ,-> Пузырьковое событие MouseUp Рис. 5.3. Туннелируемые и пузырьковые события * • Tunne»edKeyPress Image ana text label Type here: dj Sender RoutedEvents.TunnetedKeyPress Source: System. W»ndows.Controte TextBox Original Source: System.Winoows.Controls.TextBox Event: Keyboard PrevtewKeyDown #2: Sender System Windows Controte-Label Source System.Windows.Controls.TextBox Ongtnal Source: System. Windov».Contro»s. TextBox Event Keyboard.PreviewKeyDown #3 Sender System.WindowsControJsStackPanel Source: System.Windows.Controls TextBox Original Source System.Windows.Controls.TextBox Event Keyboard.PrevtewKeyOown #4: Sender System Windows Controls.DockPanel : Source: System.Windows.Controls TextBox Original Source: System.W»ndows.Controls.TextBox Event: Keyboard.PreviewKeyDown #5: Sender System Windows Controls TextBox Source System Windows ControlsTextBox Original Source: 5ystem.Windows.Controls.TextBox : Event Keyboard .PreviewKeyOown #6: Sender: System.Windows-Controls.TextBox Source: System Windows Controls TextBox Original Source System.Windows.Contrors.TextBox Event: Keyboard.KeyDown ] Handle first event Интересный момент: если пометить тун- нелируемое событие как обработанное, то пузырьковое событие не возникнет Это связано с тем, что оба события совместно используют один и тот же экземпляр класса RoutedEventArgs. Туннелируемые события полезны, если нужно выполнить предварительную обработку, связанную с определенными нажатиями клавиш, или отфильтровать некоторые события мыши. На рис. 5.4 показан результат проверки туннелирования на примере события PreviewKeyDown. Если нажать клавишу, когда фокус находится в текстовом поле, событие возникает сначала в этом поле, а затем спускается по иерархии. И если на каком-то этапе пометить событие PreviewKeyDown как обработанное, то пузырьковое событие Key Down не возникнет. Совет. Будьте аккуратны с пометкой туннелируе- мого события как обработанного. В зависимости от конструкции управляющего элемента, это может помешать ему обработать собственное (соответствующее пузырьковое) событие, чтобы выполнить какое-то действие или обновить свое состояние. Рис. 5.4. Туннелируемое нажатие клавиши
Глава 5. Маршрутизируемые события 145 Определение стратегии маршрутизации события Понятно, что разные стратегии маршрутизации влияют на способ использования событий А как определить, какой тип маршрутизации использует данное событие? С туннелируемыми событиями все просто. В соответствии с соглашениями, принятыми в .NET, туннелируемое событие всегда начинается со слова Preview (например, PreviewKeyDown). Однако похожего механизма различения пузырьковых и прямых событий не существует. Разработчикам, применяющим WPF, лучше всего найти описание события в документации по Visual Studio. В разделе "Routed Event Information" указываются статическое поле события, тип маршрутизации и сигнатура события. Эту же информацию можно получить программным способом, проверив статическое поле для события Например, свойство ButtonBase.ClickEvent.RoutingStrategy содержит перечислимое значение, которое сообщает, какой тип маршрутизации использует событие Click. События WPF Теперь вы знаете о том, как работают события WPF, и можно приступить к рассмотрению самых разнообразных событий, на которые вы можете реагировать в своем коде. Каждый элемент имеет много разнообразных событий, однако наиболее важные события обычно делятся на пять следующих категорий: • События времени существования. Возникают при инициализации, загрузке или выгрузке элемента. • События мыши. Возникают в результате действий мыши. • События клавиатуры. Возникают в результате действий клавиатуры (например, нажатие клавиши). • События пера. Возникают в результате использования пера (стилуса), которое заменяет мышь в планшетных ПК. • События одновременного касания. Возникают в результате прикасания к многопозиционному сенсорному экрану одним или несколькими пальцами. Поддерживаются только в Windows 7. Вместе события мыши, клавиатуры, пера и касания известны как события ввода. События времени существования Все элементы генерируют события при создании и освобождении. Эти события можно использовать для инициализации окна. События времени существования перечислены в табл. 5.2; все они определены в классе FrameworkElement. Таблица 5.2. События времени существования всех элементов Имя Описание Initialized Возникает после создания экземпляра элемента и установки его свойств в соответствии с разметкой XAML В этот момент элемент уже инициализирован, но другие части окна могут еще быть не инициализированными. Кроме того, еще не применены стили и привязка данных. Свойство Islnitialized имеет значение true. Данное событие является обычным событием .NET, а не маршрутизируемым Loaded Возникает после завершения инициализации всего окна и применения стилей и привязки данных. Это последний этап перед прорисовкой элемента В этот момент свойство IsLoaded имеет значение true Unloaded Возникает после освобождения элемента: или из-за закрытия содержащего его окна, или из-за удаления из окна данного элемента
146 Глава 5. Маршрутизируемые события Чтобы понять, как связаны между собой события Initialized и Loaded, полезно рассмотреть процесс прорисовки. FrameworkElement реализует интерфейс I Support Initialize, который предоставляет два метода управления процессом инициализации. Первый из них, Beginlnit(), вызывается сразу после создания экземпляра элемента. После этого вызова интерпретатор XAML устанавливает все свойства элемента (и добавляет любое содержимое). Второй метод, Endlnit(), вызывается после завершения инициализации, когда возникает событие Iniitialized. На заметку! Это несколько упрощенное описание. Интерпретатор XAML самостоятельно вызывает методы Beginlnit() и Endlnit(), как и должно быть. Однако если создать элемент вручную и добавить его в окно, то вы вряд ли будете использовать этот интерфейс. В этом случае элемент сгенерирует событие Initialized сразу после добавления его в окно, непосредственно перед событием Loaded. При создании окна каждая ветвь элементов инициализируется снизу вверх. Это означает, что глубоко вложенные элементы инициализируются до того, как будут инициализированы их контейнеры. Когда возникает событие Initialized, это означает, что дерево элементов от текущего элемента и ниже полностью инициализировано. Однако элемент, содержащий данный элемент, скорее всего, не инициализирован, и нет оснований предполагать, что инициализирована любая другая часть окна. После инициализации каждого элемента он размещается в контейнере, обрабатывается стилями и при необходимости привязывается к источнику данных. После возбуждения события Initialized для окна пора переходить к следующему этапу. После завершения процесса инициализации возникает событие Loaded. Оно распространяется в порядке, обратном событию Initialized: сначала событие Loaded генерируется во вмещающем окне, а затем его генерируют остальные вложенные элементы. Когда событие Loaded будет сгенерировано во всех элементах, окно станет видимым, и в нем будут прорисованы все элементы. События времени существования, перечисленные в табл. 5.2 — это еще не все. Содержащее окно имеет собственные события времени существования. Они перечислены в табл. 5.3. Таблица 5.3. События времени существования класса Window Имя Описание Sourcelnitialized Возникает при получении свойства HwndSource (но перед тем, как окн