WPF
WINDOWS PRESENTATION FOUNDATION В .NET 3.5 С ПРИМЕРАМИ НА C# 2008 ДЛЯ ПРОФЕССИОНАЛОВ ВТОРОЕ ИЗДАНИЕ
WPF_2_title.indd 1
30.05.2008 14:54:52
Pro
WPF
in C# 2008 Windows Presentation Foundation with .NET 3.5 SECOND EDITION Matthew MacDonald
WPF_2_title.indd 2
30.05.2008 14:54:53
WPF
WINDOWS PRESENTATION FOUNDATION В .NET 3.5 С ПРИМЕРАМИ НА C# 2008 ДЛЯ ПРОФЕССИОНАЛОВ ВТОРОЕ ИЗДАНИЕ Мэтью Мак-Дональд
Москва · Санкт-Петербург · Киев 2008
WPF_2_title.indd 3
30.05.2008 14:54:53
ББК 32.973.26-018.2.75 М15 УДК 681.3.07
Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского Я.П. Волковой, Д.Я. Иваненко, Н.А. Мухина Под редакцией Ю.Н. Артеменко По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу:
[email protected], http://www.williamspublishing.com
Мак-Дональд, Мэтью. М15 WPF: Windows Presentation Foundation в .NET 3.5 с примерами на C# 2008 для профессионалов, 2-е издание: Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2008. — 928 с. : ил. — Парал. тит. англ. ISBN 978-5-8459-1429-3 (рус.) Книга ведущего специалиста в области технологий .NET представляет собой учебное и справочное пособие по WPF, являющейся частью .NET 3.5, для разработчиков высококлассных приложений, которые ориентированы на Windows Vista (и Windows XP). В ней предлагается материал, касающийся как первоначальной инсталляции, так и проектирования и развертывания приложений для конечных пользователей. Глубина изложения материала превращает эту книгу в незаменимый источник информации для разработчиков. Подробно рассматриваются XAML, элементы управления, компоновка, реализация навигации, локализации и развертывания ClickOnce. Немалое внимание уделяется работе с документами, начиная с отображения и редактирования и заканчивая выводом на печать. Предлагаются уникальные сведения по рисованию собственных графических элементов, внедрению мультимедиа-средств и работе с трехмерной графикой, включая трансформации, спецэффекты и анимацию, а также техника построения многопоточных приложений и совместного использования WPF и Windows Forms. Книга рассчитана на программистов разной квалификации, а также будет полезна студентам и преподавателям дисциплин, связанных с программированием и разработкой для Windows и .NET. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства APress, Berkeley, CA. Authorized translation from the English language edition published by APress, Inc., Copyright © 2008 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 © 2008.
ISBN 978-5-8459-1429-3 (рус.) ISBN 978-1-59-059955-6 (англ.)
OT_Pro-WPF2.indd 4
© Издательский дом “Вильямс”, 2008 © by Matthew MacDonald, 2008
20.05.2008 16:05:15
Оглавление Введение Глава 1. Введение в WPF Глава 2. XAML Глава 3. Класс Application Глава 4. Компоновка Глава 5. Содержимое Глава 6. Свойства зависимостей и маршрутизируемые события Глава 7. Классические элементы управления Глава 8. Окна Глава 9. Страницы и навигация Глава 10. Команды Глава 11. Ресурсы Глава 12. Стили Глава 13. Фигуры, трансформации и кисти Глава 14. Классы Geometry, Drawing и Visual Глава 15. Шаблоны элементов управления Глава 16. Привязка данных Глава 17. Шаблоны данных, представления данных и поставщики данных Глава 18. Списки, деревья, панели инструментов и меню Глава 19. Документы Глава 20. Печать Глава 21. Анимация Глава 22. Звук и видео Глава 23. Трехмерная графика Глава 24. Пользовательские элементы Глава 25. Взаимодействие с Windows Forms Глава 26. Многопоточность и дополнения Глава 27. Развертывание ClickOnce Предметный указатель
Book_Pro_WPF-2.indb 5
19 25 45 76 92 130 149 187 221 248 289 315 343 359 397 429 471
525 569 611 661 689 737 760 801 846 868 901 917
19.05.2008 18:09:32
Содержание Об авторе О техническом редакторе Благодарности
18 18 18
Введение
19
На кого рассчитана эта книга Как организована эта книга Что необходимо для использования этой книги Примеры кода и URL-адреса От издательства
20 20 23 23 24
Глава 1. Введение в WPF
25
Графика в Windows DirectX: новый графический механизм Аппаратное ускорение и WPF WPF: высокоуровневый API Независимость от разрешения Эволюция WPF Windows Forms все еще в силе DirectX также в силе Silverlight Архитектура WPF Иерархия классов Резюме
25 25 26 28 29 35 37 38 38 39 41 44
Глава 2. XAML
45
Особенности XAML Графический интерфейс пользователя до WPF Варианты XAML Компиляция XAML Основы XAML Пространства имен XAML Класс отделенного кода Свойства и события в XAML Простые свойства и конвертеры типов Сложные свойства Расширения разметки Прикрепленные свойства Вложенные элементы Специальные символы и пробелы События Полный пример автоответчика Использование типов из других пространств имен Загрузка и компиляция XAML Только код Код и не компилированный XAML Код и компилированный XAML
46 46 48 48 49 50 51 53 55 56 57 58 60 62 64 65 66 68 69 71 72
Book_Pro_WPF-2.indb 6
19.05.2008 18:09:32
Содержание
7
Только XAML Резюме
74 75
Глава 3. Класс Application
76
Жизненный цикл приложения Создание объекта Application Наследование специального класса приложения Останов приложения События класса Application Задачи приложения Обработка аргументов командной строки Доступ к текущему приложению Взаимодействие между окнами Приложение одного экземпляра Резюме
76 76 77 79 80 82 82 83 84 86 91
Глава 4. Компоновка Понятие компоновки в WPF Философия компоновки WPF Процесс компоновки Контейнеры компоновки Простая компоновка с помощью StackPanel Свойства компоновки Выравнивание Поля Минимальный, максимальный и явный размеры WrapPanel и DockPanel
WrapPanel DockPanel Вложение контейнеров компоновки
Grid Тонкая настройка строк и колонок Объединение строк и колонок Разделенные окна Группы с общими размерами
UniformGrid Координатная компоновка с помощью Canvas Z-порядок
InkCanvas Примеры компоновки Колонка настроек Динамическое содержимое Модульный пользовательский интерфейс Резюме
92 92 93 94 94 96 98 99 100 100 103 104 105 106 108 111 112 113 117 119 120 121 121 123 124 126 127 129
Глава 5. Содержимое
130
Элементы управления содержимым Свойство Content Выравнивание содержимого Модель содержимого в WPF
130 132 134 135
Book_Pro_WPF-2.indb 7
19.05.2008 18:09:32
8
Содержание
Специализированные контейнеры Элемент управления ScrollViewer GroupBox и TabItem: элементы управления содержимым, имеющие заголовки Элемент управления Expander Декораторы Декоратор Border Декоратор Viewbox Резюме
136 136 139 141 144 145 146 148
Глава 6. Свойства зависимостей и маршрутизируемые события
149
Свойства зависимостей Определение и регистрация свойства зависимостей Как WPF использует свойства зависимостей Маршрутизированные события Определение и регистрация маршрутизируемых событий Присоединение обработчика событий Маршрутизация событий События WPF События времени существования События ввода Ввод с использованием клавиатуры Ввод с использованием мыши Резюме
150 150 159 161 161 163 164 172 173 175 175 181 186
Глава 7. Классические элементы управления
187
Класс Control Кисти фона и переднего плана Шрифты Указатели мыши Элементы управления содержимым Метки Кнопки Контекстные окна указателя Текстовые элементы управления Множество строк текста Выделение текста Другие возможности элемента управления TextBox Элемент управления PasswordBox Элементы управления списками Элемент управления ListBox Элемент управления ComboBox Элементы управления, основанные на диапазонах значений Элемент управления Slider Элемент управления ProgressBar Резюме
187 187 192 196 197 198 199 202 210 210 211 212 213 213 214 217 217 217 219 220
Глава 8. Окна
221
Класс Window Отображение окна Позиционирование окна
221 224 225
Book_Pro_WPF-2.indb 8
19.05.2008 18:09:32
Содержание
9
Сохранение и восстановление информации о местоположении окна Взаимодействие окон Владение окнами Модель диалогового окна Общие диалоговые окна Непрямоугольные окна Простое окно нестандартной формы Прозрачные окна с содержимым необычной формы Перемещение окон нестандартной формы Изменение размеров окон нестандартной формы Окна в стиле Vista Использование стеклянного эффекта Windows Vista Диалоговые окна задач и файлов Резюме
226 228 230 231 232 233 233 236 238 238 240 241 245 247
Глава 9. Страницы и навигация
248
Общие сведения о страничной навигации Страничные интерфейсы Простое страничное приложение с элементом NavigationWindow Класс Page Гиперссылки Размещение страниц во фрейме Размещение страниц в другой странице Размещение страниц в Web-браузере Хронология страниц Более детальное рассмотрение URI-адресов в WPF Хронология навигации Добавление специальных свойств Служба навигации Программная навигация События навигации Управление журналом Добавление в журнал специальных элементов Страничные функции Приложения XBAP Требования XBAP Создание приложения XBAP Развертывание приложения XBAP Обновление приложения XBAP Безопасность приложения XBAP Приложения XBAP с полным доверием Комбинирование приложений XBAP и автономных приложений Кодирование с обеспечением различных уровней безопасности Имитация диалоговых окон с помощью элемента управления Popup Вставка XBAP-приложения в Web-страницу Резюме
248 249 250 251 252 255 257 258 259 259 260 262 263 263 264 266 267 271 275 276 276 277 279 280 281 282 283 285 288 288
Глава 10. Команды
289
Общие сведения о командах Модель команд WPF
289 291
Book_Pro_WPF-2.indb 9
19.05.2008 18:09:33
10
Содержание
Интерфейс ICommand Класс RoutedCommand Класс RoutedUICommand Библиотека команд Выполнение команд Источники команд Привязки команд Использование множества источников команд Точная настройка текста команды Вызов команды напрямую Отключение команд Элементы управления со встроенными командами Усовершенствованные команды Специальные команды Использование одной и той же команды в разных местах Использование параметра команды Отслеживание и отмена команд Резюме
292 292 293 294 296 296 297 299 300 301 302 304 306 306 307 309 310 314
Глава 11. Ресурсы
315
Ресурсы сборки Добавление ресурсов Извлечение ресурсов Упакованные URI Ресурсы в других сборках Файлы содержимого Локализация Создание локализуемых пользовательских интерфейсов Подготовка приложения для локализации Процесс перевода Объектные ресурсы Коллекция ресурсов Иерархия ресурсов Статические и динамические ресурсы Неразделяемые ресурсы Получение доступа к ресурсам в коде Ресурсы приложения Ресурсы системы Организация ресурсов с помощью словарей ресурсов Разделение ресурсов между сборками Резюме
315 315 317 319 319 320 321 321 322 323 329 330 331 333 334 335 335 336 338 339 342
Глава 12. Стили
343
Основные сведения о стилях Создание объекта стиля Установка свойств Добавление обработчиков событий Несколько уровней стилей Автоматическое применение стилей по типу
343 346 347 349 350 352
Book_Pro_WPF-2.indb 10
19.05.2008 18:09:33
Содержание
11
Триггеры Простой триггер Триггер события Резюме
353 354 356 358
Глава 13. Фигуры, трансформации и кисти
359
Фигуры Классы фигур Rectangle и Ellipse Установка размеров и расположения фигур Пропорциональное определение размеров в Viewbox
Маски непрозрачности Битовые эффекты Размывание Выпуклые грани Тисненые грани Блики и тени Резюме
359 360 362 363 365 368 369 370 371 372 374 375 376 378 379 380 381 383 385 387 389 391 392 393 394 395 396
Глава 14. Классы Geometry, Drawing и Visual
397
Классы Path и Geometry Геометрии линий, прямоугольников и эллипсов Комбинирование фигур в GeometryGroup Комбинирование объектов Geometry и CombinedGeometry Кривые и прямые линии, представляемые с помощью PathGeometry Мини-язык описания геометрии Кадрирование геометрии Классы Drawing Отображение рисунка Экспорт рисунка Классы Visual Рисование объектов Visual Помещение визуальных объектов в оболочку элемента Проверка попадания Сложная проверка попадания Резюме
397 398 399 401 404 409 411 412 413 416 417 418 420 423 425 428
Line Polyline Polygon Концы линий и стыки линий Пунктиры Выравнивание пикселей Трансформации Трансформация фигур Трансформация элементов Лучшие кисти
LinearGradientBrush RadialGradientBrush ImageBrush “Черепичная” кисть ImageBrush VisualBrush
Book_Pro_WPF-2.indb 11
19.05.2008 18:09:33
12
Содержание
Глава 15. Шаблоны элементов управления
429
Что собой представляют логические и визуальные деревья Что собой представляют шаблоны Классы Chrome Разбиение элементов управления Создание шаблонов элементов управления Простая кнопка Привязка шаблонов Триггеры шаблонов Организация ресурсов шаблонов Рефакторизация шаблона элемента управления Button Использование шаблонов со стилями Автоматическое применение шаблонов Обложки, выбираемые пользователем Создание более сложных шаблонов Шаблоны, состоящие из множества частей Шаблоны элементов управления в ItemsControl Изменение полосы прокрутки Создание специального окна Простые стили Резюме
430 435 437 439 441 442 443 445 448 449 450 452 453 455 456 457 459 464 468 470
Глава 16. Привязка данных
471
Основы привязки данных Привязка к свойству элемента Создание привязки в коде Множественные привязки Направление привязки Обновления привязки Привязка объектов, не являющихся элементами Привязка пользовательских объектов к базе данных Построение компонента доступа к данным Построение объекта данных Отображение привязанного объекта Обновление базы данных Уведомление об изменениях Привязка к коллекции объектов Отображение и редактирование элементов коллекции Вставка и удаление элементов коллекций Привязка объектов ADO.NET Привязка к выражению LINQ Преобразование данных Форматирование строк конвертером значений Создание объектов с конвертером значений Применение условного форматирования Оценка множественных свойств Проверка достоверности Проверка достоверности в объекте данных
471 472 475 476 479 481 483 486 486 489 490 492 493 494 495 498 499 501 504 505 508 510 511 512 513 514 515
ExceptionValidationRule DataErrorValidationRule
Book_Pro_WPF-2.indb 12
19.05.2008 18:09:33
Содержание
13
Специальные правила проверки достоверности Реакция на ошибки проверки достоверности Получение списка исключений Отображение отличающегося индикатора ошибки Резюме
517 519 520 521 523
Глава 17. Шаблоны данных, представления данных и поставщики данных
525
Еще раз о привязке данных Шаблоны данных Отделение и многократное использование шаблонов Усовершенствованные шаблоны Варьирование шаблонов Селекторы шаблонов Шаблоны и выбор Селекторы стилей Изменение компоновки элемента Представления данных Извлечение объекта представления Фильтрация коллекций Фильтрация объекта DataTable Сортировка Группирование Создание представлений декларативным образом Навигация в представлении Поставщики данных Объект ObjectDataProvider Поставщик XmlDataProvider Резюме
525 527 529 530 532 534 538 543 545 545 547 547 550 551 552 557 558 562 563 566 568
Глава 18. Списки, деревья, панели инструментов и меню
569
Класс ItemsControl Элемент управления ComboBox Элемент управления ListBox с флажками или переключателями Класс ListView Создание столбцов с помощью GridView Изменение размера столбцов Шаблоны ячеек Создание специального представления Элемент управления TreeView Привязка данных к элементу управления TreeView Привязка элемента управления TreeView к объекту DataSet Оперативное создание узлов Меню Класс Menu Элементы меню Класс ContextMenu Разделители меню Панели инструментов и строки состояния Элемент управления ToolBar
570 573 576 578 580 582 582 585 592 593 596 597 600 600 601 603 604 605 605
Book_Pro_WPF-2.indb 13
19.05.2008 18:09:33
14
Содержание
Элемент управления StatusBar Резюме
609 610
Глава 19. Документы
611
Что собой представляют документы Потоковые документы Потоковые элементы Форматирование элементов содержимого Создание простого потокового документа Блочные элементы Встроенные элементы Взаимодействие с элементами программным образом Выравнивание текста Контейнеры потоковых документов, доступные только для чтения Изменение масштаба Страницы и колонки Загрузка компонентов из файла Печать Редактирование потокового документа Загрузка файла Сохранение файла Форматирование выделенного текста Получение отдельных слов Фиксированные документы Аннотации Классы аннотаций Включение службы аннотаций Создание аннотаций Проверка аннотаций Реагирование на изменения в аннотациях Сохранение аннотаций в фиксированном документе Настройка внешнего вида клейких листков Резюме
611 612 613 615 617 618 623 629 633 634 635 636 638 639 640 640 642 643 645 647 648 649 650 651 654 657 658 658 660
Глава 20. Печать
661
Основные сведения о печати Печать элемента Трансформирование распечатываемого вывода Печать элементов без их отображения Печать документа Манипулирование страницами в распечатке документа Специальная печать Печать с помощью классов уровня визуальных объектов Специальная печать с использованием множества страниц Параметры печати и управление Сохранение параметров печати Печать диапазонов страниц Управление очередью на печать Печать через XPS Создание документа XPS для предварительного просмотра перед печатью
661 662 664 667 667 670 673 673 675 680 681 681 682 685 686
Book_Pro_WPF-2.indb 14
19.05.2008 18:09:33
Содержание
15
Печать прямо на принтер через XPS Асинхронная печать Резюме
687 687 688
Глава 21. Анимация
689
Основы анимации WPF Анимация на основе таймера Анимация на основе свойств Базовая анимация Классы анимации Анимация в коде Одновременные анимации Время жизни анимации Класс TimeLine Декларативная анимация и раскадровки Раскадровка Триггеры событий Перекрывающиеся анимации Одновременные анимации Управление воспроизведением Отслеживание хода анимации Желательная частота кадров Еще раз о типах анимаций Анимация трансформаций Анимированные кисти Анимация ключевого кадра Анимация на основе пути Анимация на основе фрейма Резюме
689 690 691 692 692 695 700 700 701 704 705 705 709 710 711 715 717 719 720 724 727 730 732 736
Глава 22. Звук и видео
737
Воспроизведение WAV-аудио
737 738 739 740 740 743 743 744 744 746 748 749 751 752 755 755 757 759
SoundPlayer SoundPlayerAction Системные звуки
MediaPlayer MediaElement Программное воспроизведение аудио Обработка событий Воспроизведение аудио с помощью триггеров Воспроизведение множества звуков Изменение громкости, баланса, скорости и позиции воспроизведения Синхронизация анимации с аудио Воспроизведение видео Видео-эффекты Речь Синтез речи Распознавание текста Резюме
Book_Pro_WPF-2.indb 15
19.05.2008 18:09:33
16
Содержание
Глава 23. Трехмерная графика
760
Основы трехмерной графики Окно просмотра Трехмерные объекты Камера Дополнительные сведения о 3-D Затенение и нормали Более сложные фигуры Коллекции Model3DGroup Снова о материалах Отображение текстур Интерактивность и анимация Трансформации Вращения Полеты Шаровой манипулятор Проверка попадания курсора Двумерные элементы на трехмерных поверхностях Резюме
761 761 762 769 773 774 778 779 780 782 786 786 788 789 791 792 797 799
Глава 24. Пользовательские элементы
801
Что такое пользовательские элементы в WPF Построение базового пользовательского элемента управления Определение свойств зависимостей Определение маршрутизируемых событий Добавление кода разметки Использование элемента управления Поддержка команд Пристальный взгляд на UserControl Элементы управления, лишенные внешнего вида Рефакторинг кода указателя цвета Рефакторинг кода разметки указателя цвета Оптимизация шаблона элемента управления Стили, специфичные для темы, и стиль по умолчанию Расширение существующего элемента управления Маскируемые редактирующие элементы управления Синтаксис маски Реализация маскируемого текстового поля WPF Усовершенствование MaskedTextBox Пользовательские панели Двухшаговый процесс компоновки Клон Canvas Улучшенная WrapPanel Рисованные элементы Метод OnRender() Выполнение специального рисования Элемент, выполняющий специальное рисование Специальный декоратор Резюме
802 805 805 808 809 811 812 814 816 816 817 819 821 824 824 825 826 830 831 832 834 836 839 839 841 842 843 845
Book_Pro_WPF-2.indb 16
19.05.2008 18:09:34
Содержание
17
Глава 25. Взаимодействие с Windows Forms
846
Оценка способности к взаимодействию Отсутствующие средства в WPF Смешивание окон и форм Добавление форм к приложению WPF Добавление окон WPF в приложение Windows Forms Отображение модальных окон и форм Отображение немодальных окон и форм Визуальные стили элементов управления Windows Forms Классы Windows Forms, которые не нуждаются во взаимодействии с WPF Создание окон со смешанным содержимым Зазор между WPF и Windows Forms Размещение элементов управления Windows Forms в WPF WPF и пользовательские элементы управления Windows Forms Размещение элементов управления WPF в Windows Forms Ключи доступа, мнемоники и фокус Отображение свойств Резюме
846 847 849 850 850 851 851 852 853 856 857 858 861 862 864 865 867
Глава 26. Многопоточность и дополнения
868
Многопоточность Класс Dispatcher Класс DispatcherObject Класс BackgroundWorker Дополнения приложений Канал дополнений Приложение, использующее дополнения Взаимодействие с хостом Визуальные дополнения Резюме
868 869 870 872 880 880 885 893 897 900
Глава 27. Развертывание ClickOnce
901
Развертывание приложения Что такое ClickOnce Инсталляционная модель ClickOnce Ограничения ClickOnce Простая публикация ClickOnce Выбор местоположения Развернутые файлы Инсталляция приложения ClickOnce Обновление приложения ClickOnce Опции ClickOnce Версия публикации Обновления Опции публикации Резюме
901 902 903 904 905 906 910 910 912 913 914 914 916 916
Предметный указатель
917
Book_Pro_WPF-2.indb 17
19.05.2008 18:09:34
Об авторе Мэтью Мак-Дональд (Matthew MacDonald) — автор, преподаватель и обладатель статуса Microsoft MVP (Most Valuable Professional). Он регулярно пишет статьи в журналах по программированию и является автором более десятка книг по программированию с использованием платформы .NET, включая Pro .NT 2.0 Windows Forms and Custom Controls in C# (Apress, 2005 г.) и Pro ASP.NET 3.5 in C# 2008 (Microsoft ASP.NET 3.5 с примерами на C# 2008 для профессионалов, 2-е издание, ИД “Вильямс”, 2008 г.). Мэтью живет в Торонто вместе со своей женой и дочерью.
О техническом редакторе Кристоф Насарр (Christophe Nasarre) — архитектор и ведущий разработчик программного обеспечения в транснациональной компании Business Objects, специализирующейся на построении решений в области интеллектуальных ресурсов предприятия. В свободное время Кристоф пишет статьи для MSDN Magazine, MSDN и ASPToday. Начиная с 1996 г., он редактировал книги по Win32, COM, MFC, .NET и WPF. В 2007 г., Кристоф написал свою первую книгу Windows via C/C++, опубликованную издательством MSPress.
Благодарности Ни один автор не может написать книгу, не заручившись поддержкой небольшой армии полезных людей. Я глубоко признателен всей команде из издательства Apress, включая Софию Марчант (Sofia Marchant) и Лору Эстерман (Laura Esterman), которые сопровождали это второе издание в течение всего периода подготовки, Ким Уимпсетт (Kim Wimpsett), быстро отредактировавшую текст, и много других людей, занимавшихся предметным указателем, рисованием иллюстраций, корректурой и окончательной вычиткой текста. Я выражаю отдельную благодарность Гэри Корнеллу (Gary Cornell) за его постоянные бесценные советы по организации проекта и по издательскому делу в целом. Кристоф Насарр, как технический редактор, заслуживает моей искренней благодарности за его безотказные и глубокие по своей сути редакторские комментарии — они очень помогли мне при написании книги. Я благодарен также множеству блоггеров из разных команд WPF, которые помогли мне понять суть самых темных сторон WPF. Я приветствую всех, кто желает узнать гораздо больше о будущем WPF. Наконец, я бы никогда не написал эту книгу, не будь у меня такой поддержки со стороны моей жены и следующих людей: Нора (Nora), Рация (Razia), Пол (Paul) и Хамид (Hamid). Спасибо вам всем!
Book_Pro_WPF-2.indb 18
19.05.2008 18:09:34
Введение С
разу после появления платформа .NET породила небольшую лавину новых технологий. Это был абсолютно новый способ написания Web-приложений (ASP.NET), совершенно новый способ подключения к базам данных (ADO.NET), новые языки программирования с безопасностью в отношении типов (C# и VB.NET) и управляемая исполняющая среда (CLR). Не менее важной среди этих новшеств была Windows Forms — библиотека классов, необходимых для создания Windows-приложений. Несмотря на то что Windows Forms является зрелым и полнофункциональным инструментальным средством, оно жестко связано с основными конструктивными особенностями Windows, которые не меняются на протяжении последних десяти лет. Более того, Windows Forms основывается на интерфейсе Windows API при создании внешнего вида стандартных элементов пользовательского интерфейса, таких как кнопки, текстовые окна, флажки и т.п. Как результат, эти ингредиенты, по сути, не поддаются настройке. Например, если вы хотите создать элегантную кнопку, вам нужно построить специальный элемент управления и раскрасить каждую частицу кнопки (во всех ее разных состояниях) с помощью низкоуровневой модели рисования. Более того, обычные окна делятся на разные области, в каждой из которых имеются свои элементы управления. В результате нет хорошего способа рисования в отдельном элементе управления (например, эффекта свечения ниже кнопки), чтобы при этом не затронуть областей, которыми владеют другие элементы. И даже не думайте об анимационных эффектах, таких как вращающийся текст, мерцающие окна или живые окна предварительного просмотра, поскольку вам придется рисовать каждую деталь вручную. Все поменялось благодаря новой модели с совершенно другой структурой, которую предлагает Windows Presentation Foundation (WPF). Несмотря на то что WPF включает уже знакомые вам стандартные элементы управления, она сама рисует каждый текст, рамку и фон. Как результат, WPF может предложить гораздо больше мощных функций, которые помогут вам изменить любой элемент содержимого, визуализируемого на экране. С помощью этих функций вы можете изменить стиль обычных элементов управления, таких как кнопки, зачастую без переписывания кода. Точно так же вы можете использовать объекты трансформации, чтобы вращать, растягивать, изменять масштаб и искажать все, что относится к пользовательскому интерфейсу; вы можете даже использовать встроенную систему анимации в WPF, чтобы все это делалось на глазах у пользователя. И поскольку механизм WPF визуализирует содержимое окна как часть одной операции, он может обрабатывать неограниченное число слоев перекрытия элементов управления, даже если они имеют нестандартные формы или частичную прозрачность. В основе новых возможностей WPF лежит мощная новая инфраструктура, основанная на DirectX — API-интерфейсе аппаратно-ускоренной графики, который обычно используется в современных компьютерных играх. Это означает, что вы можете применять богатые графические эффекты без ущерба для производительности, как это было бы при использовании Windows Forms. В действительности, вы можете даже получить расширенные функции, такие как поддержка видеофайлов и трехмерного содержимого. С их помощью (а также при наличии хорошего инструмента для проектирования) мож-
Book_Pro_WPF-2.indb 19
19.05.2008 18:09:34
20
Введение
но создавать потрясающие пользовательские интерфейсы и визуальные эффекты, чего невозможно сделать с помощью Windows Forms. Несмотря на то что современные функции видео, анимации и трехмерных изображений часто становятся объектом наибольшего внимания в WPF, важно отметить, что вы можете применять WPF и для создания обычных Windows-приложений со стандартными элементами управления и простым внешним видом. В действительности, совсем несложно использовать обычные элементы управления в WPF, как и в Windows Forms. Более того, WPF улучшает функции, которые будут представлять интерес для разработчиков бизнес-приложений, включая существенно улучшенную модель привязки данных, новый набор классов для печати содержимого и управления очередью печати, а также возможность использования документов для отображения больших объемов форматированного текста. Вы получите даже новую модель для создания страничных приложений, плавно выполняющихся в Internet Explorer, и которые могут запускаться с Web-сайта, и все это без обычных предупреждений о безопасности и раздражающих подсказок по инсталляции. Наконец, WPF комбинирует лучшие качества из старого мира разработки приложений для Windows и новые инновационные технологии для создания современных, насыщенных качественной графикой пользовательских интерфейсов. Несмотря на то что приложения, созданные с помощью Windows Forms, будут существовать еще многие годы, разработчикам, интересующимся новыми проектами разработки приложений для Windows, следует глубже знакомиться с WPF. Совет. Если вы потратили немало усилий на создание приложения Windows Forms, вы не должны переносить его полностью в WPF, чтобы получить доступ к новым возможностям вроде анимации. В таком случае лучше добавить WPF-содержимое к существующему приложению Windows Forms, или же создать WPF-приложение, которое будет включать в себя унаследованное содержимое Windows Forms. Варианты взаимодействия описаны в главе 25.
На кого рассчитана эта книга Настоящая книга предлагает глубокие исследования WPF для профессиональных разработчиков, которые знакомы с платформой .NET, языком программирования C# и средой разработки Visual Studio. Наличие опыта работы с Windows Forms приветствуется, хотя и не является обязательным для освоения материала. Эта книга дает полное описание каждой главной функциональной особенности WPF, от XAML (язык разметки, применяемый для описания пользовательских интерфейсов WPF) до трехмерной графики и анимации. Вместе с тем вам будут предложены примеры кода, включающие другие возможности .NET Framework, такие как классы ADO.NET, служащие для реализации запросов к базам данных. Эти возможности здесь не рассматриваются. Если нужны дополнительные сведения о средствах .NET, не являющихся специфическими для WPF, обратитесь к специализированным книгам по .NET.
Как организована эта книга Эта книга содержит 27 глав. Если вы только начали знакомство с WPF, вам лучше читать книгу с самого начала, так как в последних главах часто будут упоминаться технологии, описанные в более ранних главах. Ниже приведен краткий обзор каждой главы.
Book_Pro_WPF-2.indb 20
19.05.2008 18:09:34
Введение
21
Глава 1, “Введение в WPF”. В ней описывается архитектура WPF, основы DirectX и новая система измерений, не зависящая от устройств, которая автоматически изменяет размеры пользовательских интерфейсов. Глава 2, “XAML”. В этой главе рассматривается язык XAML, который вы будете применять для определения пользовательских интерфейсов. Вы узнаете о том, почему он был создан, и как он работает, а также создадите базовое окно WPF с помощью разных приемов написания кода. Глава 3, “Класс Application ”. В ней рассказывается о модели приложений в WPF. Вы увидите, как создаются отдельные экземпляры WPF-приложений на основе документов. Глава 4, “Компоновка”. Эта глава посвящена панелям компоновки, которые позволяют организовать элементы в окне WPF. Мы рассмотрим разные стратегии компоновки и создадим некоторые обычные типы окон. Глава 5, “Содержимое”. В ней описывается модель элементов управления содержимым, которая позволяет помещать элементы в других элементах, чтобы настроить внешний вид обычных элементов управления, таких как кнопки и метки. Глава 6, “Свойства зависимостей и маршрутизируемые события”. Здесь будет рассказано о том, как WPF расширяет систему свойств и событий .NET. Вы увидите, как WPF использует свойства зависимостей для поддержки ключевых функциональных возможностей, таких как привязка данных и анимация, и как она применяет маршрутизацию событий для отправки передающихся вверх и туннельных событий через элементы вашего пользовательского интерфейса. Глава 7, “Классические элементы управления”. В ней будут рассмотрены некоторые обычные элементы управления, с которыми знаком каждый разработчик Windows-приложений. К ним относятся кнопки, текстовые поля и метки, а также их характерные отличия в WPF. Глава 8, “Окна”. В этой главе мы поговорим о том, как работают окна в WPF. Вы узнаете также, каким образом создаются окна с нерегулярными формами и как используются “стеклянные” эффекты, являющиеся визитной карточкой интерфейса Vista. Глава 9, “Страницы и навигация”. Здесь будет описан процесс создания страниц в WPF и отслеживание хронологии посещения страниц. Вы увидите также, как можно создать WPF-приложение, размещаемое в браузере, которое можно запускать на Webсайте, не выполняя утомительную процедуру инсталляции. Глава 10, “Команды”. Эта глава посвящена модели команд WPF, позволяющей связывать множество элементов управления с одним и тем же логическим действием. Глава 11, “Ресурсы”. В этой главе речь идет о том, как внедрять бинарные файлы в вашу сборку с помощью ресурсов, и как многократно использовать важные объекты в пользовательском интерфейсе. Глава 12, “Стили”. Эта глава посвящена системе стилей WPF, которая позволяет применять набор обычных значений свойств к целой группе элементов управления. Глава 13, “Фигуры, трансформации и кисти”. Эта глава рассказывает о модели двухмерного рисования в WPF. Вы узнаете о том, как создаются формы, как изменяются элементы с помощью преобразований, и как рисуются экзотические эффекты с градиентами, мозаикой и изображениями. Глава 14, “Классы Geometry, Drawing и Visual”. В этой главе более глубоко рассматривается двухмерная модель рисования. Вы узнаете о том, как создаются сложные пути, включающие дуги и кривые, как эффективно используется сложная графика, и как применяется низкоуровневый визуальный слой для оптимизированного рисования. Глава 15, “Шаблоны элементов управления”. В ней будет показано, как можно придать любому элементу управления WPF совершенно новый внешний вид (и новое пове-
Book_Pro_WPF-2.indb 21
19.05.2008 18:09:34
22
Введение
дение), подключая специально созданный шаблон. Также вы увидите, как с помощью шаблонов можно создавать приложения со сменными обложками. Глава 16, “Привязка данных”. Эта глава рассказывает о привязке данных в WPF. Вы увидите, как можно привязать любой тип объекта к пользовательскому интерфейсу, будь то экземпляр специального класса данных или полноценный DataSet из ADO.NET. Вы узнаете также о том, как осуществляется преобразование, форматирование и проверка данных. Глава 17, “Шаблоны данных, представления данных и поставщики данных”. В ней будет рассказано о хитростях, которые можно применять при проектировании профессиональных интерфейсов, управляемых данными. По ходу дела будут созданы расширенные списки данных, включающие рисунки, элементы управления и эффекты выделения. Глава 18, “Списки, деревья, панели инструментов и меню”. Здесь рассматривается семейство элементов управления списками в WPF. Вы познакомитесь с элементами управления, ориентированными на данные, такими как сетки и деревья, а также с элементами управления, ориентированными на команды, такими как панели инструментов и меню. Глава 19, “Документы”. В ней будет рассмотрена поддержка расширенных документов в WPF. Вы узнаете о том, как используются потоковые документы для представления больших объемов текста в наиболее удобной для чтения форме, и будете работать с фиксированными документами для отображения страниц, готовых к печати. А с помощью элемента управления RichTextBox можно предоставить пользователям возможность редактировать документы. Глава 20, “Печать”. Посвящена модели печати в WPF, с помощью которой вы можете рисовать текст и формы в печатаемом документе. Вы узнаете также о том, как производится управление параметрами настройки страницы и очередью печати. Глава 21, “Анимация”. В этой главе рассматривается структура анимации в WPF, которая позволяет интегрировать динамические эффекты в приложение с помощью простой декларативной разметки. Глава 22, “Звук и видео”. Здесь будет описана поддержка медиа-информации в WPF. Вы увидите, как управлять воспроизведением звука и видео, и узнаете, как получать синхронизированную анимацию и создавать живые эффекты. Глава 23, “Трехмерная графика”. В ней рассматривается поддержка рисования трехмерных форм в WPF. Вы узнаете о том, как создаются, преобразуются и анимируются трехмерные объекты. Вы увидите даже, как помещать интерактивные двухмерные элементы управления на трехмерных поверхностях. Глава 24, “Пользовательские элементы”. Здесь речь пойдет о том, как расширять существующие элементы управления в WPF, и как создавать собственные элементы управления. Вы увидите несколько примеров, включая средство для выбора цвета, основанное на шаблоне, маскированное текстовое поле, а также декоратор, с помощью которого выполняется специальное рисование. Глава 25, “Взаимодействие с Windows Forms”. В этой главе вы узнаете о том, как комбинировать содержимое WPF и Windows Forms в одном и том же приложении, и даже в одном и том же окне. Глава 26, “Многопоточность и дополнения”. Эта глава посвящена двум сложным темам. Вы научитесь строить многопоточные WPF-приложения, выполняющие длительные задачи в фоновом режиме, а также узнаете, как использовать модель дополнений для создания расширяемого приложения, которое может динамически обнаруживать и загружать отдельные компоненты. Глава 27, “Развертывание ClickOnce”. В этой главе будет показано, как развертывать WPF-приложения с помощью модели ClickOne, появившейся в .NET 2.0.
Book_Pro_WPF-2.indb 22
19.05.2008 18:09:34
23
Введение
Что необходимо для использования этой книги Система WPF доступна в двух версиях. Первоначальная версия была выпущена в рамках .NET 3.0 и поставлялась вместе с ОС Windows Vista. Вторая (слегка усовершенствованная) версия входит в состав .NET 3.5. И неслучайно она именуется как WPF 3.5, дабы соответствовать версии .NET Framework. В настоящей книге предполагается, что вы используете последнюю версию платформы — .NET 3.5. Все загружаемые примеры кода рассчитаны на Visual Studio 2008 и .NET 3.5. Тем не менее, большинство концепций, описанных в книге, справедливо и для .NET 3.0. Детальную информацию об улучшениях WPF в .NET 3.5 можно найти в разделе “Эволюция WPF” главы 1. Для того чтобы запускать приложения WPF 3.5, ваш компьютер должен работать под управлением Microsoft Windows Vista или Microsoft Windows XP с пакетом обновлений Service Pack 2. Также должен быть установлен каркас .NET Framework 3.5. Совет. В этой книге часто упоминаются Windows Vista и Windows XP — две клиентских операционных системы, которые поддерживают WPF. Понятно, что WPF поддерживается и на их серверных эквивалентах — Windows Server 2003 и Windows Server 2008. Чтобы создавать приложения WPF 3.5 (и открывать примеры проектов, которые прилагаются к этой книге), вам необходима среда Visual Studio 2008, включающая .NET Framework 3.5. Можно поступить и по-другому. Вместо того чтобы устанавливать Visual Studio, вы можете использовать Expression Blend — графически ориентированный инструмент проектирования — для создания и тестирования WPF-приложений. В общем случае Expression Blend предназначен для графических дизайнеров, занимающихся серьезными и красивыми по внешнему виду проектами, в то время как Visual Studio идеально подходит для программистов, создающих приложения на основе кода. В этой книге предполагается, что вы имеете дело с Visual Studio. Если вы желаете узнать больше о Expression Blend, можете прочитать одну из многих книг, посвященных этому продукту. В некоторых примерах этой книги используется код доступа к данным ADO.NET для запроса к базе данных SQL Server. Чтобы иметь возможность работать с этими примерами, можете воспользоваться файлом сценария, включенным в загружаемый код, чтобы инсталлировать базу данных (в SQL Server 2000 или более поздней версии). Как вариант, вы можете использовать файловый компонент базы данных, который также входит в состав загружаемого кода. Этот компонент получает такие же данные из файла XML, имитируя работу полного компонента базы данных, не запрашивая при этом активный экземпляр SQL Server.
Примеры кода и URL-адреса Рекомендуется посетить сайт издательства или зайти на страницу http://www. prosetech.com и загрузить примеры кода для этой книги. Это нужно для того, чтобы вы смогли самостоятельно проработать большинство самых сложных примеров кода, поскольку менее важные детали обычно остаются за кадром. Эта книга сфокусирована на наиболее важных разделах, поэтому вам не придется понапрасну перелистывать страницы, чтобы понять суть.
Book_Pro_WPF-2.indb 23
19.05.2008 18:09:35
24
Введение
От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: WWW:
[email protected] http://www.williamspublishing.com
Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152
Book_Pro_WPF-2.indb 24
19.05.2008 18:09:35
ГЛАВА
1
Введение в WPF W
indows Presentation Foundation (WPF) — совершенно новая графическая система отображения для Windows. WPF спроектирована для .NET под влиянием таких современных технологий отображения, как HTML и Flash, с использования аппаратного ускорения. Она также представляет собой наиболее радикальное изменение в пользовательском интерфейсе Windows со времен Windows 95. В этой главе вы ознакомитесь с архитектурой WPF, получите начальное представление о ее работе и узнаете, что она несет с собой для будущих поколений приложений Windows.
Графика в 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-разрядный мир.
DirectX: новый графический механизм В Microsoft разработали один обходной путь для преодоления ограничений, присущих библиотекам User32 и GDI/GDI+. Этот путь — DirectX. DirectX начинался как “топорный”, полный ошибок инструментарий для создания игр на платформе Windows.
Book_Pro_WPF-2.indb 25
19.05.2008 18:09:35
26
Глава 1
Главной его целью была скорость, и потому Microsoft тесно сотрудничала с производителями видеокарт, чтобы обеспечить для DirectX аппаратную поддержку, необходимую для отображения сложных текстур, специальных эффектов, таких как частичная прозрачность и трехмерная графика. За годы, прошедшие с момента его появления (вскоре после Windows 95), механизм DirectX обрел зрелость. Теперь это неотъемлемая часть Windows, которая включает поддержку всех современных видеокарт. Однако программный интерфейс DirectX по-прежнему несет в себе наследие своих корней как средства разработки игр. Из-за присущей DirectX сложности он почти никогда не использовался в традиционных приложениях Windows (вроде программы для поддержки бизнеса). WPF в корне изменяет ситуацию. Лежащая в основе WPF графическая технология — это не GDI/GDI+. Теперь это DirectX. Примечательно, что приложения WPF используют DirectX независимо от создаваемого типа пользовательского интерфейса. Это значит, что создаете ли вы сложную трехмерную графику (DirectX’s forté), либо просто рисуете кнопки и простой текст — вся работа по рисованию проходит через конвейер DirectX. В результате даже самые заурядные бизнес-приложения могут использовать богатые эффекты вроде прозрачности и сглаживания. Вы также выигрываете от аппаратного ускорения, и это означает, что DirectX передает как можно больше работы GPU (graphics processing unit — узел обработки графики), который представляет собой отдельный процессор на видеокарте. На заметку! DirectX более эффективен, потому что оперирует высокоуровневыми ингредиентами вроде текстур и градиентов, которые могут отображаться непосредственно видеокартой. GDI/ GDI+ на это не способен, поэтому ему приходится конвертировать их в инструкции рисования пикселей, и потому отображение идет намного медленнее на современных видеокартах. Один компонент, который остается на сцене (в ограниченной степени) — это User32. Это объясняется тем, что WPF по-прежнему полагается на User32 в отношении таких служб, как обработка и маршрутизация ввода, а также определение того, какое приложение владеет какой частью экрана. Однако все рисование осуществляется через DirectX. На заметку! Это наиболее существенное изменение в WPF. WPF — это не оболочка для GDI/GDI+. На самом деле это его замена — отдельный слой, работающий через DirectX.
Аппаратное ускорение и WPF Вы, вероятно, уже знаете, что видеокарты различаются между собой в их поддержке специализированных средств визуализации и оптимизации. При программировании с DirectX это является существенной проблемой. С применением WPF она не так сильно проявляется, поскольку WPF обладает способностью выполнять всю работу с использованием программных вычислений вместо того, чтобы полагаться на встроенную поддержку видеокарты. На заметку! Существует одно исключение в отношении программной поддержки WPF. Из-за слабой поддержки драйверов WPF выполняет сглаживание трехмерной графики только в случае, если ваше приложение запущено под Windows Vista (и у вас есть “родной” драйвер Windows Vista для установленной видеокарты). Это значит, что если вы рисуете трехмерные фигуры на компьютере с Windows XP, то получите ступенчатые ломаные линии вместо гладких наклонных. Но сглаживание всегда обеспечивается для двумерной графики, независимо от операционной системы и поддержки драйверов.
Book_Pro_WPF-2.indb 26
19.05.2008 18:09:35
Введение в WPF
27
Наличие мощной видеокарты не дает абсолютной гарантии, что вы получите максимальную, с аппаратной поддержкой производительность на WPF. Программное обеспечение также играет важную роль. Например, WPF не может обеспечить аппаратного ускорения на видеокартах, если используются устаревшие драйверы. (Если у вас установлена устаревшая видеокарта, такие драйверы, скорее всего, будут единственно доступными.) WPF также обеспечивает более высокую производительность в среде операционной системы Windows Vista, где может воспользоваться преимуществами новой модели дисплейных драйверов Windows Vista (Windows Vista Display Driver Model — WDDM). WDDM предлагает несколько важных усовершенствований по сравнению с Windows XP Display Driver Model (XPDM). Что более важно, WDDM позволяет запланировать несколько операций GPU сразу и отображать страницы памяти видеокарты на нормальную системную память, если вы израсходовали всю память видеокарты. В качестве главного эмпирического правила: WPF предоставляет некоторого рода аппаратное ускорение всем драйверам WDDM (Windows Vista) и драйверам XPDM (Windows XP), созданным после ноября 2004 г., когда Microsoft издала новые руководства по разработке драйверов. Конечно, уровень поддержки отличается. Когда запускается инфраструктура WPF, она оценивает вашу видеокарту и присваивает ей рейтинг от 0 до 2, как описано во врезке “Уровни WPF”. Среди обещаний WPF было то, что вам не нужно беспокоиться о деталях и сложностях, связанных со специфическим аппаратным обеспечением. WPF достаточно интеллектуален, чтобы по возможности использовать аппаратную оптимизацию, но в случае неудачи все будет обработано программно. Поэтому если вы запустите приложение WPF на компьютере с унаследованной видеокартой, интерфейс будет выглядеть так, как вы его разработали. Конечно, программные альтернативы могут быть значительно медленнее, так что вы столкнетесь с тем, что компьютеры со старыми видеокартами не очень хорошо отрабатывают развитые приложения WPF — особенно те, что включают сложную анимацию или другие сложные графические эффекты. На практике вы можете предпочесть упростить некоторые сложные эффекты в пользовательском интерфейсе, в зависимости от уровня аппаратной поддержки, доступной клиенту (определяется свойством RenderCapability.Tier). На заметку! Целью WPF является взвалить на видеокарту как можно больше работы, чтобы сложные графические процедуры зависели от визуализации (ограничены GPU), а не от вычислительной мощности процессора (т.е. ограничены центральным процессором (CPU) вашего компьютера). Таким образом, вы освобождаете CPU для другой работы, наиболее эффективно используете видеокарту и можете воспользоваться преимуществами новых видеокарт по мере их появления.
Уровни WPF Видеокарты значительно различаются между собой. Когда WPF обращается к видеокарте, то учитывает много факторов, включая объем памяти видеокарты, поддержку затенения пикселей (встроенные процедуры вычисления попиксельных эффектов — таких как прозрачность), затенения вершин (встроенные процедуры вычисления вершин треугольника, применяемых при затенении трехмерных объектов). На основе всех этих деталей определяется значение уровня визуализации WPF. WPF распознает следующие три уровня визуализации. • Уровень визуализации 0. Видеокарта не предоставляет никакого аппаратного ускорения. Это соответствует версии DirectX уровня ниже 7.0.
Book_Pro_WPF-2.indb 27
19.05.2008 18:09:35
28
Глава 1
• Уровень визуализации 1. Видеокарта обеспечивает частичное аппаратное ускорение. Это соответствует уровню версии DirectX выше 7.0, но ниже 9.0. • Уровень визуализации 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://msdn2.microsoft.com/en-gb/library/ms742196.aspx.
WPF: высокоуровневый API Если бы единственным достоинством WPF было аппаратное ускорение через DirectX, это уже было бы значительным усовершенствованием, хотя и не революционным. Однако WPF на самом деле включает целый набор высокоуровневых служб, ориентированных на прикладных программистов. Ниже приведен список некоторых наиболее существенных изменений, которые принес с собой WPF в мир программирования Windows.
• Web-подобная модель компоновки. Вместо того чтобы фиксировать элементы управления на месте с определенными координатами, WPF поддерживает гибкий поток, размещающий элементы управления на основе их содержимого. В результате получается пользовательский интерфейс, который может быть адаптирован для отображения высокодинамичного содержимого или разных языков.
• Богатая модель рисования. Вместо рисования пикселей в WPF вы имеете дело с примитивами — базовыми фигурами, блоками текста и прочими графическими ингредиентами. Вы также имеете такие новые средства, как действительно прозрачные элементы управления, возможность складывать множество уровней с разной степенью прозрачности, а также встроенную поддержку трехмерной графики (3-D). На заметку! Поддержка 3-D в WPF не столь зрелая, как в Direct3D или OpenGL. Если вы планируете проектировать приложение, которое интенсивно использует трехмерную графику (подобное игре реального времени), WPF, возможно, не предоставит всех средств и производительности, которые вам понадобятся.
• Богатая текстовая модель. После многих лет нестандартной обработки текстов в таких несовершенных элементах управления, как классический Label, WPF наконец-то предоставляет приложениям Windows возможность отображения богатого
Book_Pro_WPF-2.indb 28
19.05.2008 18:09:35
Введение в WPF
29
стилизованного текста в любом месте пользовательского интерфейса. И если вам нужно отображать значительные объемы текста, вы можете воспользоваться развитыми средствами отображения документов, такими как переносы, разбиение на колонки и выравнивание для повышения читабельности.
• Анимация как первоклассная программная концепция. Да, вы можете использовать таймер для того, чтобы заставить форму перерисовать себя. Но в WPF анимация — неотъемлемая часть программного каркаса. Вы определяете анимацию декларативными дескрипторами, и WPF запускает ее в действие автоматически.
• Поддержка аудио и видео. Прежние инструментарии пользовательского интерфейса, такие как Windows Forms, были весьма ограничены в работе с мультимедиа. Но WPF включает поддержку воспроизведения любого аудио- или видеофайла, поддерживаемого Windows Media Player, позволяя вам воспроизводить более одного медиафайла одновременно. Что еще больше впечатляет — он предоставляет в ваше распоряжение инструменты для интеграции видеосодержимого в остальную часть вашего пользовательского интерфейса, позволяя выполнять такие экзотические трюки, как размещение видеоокна на поверхности вращающегося трехмерного куба.
• Стили и шаблоны. Стили позволяют стандартизовать форматирование и повторно использовать его по всему приложению. Шаблоны позволяют изменить способ отображения элементов, даже таких основополагающих, как кнопки. Построение настраиваемого (skinned — с обложками) интерфейса еще никогда не было таким простым.
• Команды. Большинство пользователей знают, что не имеет значения, откуда они инициируют команду Open (Открыть) — через меню или панель инструментов; конечный результат один и тот же. Теперь эта абстракция доступна вашему коду — вы можете определять прикладные команды в одном месте и привязывать их к множеству элементов управления.
• Декларативный пользовательский интерфейс. Хотя вы можете конструировать окно WPF в коде, Visual Studio использует другой подход. Содержимое каждого окна сериализуется в виде XML-дескрипторов в документе XAML. Преимущество состоит в том, что ваш пользовательский интерфейс полностью отделен от кода, и графические дизайнеры могут использовать профессиональные инструменты, чтобы редактировать ваши файлы XAML, улучшая внешний вид всего приложения. (XAML — это сокращение от Extensible Application Markup Language (Расширяемый язык разметки приложений), который описан в главе 2.)
• Приложения на основе страниц. Используя WPF, вы можете строить браузер-подобные приложения, которые позволяют перемещаться по коллекции страниц, оснащенной кнопками навигации вперед и назад. WPF автоматически обрабатывает все сложные детали, такие как хронология посещения страниц. Вы можете даже развернуть ваш проект в виде браузерного приложения, которое выполняется внутри Internet Explorer.
Независимость от разрешения Традиционные приложения Windows связаны определенными предположениями относительно разрешения экрана. Обычно разработчики рассчитывают на стандартное разрешение монитора (подобное 1024×768 пикселей) и проектируют свои окна с учетом этого, стараясь обеспечить разумное поведение при изменении размеров в большую и меньшую сторону.
Book_Pro_WPF-2.indb 29
19.05.2008 18:09:35
30
Глава 1
Проблема в том, что пользовательский интерфейс в традиционных приложениях Windows не является масштабируемым. В результате, если вы используете монитор высокого разрешения, который располагает пиксели более плотно, окно вашего приложения становится меньше и читать его труднее. Эта проблема особенно актуальна для новых мониторов, которые имеют высокую плотность пикселей и соответственно работают с более высоким разрешением. Например, легче встретить мониторы (особенно на портативных компьютерах), которые имеют плотность пикселей в 120 dpi или 144 dpi (точек на дюйм), чем более традиционные 96 dpi. При их родном разрешении эти мониторы располагают пиксели более плотно, создавая напрягающие глаз мелкие элементы управления и текст. В идеале приложения должны использовать более высокую плотность пикселей, чтобы отобразить больше деталей. Например, монитор высокого разрешения может отображать одинакового размера пиктограммы панели инструментов, но использовать дополнительные пиксели для отображения мелкой графики. Таким образом, вы можете сохранить некоторую базовую компоновку, но обеспечить более высокую четкость деталей. По разным причинам такое решение было невозможно в прошлом. Хотя вы можете изменять размер графического содержимого, нарисованного в GDI/GDI+, User32 (который генерирует визуальное представление распространенных элементов управления) не поддерживает реального масштабирования. WPF не страдает от этой проблемы, потому что самостоятельно визуализирует все элементы пользовательского интерфейса — от простых фигур до распространенных элементов управления, таких как кнопки. В результате если вы создаете кнопку шириной в 1 дюйм на вашем мониторе, она останется шириной в 1 дюйм и на мониторе высокого разрешения. WPF просто визуализирует ее более детализировано, с большим количеством пикселей. На заметку! Независимость от разрешения также имеет преимущества при печати содержимого окна, как будет показано в главе 20. Так выглядит картина в целом, но нужно уточнить еще несколько деталей. Самое важное, что вы должны осознать — это то, что WPF базирует свое масштабирование на системной установке DPI, а не на DPI физического устройства дисплея. Это совершенно логично — в конце концов, если вы отображаете ваше приложение на 100-дюймовом проекторе, то вероятно, отойдете на несколько шагов назад и будете ожидать увидеть огромную версию ваших окон. Конечно, вам не желательно, чтобы WPF масштабировал ваше приложение, уменьшив его до “нормального” размера. Аналогично, если вы используете портативный компьютер с дисплеем высокого разрешения, то хотите увидеть несколько уменьшенные окна; это цена, которую вы платите за то, чтобы вместить всю вашу информацию на маленьком экране. Более того, у разных пользователей разные предпочтения на этот счет. Некоторым нужны богатые подробности, в то время как другие хотят увидеть больше содержимого. Так каким же образом WPF определяет, насколько большим должно быть окно приложения? Короткий ответ состоит в том, что WPF использует системную установку DPI при вычислении размеров. Но чтобы понять, как это на самом деле работает, стоит присмотреться к системе измерений WPF.
Единицы WPF Окно WPF и все элементы внутри него измеряются в независимых от устройства единицах. Одна такая единица определяется как 1/96 дюйма. Чтобы понять, что это означает на практике, нужно рассмотреть пример.
Book_Pro_WPF-2.indb 30
19.05.2008 18:09:35
Введение в WPF
31
Предположим, что вы создаете в WPF маленькую кнопку размером 96×96 единиц. Если вы используете стандартную установку Windows DPI (96 dpi), то каждая независимая от устройства единица измерения соответствует одному реальному физическому пикселю. Это потому, что WPF использует следующее вычисление: [Размер в физических единицах] = × = =
[Размер в независимых от устройства единицах] [DPI системы] 1/96 дюйма × 96 dpi 1 пиксель
По сути, WPF предполагает, что ему нужно 96 пикселей, чтобы отобразить один дюйм, потому что Windows сообщает ему об этом через системную настройку DPI. Однако в действительности это зависит от вашего дисплейного устройства. Например, рассмотрим 20-дюймовый жидкокристаллический монитор с максимальным разрешением в 1600×1200 пикселей. Используя теорему Пифагора, вы можете вычислить плотность пикселей для этого монитора, как показано ниже:
[DPI экрана] =
16002 +12002 пикселей
= 100 dpi
19дюймов В этом случае плотность пикселей составляет 100 dpi, что немного больше того, что предполагает Windows. В результате на этом мониторе кнопка размером 96×96 пикселей будет немного меньше одного дюйма. С другой стороны, рассмотрим 15-дюймовый жидкокристаллический монитор с разрешением 1024×768 пикселей. Здесь плотность пикселей составит около 85 dpi, поэтому кнопка размером 96×96 пикселей окажется размером немного больше 1 дюйма. В обоих этих случаях, если вы уменьшите размер экрана (скажем, переключившись на разрешение 800×600), то кнопка (и любой другой экранный элемент) станет пропорционально больше. Это потому, что системная установка DPI останется 96 dpi. Другими словами, Windows продолжает предполагать, что 96 пикселей составляют дюйм, даже несмотря на то, что при меньшем разрешении потребуется существенно меньше пикселей. Совет. Как вам, возможно, известно, жидкокристаллические мониторы создаются с единственным разрешением, которое называется естественным разрешением. Если вы уменьшаете это разрешение, то монитору приходится использовать интерполяцию, чтобы заполнить лишние пиксели, что может вызвать нерезкость. Чтобы получить наилучшее качество изображения, всегда лучше использовать естественное разрешение. Если вы хотите иметь более крупные окна, кнопки и текст, рассмотрите вместо этого возможность модификации системной установки DPI (как описано далее).
Системная установка DPI До сих пор пример кнопки WPF работал точно так же, как любой другой интерфейсный элемент в приложении Windows любого иного типа. Отличие проявляется при изменении вашей системной установки DPI. В предыдущем поколении Windows это средство иногда называли крупными шрифтами. Это потому, что системная установка DPI влияет на размер системных шрифтов, часто оставляя прочие детали неизменными. На заметку! Многие приложения Windows не полностью поддерживают увеличенные установки DPI. В худшем случае увеличение системной установки DPI может привести к появлению окон, в которых некоторое содержимое увеличено, а другое — нет, что может привести к утере части содержимого или даже к нефункциональным окнам.
Book_Pro_WPF-2.indb 31
19.05.2008 18:09:36
32
Глава 1
Здесь поведение WPF отличается. WPF воспринимает системную установку DPI естественно и без особых затрат. Например, если вы измените системную установку DPI на 120 dpi (распространенный выбор пользователей экранов большого разрешения), WPF предполагает, что для заполнения дюйма пространства нужно 120 пикселей. WPF использует следующее вычисление для определения того, как он должен транслировать логические единицы в физические пиксели устройства: [Размер в физических единицах] = × = =
[Размер в независимых от устройства единицах] [DPI системы] 1/96 дюйма × 120 dpi 1,25 пикселя
Другими словами, когда вы устанавливаете системную настройку DPI в 120 dpi, то механизм визуализации WPF предполагает, что одна независимая от устройства единица измерения соответствует 1,25 пикселей. Если вы отображаете кнопку 96×96, то физический ее размер составит 120×120 пикселей (потому что 96 × 1,25 = 120). Именно такого результата вы и ожидаете — кнопка размером в 1 дюйм имеет такой же размер на мониторе с повышенной плотностью пикселей. Такое автоматическое масштабирование было бы не слишком полезным, если бы касалось только кнопок. Но WPF использует независимые от устройства единицы для всего, что отображает, включая фигуры, элементы управления, текст и любые другие ингредиенты, которые вы помещаете в окно. В результате вы можете изменять системную установку DPI, как вам заблагорассудится, и WPF незаметно подгонит размеры окон вашего приложения. На заметку! В зависимости от системной установки DPI вычисляемый размер пикселя может быть выражен дробным значением. Вы можете предположить, что WPF просто округляет все размеры до ближайшего пикселя. (На самом деле WPF поддерживает средство усечения пикселей, которое именно это и делает, и в главе 13 вы узнаете, как включить его для определенных частей содержимого.) Однако по умолчанию WPF поступает несколько иначе. Если грань элементов приходится на точку между пикселями, WPF использует сглаживание, чтобы размыть эту грань. Это может показаться странным решением, но на самом деле оно вполне оправдано. Ваши элементы управления не обязательно должны иметь прямые четкие грани, если вы используете специально рисованную графику для их отображения; поэтому некоторая степень сглаживания все равно необходима. Шаги для изменения системной установки DPI зависят от операционной системы. В Windows XP это делается следующим образом. 1. Щелкните правой кнопкой мыши на рабочем столе и выберите команду Display (Отобразить) из контекстного меню. 2. Перейдите на вкладку Settings (Настройка) и щелкните на кнопке Advanced (Дополнительно). 3. На вкладке General (Общие) выберите Normal Size (96 dpi) (Обычный размер (96 dpi)) или Large Size (120 dpi) (Крупный размер (120 dpi)). Это две рекомендованных опции для Windows XP, потому что специальные установки DPI, скорее всего, не будут поддерживаться старыми программами. Чтобы попробовать установить собственное значение DPI, выберите Custom Setting (Специальная настройка). Вы затем вы можете указать определенное значение в процентах (например, 175% увеличивает стандартное значение 96 dpi до 168 dpi).
Book_Pro_WPF-2.indb 32
19.05.2008 18:09:36
Введение в WPF
33
А вот как изменяется системная настройка DPI в Windows Vista. 1. Щелкните правой кнопкой мыши на рабочем столе и выберите команду Personalize (Персонализация) из контекстного меню. 2. В списке ссылок слева выберите Adjust Font Size (DPI) (Корректировка размеров шрифта (DPI)). 3. Выберите между 96 и 120 dpi. Или же щелкните на кнопке Custom DPI (Специальное значение DPI), чтобы указать специальное значение DPI. Затем вы можете задать значение в процентах, как показано на рис. 1.1 (например, 175% увеличивает стандартное значение 96 dpi до 168 dpi). В дополнение, используя специальную установку DPI, у вас появляется опция под названием Use Windows XP Style DPI Scaling (Использовать масштабирование DPI, принятое в Windows XP), которая описана во врезке “Масштабирование DPI в Windows Vista”.
Рис. 1.1. Изменение системной установки DPI
Масштабирование DPI в Windows Vista Поскольку старые приложения известны недостатком поддержки высоких установок DPI, в Windows Vista используется новый прием: масштабирование битовой карты (bitmap scaling). Если вы запускаете приложение, которое не поддерживает высоких значений DPI, то Windows Vista изменяет размер содержимого окна до желаемого DPI, как если бы это было просто графическое изображение. Преимущество такого решения в том, что приложению кажется, что оно работает при стандартных 96 dpi. Windows незаметно транслирует ввод (такой как щелчки кнопками мыши) и маршрутизирует его в правильное место соответствующей “реальной” координатной системы.
Book_Pro_WPF-2.indb 33
19.05.2008 18:09:36
34
Глава 1
Алгоритм масштабирования, используемый Windows Vista, достаточно хорош — он старается избегать размытости граней и использует аппаратную поддержку видеокарты, когда это позволяет увеличить скорость, но это неизбежно приводит к некоторой общей размытости изображения. К тому же это имеет серьезные ограничения в том, что Windows не может распознать старые приложения, которые поддерживают высокие значения DPI. Поэтому приложения должны включать манифест или вызывать SetProcessDPIAware (в User32) для объявления о своей поддержке высоких значений DPI. Хотя приложения WPF обрабатывают этот шаг корректно, приложения, разработанные до появления Windows Vista, не могут использовать ни один из подходов, и обречены на неидеальное масштабирование битовой карты. Здесь есть два возможных решения. Если у вас имеется несколько специфичных приложений, которые поддерживают высокие установки DPI, но не сообщают об этом, вы можете конфигурировать эту деталь вручную. Чтобы сделать это, щелкните правой кнопкой мыши на ярлыке, запускающем приложение (в меню Start (Пуск)) и выберите команжу Properties (Свойства) из контекстного меню. На вкладке Compatibility (Совместимость) переключите опцию под названием Disable Display Scaling (Запретить масштабирование отображения) на High DPI Settings (Высокие установки DPI). Если вам придется конфигурировать много приложений, это может оказаться довольно утомительным. Другое возможное решение заключается в том, чтобы вообще отключить масштабирование битовой карты. Чтобы сделать это, выберите опцию Use Windows XP Style DPI Scaling (Использовать масштабирование DPI, принятое в Windows XP) в диалоговом окне Custom DPI Setting (Специальные установки DPI), как показано на рис. 1.1. Единственное ограничение этого подхода состоит в том, что могут существовать приложения, которые неправильно отображаются (и потому могут даже оказаться неработоспособными) при высоких установках DPI. По умолчанию опция Use Windows XP Style DPI Scaling отмечена для размеров DPI 120 и менее, однако отметка снята для размеров DPI свыше 120.
Растровая и векторная графика Когда вы имеете дело с обычными элементами управления, то можете рассчитывать на независимость WPF от разрешения. WPF автоматически заботится о том, чтобы все имело правильный размер. Однако если вы планируете включать в приложение изображения, вы не можете быть так уверены. Например, в традиционных приложениях Windows разработчики используют крошечные растровые изображения для команд панели инструментов. В приложении WPF такой подход не идеален, потому что битовая карта может отображать артефакты (размытые), которые будут масштабироваться вверх и вниз согласно системной установке DPI. Вместо этого при проектировании пользовательского интерфейса WPF даже самые мелкие пиктограммы обычно реализованы в векторной графике. Векторная графика определена как набор фигур, каждая из которых может быть легко масштабирована до любого размера. На заметку! Конечно, отображение векторной графики требует больше времени, чем базовая битовая карта, но WPF включает оптимизацию, которая призвана снизить накладные расходы, обеспечивая разумную производительность для любого бизнес-приложения и большинства приложений, ориентированных на индивидуальных потребителей. Сложно недооценить важность независимости от разрешения. На первый взгляд это кажется очевидным, элегантным решением старой проблемы (что так и есть). Однако чтобы проектировать полностью масштабируемые интерфейсы, разработчикам следует научиться новому образу мышления.
Book_Pro_WPF-2.indb 34
19.05.2008 18:09:36
Введение в WPF
35
Эволюция WPF Хотя WPF — относительно новая технология, уже существуют две ее версии.
• WPF 3.0. Первая версия WPF, которая реализована вместе с двумя другими новыми технологиями — Windows Communication Foundation (WCF) и Windows Workflow Foundation (WF). Вместе все эти три технологии получили название .NET Framework 3.0 (несмотря на то, что ядро .NET не было изменено).
• WPF 3.5. Годом позже вышла новая версия WPF как часть .NET Framework 3.5. Новые средства WPF претерпели в основном незначительные усовершенствования. Некоторые из них касались исправлений ошибок и оптимизации производительности и были доступны приложениям .NET Framework 3.0 благодаря пакету обновлений .NET Framework 3.0 Service Pack 1. С точки зрения разработчика наиболее существенное различие между WPF 3.0 и WPF 3.5 заключается в поддержке визуальной среды проектирования. Версия .NET Framework 3.0 вышла без соответствующей версии Visual Studio. Разработчики могли получить лишь базовую поддержку Visual Studio 2005, инсталлировав бесплатный пакет Community Technology Preview (CTP). Хотя эти расширения сделали возможным создание приложений WPF в среде Visual Studio 2005, они не включали визуального дизайнера окон WPF с поддержкой перетаскивания. Версия .NET Framework 3.5 вышла в сочетании с Visual Studio 2008, в результате чего она получила намного лучшую поддержку времени проектирования для построения приложений WPF. В этой книге мы предполагаем, что вы используете WPF 3.5 и Visual Studio 2008. Однако даже если вы работаете с версией WPF 3.0, к ней применимы почти все описанные концепции.
Новые средства в WPF 3.5 Если вы программировали в первой версии WPF, вас могут заинтересовать произошедшие изменения. Помимо исправления ошибок, повышения производительности и лучшей поддержки времени проектирования версия WPF 3.5 предоставила следующие усовершенствования (перечислены в порядке, в котором они появляются в этой книге).
• Поддержка Firefox для XBAP. Теперь можно запускать браузерные приложения WPF (известные как XBAP) наряду с Internet Explorer, в браузере Firefox. Подробности см. в главе 9.
• Поддержка привязки данных для LINQ. LINQ — это набор расширений языка, которые позволяют разработчику писать запросы. Эти запросы могут извлекать данные из разных источников, включая находящиеся в памяти коллекции, файлы XML и базы данных — и все это не требуя написания ни одной строки низкоуровневого кода. (Чтобы узнать больше о LINQ, обратитесь по адресу http:// msdn.microsoft.com/data/ref/linq или к специальным изданиям на эту тему.) Теперь WPF полностью поддерживает LINQ в сценариях привязки данных вроде того, что описан в главе 16.
• Поддержка привязки данных для IDataErrorInfo. Интерфейс IDataErrorInfo — ключевой механизм, предназначенный для бизнес-разработчиков, которые желают создавать развитые объекты данных со встроенной проверкой достоверности. Теперь инфраструктура привязки данных может перехватывать ошибки проверки достоверности и отображать их в пользовательском интерфейсе.
Book_Pro_WPF-2.indb 35
19.05.2008 18:09:36
36
Глава 1
• Поддержка размещения интерактивных элементов управления (типа кнопок) внутри элемента RichTextBox. Это средство ранее требовало сложного обходного пути. Теперь оно работает через простое свойство, описанное в главе 19.
• Поддержка размещения двумерных элементов на трехмерных поверхностях. Это средство ранее требовало отдельной загрузки. Теперь оно включено в каркас наряду с лучшей поддержкой трехмерных объектов, которые могут возбуждать события мыши и клавиатуры. Использование этих средств вы изучите в главе 23.
• Модель дополнений. Модель дополнений (add-in) позволяет приложению размещать компоненты от независимых разработчиков в ограниченном контексте безопасности. Технически это средство не является специфичным для WPF, потому что может быть использовано в любом приложении .NET 3.5. О том, как оно работает с WPF, вы узнаете в главе 26.
Множество целевых платформ Предыдущие версии Visual Studio были тесно привязаны к определенным версиям .NET. Вы использовали Visual Studio .NET для создания приложений .NET 1.0, Visual Studio .NET 2003 для создания приложений .NET 1.1 и Visual Studio 2005 — для приложений .NET 2.0. Отчасти Visual Studio 2008 устраняет это ограничение. Эта среда позволяет вам создавать приложения, которые предназначены для работы с .NET 2.0, .NET 3.0 или .NET 3.5. Хотя, очевидно, невозможно создавать приложения WPF с помощью .NET 2.0, зато и .NET 3.0, и .NET 3.5 имеют поддержку WPF. Вы можете предпочесть ориентацию на .NET 3.0 для несколько более широкой совместимости (поскольку приложения .NET 3.0 могут работать как в исполняющей среде .NET 3.0, так и в .NET 3.5). Или же вы можете ориентироваться на .NET 3.5, чтобы получить доступ к новейшим средствам WPF и самой платформы .NET. (Одна распространенная причина для ориентации на .NET 3.5 заключается в поддержке LINQ — набора технологий, которые позволяют языкам .NET получать доступ к разным источникам данных, используя тесно интегрированный синтаксис запросов.) Когда вы создаете новый проект в Visual Studio (выбирая в меню FileNewProject (ФайлСоздатьПроект)), то можете выбрать версию .NET Framework, на которую будет ориентировано ваше приложение, из раскрывающегося списка в правом верхнем углу диалогового окна New Project (Новый проект), как показано на рис. 1.2. Вы можете также в любое время позднее изменить версию, на которую нацелены, выполнив двойной щелчок на узле Properties (Свойства) в Solution Explorer и изменив выбор в списке Target Framework (Целевая платформа). Чтобы действительно разобраться, как работает многоцелевая система Visual Studio, вам нужно знать немного больше о том, как структурирован .NET 3.5. По сути, .NET 3.5 состоит из трех отдельных процессов — копии оригинальных сборок .NET 2.0, копии сборок, добавленных в .NET 3.0 (для WPF, WCF и WF), а также новых сборок, добавленных в .NET 3.5 (для LINQ и ряда других средств). Однако когда вы создаете и тестируете приложение в Visual Studio, то всегда используете сборки .NET 3.5. Когда в качестве целевой платформы выбирается другая версия .NET, то Visual Studio просто использует подмножество сборок .NET 3.5. Например, когда вы выбираете в качестве целевого каркас .NET 3.0, то тем самым конфигурируете Visual Studio на использование части .NET 3.5 — только тех сборок, которые были доступны в .NET 2.0 и .NET 3.0. В этой системе есть один потенциальный камень преткновения. Хотя считается, что эти сборки не изменились в .NET 3.5, на самом деле они не вполне идентичны версиям из .NET 2.0. Например, они могут включать оптимизацию производительности, исправления ошибок и (очень редко) новые обще-
Book_Pro_WPF-2.indb 36
19.05.2008 18:09:37
Введение в WPF
37
доступные члены классов. По этой причине, если вы строите сборку, ориентированную на более ранние версии .NET, то должны тестировать ее с этой версией .NET, чтобы быть абсолютно уверенным, что не возникнет сюрпризов с обратной совместимостью.
Рис. 1.2. Выбор целевой версии .NET Framework На заметку! В Visual Studio 2008 не предусмотрено способа построения приложения, которое специально ориентировано на .NET 3.0 с пакетом обновлений SP1. Поэтому, если есть какое-то средство, добавленное в .NET Framework 3.0 Service Pack 1, вы не сможете использовать его (если только не скомпилируете ваш проект вручную из командной строки). Единственное решение в этом случае — сделать еще один шаг и перейти на .NET 3.5.
Windows Forms все еще в силе WPF — платформа для разработки будущего пользовательского интерфейса Windows. Однако она не заменит полностью Windows Forms. Во многих отношениях Windows Forms — это кульминация технологии отображения, построенной на GDI/GDI+ и User32. Это более зрелая технология, чем WPF, и она все еще включает средства, которые пока еще не нашли своего места в инструментарии WPF (такие как элементы управления WebBrowser, DataGridView и компонент HelpProvider). Так какую же платформу вам стоит выбрать, когда вы начинаете проектирование нового приложения Windows? Если вы начинаете с нуля, WPF — идеальный выбор, которому обеспечены наилучшие перспективы в отношении расширяемости и долгожительства. Также если вам нужно одно из средств, представленных в WPF, но отсутствующих в Windows Forms, такое как трехмерная графика или постраничное приложение, имеет смысл обратиться туда же. С другой стороны, если вы сделали существенные вложения в бизнес-приложение на основе Windows Forms, то нет необходимости переносить его на WPF. Платформа Windows Forms будет поддерживаться еще долгие годы.
Book_Pro_WPF-2.indb 37
19.05.2008 18:09:37
38
Глава 1
Возможно, лучшая часть этой истории заключается в том факте, что Microsoft прикладывает значительные усилия для построения промежуточного слоя между WPF и Windows Forms (который играет роль, аналогичную промежуточному слою, позволяющему приложениям .NET продолжать пользоваться унаследованными компонентами COM). В главе 25 вы узнаете, как использовать эту поддержку для размещения элементов управления Windows Forms внутри приложения WPF и наоборот. WPF предлагает аналогичную солидную поддержку для интеграции со старыми приложениями в стиле Win32.
DirectX также в силе Есть одна область, где WPF пока далек от идеала: когда нужно создавать приложения со строгими требованиями к графике реального времени, вроде сложных симуляторов физических процессов или современных игр. Если вам нужно получить максимально возможную производительность видео для приложений подобного рода, вам придется программировать на значительно более низком уровне, используя “сырой” DirectX. Вы можете загрузить управляемые библиотеки .NET для программирования с DirectX, посетив сайт http://msdn.microsoft.com/directx.
Silverlight Подобно самому .NET Framework, WPF — это технология, основанная на Windows. Это означает, что приложения WPF могут использоваться только на компьютерах под управлением операционной системы Windows (а именно — Windows XP и Windows Vista). Браузер-ориентированные приложения WPF столь же ограничены — они могут работать только на компьютерах под управлением Windows, хотя поддерживают и браузеры Internet Explorer и Firefox. Эти ограничения не изменятся — в конце концов, целью Microsoft при разработке WPF было использовать преимущества богатых возможностей компьютеров Windows и защитить инвестиции, вложенные в такие технологии, как DirectX. Однако есть отдельная технология под названием Silverlight, которая использует подмножество платформы WPF, развернутое в любом современном браузере посредством механизма подключаемых модулей (в том числе в Firefoх, Opera и Safari), и открытая для других операционных систем (таких как Linux и Mac OS). Это амбициозный проект, который уже привлек значительный интерес разработчиков. Чтобы еще более заинтересовать вас, упомянем, что Silverlight существует в двух версиях.
• Silverlight 1.0. Этот первый выпуск включает средства рисования двумерной графики, анимацию и средства воспроизведения мультимедиа, аналогичные применяемым в WPF. Однако Silverlight 1.0 не имеет поддержки .NET Framework в языках C# и Visual Basic. Вместо них вы должны использовать код JavaScript.
• Silverlight 2.0. Второй выпуск добавил сокращенную версию .NET Framework, оснащенную миниатюрной исполняющей средой CLR, развертываемой в браузере как подключаемый модуль и оснащенной небольшим подмножеством важнейших классов .NET Framework. Поскольку Silverlight 2.0 позволяет вам писать код на языке .NET, таком как C# и Visual Basic, это намного более привлекательная технология, чем Silverlight 1.0. Однако на момент написания нашей книги она доступна только в бета-версии. Хотя и Silverlight 1.0, и Silverlight 2.0 основаны на WPF и включают многие из его соглашений (таких как разметка XAML, о которой вы узнаете из следующей главы), они не охватывают средства некоторых областей. Например, ни одна из версий не поддер-
Book_Pro_WPF-2.indb 38
19.05.2008 18:09:37
Введение в WPF
39
живает реальной трехмерной графики или отображения форматированных документов. Новые средства могут появиться в будущих выпусках Silverlight, но наиболее сложных, видимо, ожидать не следует. Конечной целью Silverlight является представление мощного, ориентированного на разработку конкурента для Adobe Flash. Однако Flash обладает ключевым преимуществом — он используется повсеместно в Web, и его подключаемый модуль инсталлирован повсеместно. Для того чтобы заставить разработчиков перейти на новую, менее устоявшуюся технологию, Microsoft придется оснастить Silverlight средствами нового поколения, обеспечить солидную совместимость и непревзойденную поддержку времени проектирования. На заметку! Хотя модель программирования Silverlight лучше всего воспринимать как сильно сокращенную версию WPF, она, вероятно, более удобна для Web-разработчиков, чем для разработчиков “толстого” клиента. Это потому, что Web-разработчики могут использовать содержимое Silverlight для того, чтобы усовершенствовать обычные Web-сайты или Web-приложения, построенные на ASP.NET. Другими словами, Silverlight имеет две потенциальных целевых аудитории: Web-разработчики, которые стремятся создавать более интерактивные приложения, и разработчики Windows-приложений, которые стремятся сделать свои приложения более широко доступными. Чтобы узнать больше о Silverlight, обратитесь к специально посвященным этой теме изданиям, таким как Pro Silverlight 2.0, или посетите сайт http://silverlight.net.
Архитектура WPF WPF использует многослойную архитектуру. На вершине находятся ваше приложение, взаимодействующее с высокоуровневым набором служб, полностью состоящих из управляемого кода C#. Действительная работа по трансляции объектов .NET в текстуры Direct3D и треугольники происходит “за кулисами”, с использованием низкоуровневого неуправляемого компонента по имени milcore.dll. На заметку! Компонент milcore.dll реализован в неуправляемом коде потому, что ему требуется тесная интеграция с Direct3D, и потому, что для него чрезвычайно важна производительность. На рис. 1.3 показаны слои, на которых построена работа приложения WPF. Управляемый API\интерфейс WPF
PresentationFramework.dll
PresentationCore.dll
WindowsBase.dll
milcore.dll
WindowsCodecs.dll
Direct3D
User32
Уровень медиа\интеграции
Рис. 1.3. Архитектура WPF
Book_Pro_WPF-2.indb 39
19.05.2008 18:09:37
40
Глава 1 На рис. 1.3 присутствуют следующие ключевые компоненты.
• PresentationFramework.dll содержит типы WPF верхнего уровня, включая те, что представляют окна, панели и прочие виды элементов управления. Также он реализует высокоуровневые программные абстракции, такие как стили. Большинство классов, которые вы будете использовать, находятся непосредственно в этой сборке.
• PresentationCore.dll содержит базовые типы, такие как UIElement и Visual, от которых унаследованы все фигуры и элементы управления. Если вам не нужен полный слой абстракции окон и элементов управления, вы можете спуститься ниже, на этот уровень, и продолжать пользоваться преимуществами механизма визуализации WPF.
• WindowsBase.dll содержит еще более базовые ингредиенты, которые потенциально могут использоваться вне WPF, такие как DispatcherObject и DependencyObject, поддерживающие механизм свойств зависимости (эта тема будет детально рассмотрена в главе 6).
• milcore.dll — ядро системы визуализации WPF и фундамент уровня медиа-интеграции (Media Integration Layer — MIL). Его составной механизм транслирует визуальные элементы в треугольники и текстуры, которых ожидает Direct3D. Хотя milcore.dll считается частью WPF, это также важнейший компонент Windows Vista. Фактически DWM (Desktop Window Manager — диспетчер окон рабочего стола) в Windows Vista использует milcore.dll для отображения рабочего стола. На заметку! milcore.dll иногда называют механизмом “управляемой графики”. Подобно тому, как общеязыковая исполняющая система (CLR) управляет жизненным циклом приложения .NET, milcore.dll управляет состоянием дисплея. И так же, как CLR избавляет вас от забот об освобождении объектов и восстановлению памяти, milcore.dll избавляет от необходимости думать о недействительности и перерисовке окна. Вы просто создаете объекты с содержимым, которое хотите показать, а milcore.dll рисует соответствующие части окна, когда оно перемещается, скрывается и открывается, минимизируется и восстанавливается и т.д.
• WindowsCodecs.dll — низкоуровневый API, обеспечивающий поддержку изображений (например, обработку, отображение и масштабирование битовых карт и JPEG).
• Direct3D — низкоуровневый API, через который визуализируется вся графика в WPF.
• User32 используется для определения того, какое место на экране к какой программе относится. В результате он по-прежнему вовлечен в WPF, но не участвует в визуализации распространенных элементов управления. Наиболее важный факт, который вы должны осознать, состоит в том, что Direct3D визуализирует все рисование в WPF. При этом не важно, установлена на вашем компьютере видеокарта со скромными возможностями или же более мощная, используете вы базовые элементы управления или рисуете более сложное содержимое, запускаете ваше приложение в Windows XP или Windows Vista. Даже двумерные фигуры и обычный текст трансформируется в треугольники и проходит по каналу 3-D. Нет никаких обращений к GDI+ или User32.
Book_Pro_WPF-2.indb 40
19.05.2008 18:09:37
Введение в WPF
41
Иерархия классов Читая эту книгу, большую часть времени вы потратите на изучение пространств имен и классов WPF. Но прежде чем начать, полезно будет взглянуть на общую иерархию классов, которые ведут к базовому набору элементов управления WPF. На рис. 1.4 показан базовый обзор некоторых ключевых ветвей иерархии классов. Продвигаясь по главам этой книги, вы будете знакомиться с указанными (и связанными с ними) классами более подробно.
DispatcherObject
Условные обозначения Абстрактный класс
DependencyObject Конкретный класс Visual
UIElement
FrameworkElement
Shape
Control
Panel
ContentControl
ItemsControl
Рис. 1.4. Фундаментальные классы WPF В последующих разделах описаны основные классы из этой диаграммы. Многие из них ведут к целым ветвям элементов (таких как фигуры, панели и элементы управления). На заметку! Основные пространства имен WPF начинаются в System.Windows (например, System.Windows , System.Windows.Controls и System.Windows.Media ). Единственным исключением являются пространства имен, начинающиеся с System.Windows. Forms, которые являются частью инструментария Windows Forms.
Book_Pro_WPF-2.indb 41
19.05.2008 18:09:37
42
Глава 1
System.Threading.DispatcherObject Приложения WPF используют знакомую однопоточную модель (single-thread affinity — STA), а это означает, что весь пользовательский интерфейс принадлежит единственному потоку. Взаимодействовать с элементами пользовательского интерфейса из других потоков небезопасно. Чтобы содействовать работе этой модели, каждое приложение WPF управляется диспетчером, координирующим сообщения (появляющиеся в результате клавиатурного ввода, перемещений курсора мыши и процессов каркаса, таких как компоновка). Будучи унаследованным от DispatcherObject, каждый элемент вашего пользовательского интерфейса может удостовериться, выполняется ли код в правильном потоке, и обратиться к диспетчеру, чтобы направить код в поток пользовательского интерфейса. Подробнее о модели многопоточности WPF будет сказано в главе 3.
System.Windows.DependencyObject В WPF центральный путь взаимодействия с экранными элементами пролегает через свойства. На ранней стадии цикла проектирования архитекторы WPF решили создать более мощную модель свойств, которая положена в основу таких средств, как уведомления об изменениях, наследуемые значения по умолчанию и более экономичное хранилище свойств. Конечным результатом стало средство свойств зависимости (dependency property), с которым вы познакомитесь в главе 6. Наследуясь от 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. Например, именно здесь определен двухшаговый процесс измерения и организации компоновки, о котором вы узнаете в главе 4. Здесь же щелчки кнопками мыши и нажатия клавиш трансформируются в более удобные события, такие как MouseEnter. Как и со свойствами, WPF реализует расширенную систему передачи событий, именуемую маршрутизируемыми событиями (routed events). Как она работает — вы узнаете из главы 6. И, наконец, UIElement добавляет поддержку команд (см. главу 10).
System.Windows.FrameworkElement FrameworkElement — конечный пункт в центральном дереве наследования WPF. Он реализует некоторые члены, которые просто определены в UIElement. Например, UIElement устанавливает фундамент для системы компоновки WPF, но FrameworkElement включает ключевые свойства (вроде HorizontalAlignment и Margin), которые поддерживают его. UIElement также добавляет поддержку привязки данных, анимации и стилей — все это центральные средства.
Book_Pro_WPF-2.indb 42
19.05.2008 18:09:37
Введение в WPF
43
System.Windows.Shapes.Shape От этого класса наследуются базовые фигуры, такие как Rectangle , Polygon , Ellipse, Line и Path. Эти фигуры могут быть использованы наряду с более традиционными визуальными элементами Windows вроде кнопок и текстовых полей. Построением фигур мы займемся в главе 13.
System.Windows.Controls.Control Элемент управления (control) — это элемент, который может взаимодействовать с пользователем. К нему, очевидно, относятся такие классы, как TextBox, Button и ListBox. Класс Control добавляет дополнительные свойства для установки шрифта, а также цветов переднего плана и фона. Но наиболее интересная деталь, которую он предоставляет — это поддержка шаблонов, которая позволяет заменять стандартный внешний вид элемента управления вашим собственным рисованием. О шаблонах элементов управления будет сказано в главе 15. На заметку! В программировании с применением Windows Forms каждый визуальный компонент в форме называется элементом управления (control). В WPF это не так. Визуальные единицы называются элементами (element), и только некоторые из них являются элементами управления (те, что могут принимать фокус и взаимодействовать с пользователем). Чтобы еще более запутать эту систему, многие элементы определены в пространстве имен System.Windows.Controls, даже несмотря на то, что они не унаследованы от System.Windows.Controls.Control и не могут считаться элементами управления. Примером может служить класс Panel.
System.Windows.Controls.ContentControl Это базовый класс для всех элементов управления, которые имеют отдельный кусок содержимого. Сюда относится все — от скромной метки Label до окна Window. Наиболее впечатляющая часть этой модели (которая описана детально в главе 5) заключается в том, что единственный кусок содержимого может быть чем угодно — от обычной строки до панели компоновки, содержащей комбинацию других фигур и элементов управления.
System.Windows.Controls.ItemsControl Это базовый класс для всех элементов управления, которые отображают коллекцию каких-то единиц информации, вроде ListBox и TreeView. Списочный элемент управления замечательно гибок; например, используя встроенные средства класса ItemsControl, вы можете трансформировать обычный ListBox в список переключателей, список флажков, ряд картинок или комбинацию совершенно разных элементов по своему выбору. Фактически в WPF все меню, панели инструментов и линейки состояния на самом деле являются специализированными списками, и классы, реализующие их, наследуются от ItemsControl. Вы начнете использовать списки в главе 16, когда пойдет речь о привязке данных. Их расширение вы изучите в главе 17, а наиболее специализированные списочные элементы управления — в главе 18.
System.Windows.Controls.Panel Это базовый класс для всех контейнеров компоновки — элементов, которые содержат в себе один или более дочерних элементов и упорядочивают их в соответствии с определенными правилами компоновки. Эти контейнеры образуют фундамент системы компоновки WPF, и их использование — ключ к упорядочиванию вашего содержимого наиболее привлекательным и гибким способом. Система компоновки WPF более детально рассматривается в главе 4.
Book_Pro_WPF-2.indb 43
19.05.2008 18:09:38
44
Глава 1
Резюме В этой главе был представлен начальный обзор WPF и тех возможностей, которые этот каркас обещает. Вы узнали о лежащей в его основе архитектуре и кратко — об основных его классах. WPF — это начало будущего разработок для Windows. Со временем это станет системой, подобной User32 и GD/GID+, поверх которой будут добавляться новые расширения и высокоуровневые средства. В конечном итоге WPF позволит вам проектировать приложения, которые было бы невозможно (или, по крайней мере, непрактично) построить средствами Windows Forms. Естественно, WPF несет в себе много революционных изменений. Однако есть несколько ключевых принципов, которые нужно немедленно сформулировать, поскольку они совершенно отличаются от тех, что лежат в основе предшествующих инструментов для построения пользовательского интерфейса Windows, таких как Windows Forms. Ниже перечислены эти принципы.
• Аппаратное ускорение. Все рисование WPF выполняется через DirectX, что позволяет ему использовать преимущества современных видеокарт.
• Независимость от разрешения. WPF настолько гибок, что может автоматически выполнять масштабирование вверх и вниз, приспосабливаясь к предпочтениям вашего монитора и дисплея, в зависимости от системных установок DPI.
• Никакого фиксированного внешнего вида элементов управления. В традиционной разработке под Windows существует огромная пропасть между элементами управления, которые можно подогнать под ваши нужды (они называются самостоятельно рисуемыми (ownerdrawn) элементами управления), и теми, которые визуализируются операционной системой, и чей внешний вид, по сути, фиксирован. В WPF все, начиная от базового Rectangle и до стандартного Button или более сложного Toolbar, рисуется посредством механизма визуализации, и является полностью настраиваемым. По этой причине элементы управления WPF часто называют лишенными внешности (lookless controls) — они определяют функциональность элемента управления, но не имеют жестко привязанной внешности.
• Декларативный пользовательский интерфейс. В следующей главе мы рассмотрим XAML — стандарт языка разметки, которые вы используете для определения пользовательских интерфейсов WPF. XAML позволяет строить окна без кода. Впечатляет то, что XAML не ограничивает вас фиксированным неизменным пользовательским интерфейсом. Вы можете применять такие средства, как привязка данных и триггеры, для автоматизации базового поведения пользовательского интерфейса (наподобие текстовых полей, обновляющих себя, когда вы перемещаетесь по источнику записи, или меток, которые подсвечиваются при наведении на них курсора мыши) — и все это без написания единой строки кода C#.
• Рисование на базе объектов. Даже если вы планируете работать на низком визуальном уровне (вместо высокого уровня элементов), вам не придется рисовать в терминах пикселей. Вместо этого вы будете создавать объекты фигур, и позволять WPF поддерживать отображение в наиболее оптимизированной манере. На протяжении всей книги вы увидите эти принципы в действии. Но прежде чем двинуться дальше, стоит изучить еще один дополняющий стандарт. В следующей главе представлен XAML — язык разметки, служащий для определения пользовательских интерфейсов WPF.
Book_Pro_WPF-2.indb 44
19.05.2008 18:09:38
ГЛАВА
2
XAML X
AML (сокращение от Extensible Application Markup Language — расширяемый язык разметки приложений) представляет собой язык разметки, используемый для создания экземпляров объектов .NET. Хотя язык XAML — это технология, которая может быть применима ко многим различным предметным областям, его главное назначение — конструирование пользовательских интерфейсов WPF. Другими словами, документы XAML определяют расположение панелей, кнопок и прочих элементов управления, составляющих окна в приложении WPF. Маловероятно, что вам придется писать код XAML вручную. Вместо этого вы используете инструмент, генерирующий необходимый код XAML. Если вы — графический дизайнер, скорее всего, таким инструментом будет программа рисования и графического дизайна вроде Microsoft Expression Blend. Если же вы — разработчик, то наверняка начнете с Visual Studio. Поскольку оба инструмента поддерживают XAML, вы можете создать базовый пользовательский интерфейс в Visual Studio, а затем передать его команде дизайнеров, которые доведут его до совершенства, добавив специальную графику в Expression Blend. Фактически такая способность интегрировать рабочий поток разработчиков и дизайнеров — одна из ключевых причин создания Microsoft языка XAML. В этой главе вы получите детальное представление XAML. Вы рассмотрите его предназначение, общую архитектуру и синтаксис. Поняв основные правила XAML, вы узнаете, что возможно и что невозможно в пользовательском интерфейсе WPF, и как при необходимости провести в нем ручные изменения. Что более важно, исследуя дескрипторы в документе WPF XAML, вы можете много узнать об объектной модели, которая положена в основу пользовательских интерфейсов WPF, и подготовиться к углубленному ее изучению.
Создание XAML в Visual Studio В этой главе мы рассмотрим детали разметки XAML. Разумеется, когда вы проектируете приложение, вам не приходится писать XAML вручную. Вместо этого вы используете инструмент, подобный Visual Studio, чтобы создать нужное окно методом перетаскивания. Потому вы можете удивиться, стоит ли тратить столько времени на изучение синтаксиса XAML. Ответ — безусловно, да! Понимание XAML чрезвычайно важно при дизайне приложения WPF. В этом отношении приложения WPF существенно отличаются от приложений Windows Forms. В приложениях Windows Forms вы можете спокойно игнорировать автоматически сгенерированный код интерфейса пользователя (UI), в то время как в приложениях WPF код XAML часто занимает главенствующее место. Понимание XAML поможет вам разобраться с ключевыми концепциями WPF, такими как прикрепленные свойства (в этой главе), компоновка (в главе 4), модель содержимого (в главе 5), маршрутизируемые события (главе 6) и т.д. Что более важно — есть целый ряд задач, решение которых возможно только с помощью вручную написанного XAML либо подобным образом существенно облегчается. К ним относятся перечисленные ниже.
Book_Pro_WPF-2.indb 45
19.05.2008 18:09:38
46
Глава 2
• Привязка обработчиков событий. Прикрепление обработчиков событий в наиболее распространенных местах — например, к событию Click для Button — легко сделать в Visual Studio. Однако, однажды поняв, как события привязываются в XAML, вы сможете создавать более изощренные соединения. Например, вы можете установить обработчик событий, реагирующий на событие Click в каждой кнопке окна. Подробнее об этой технике говорится в главе 6. • Определение ресурсов. Ресурсы — это объекты, которые вы определяете однажды в вашем коде XAML, в специальном разделе, а затем повторно используете в разных местах вашего кода разметки. Ресурсы позволяют централизовать и стандартизовать форматирование и создание невизуальных объектов, таких как шаблоны и анимации. Создание и использование ресурсов будет описано в главе 11. • Определение шаблонов элементов управления. Элементы управления WPF проектируются как лишенные внешнего вида; это значит, что вы можете подставлять свои собственные визуальные представления вместо стандартных. Чтобы сделать это, вы должны создать собственный шаблон элемента управления, который представляет собой не что иное, как блок разметки XAML. Шаблоны элементов управления описаны в главе 15. • Написание выражений привязки данных. Привязка данных позволяет извлекать данные из объекта и отображать их в привязанном элементе. Чтобы установить это отношение и конфигурировать его работу, вы должны добавить выражение привязки данных к коду разметки XAML. Привязка данных рассматривается в главе 16. • Определение анимаций. Анимации — распространенный ингредиент приложений XAML. Обычно они определяются как ресурсы, конструируются с использованием разметки XAML, а затем привязываются к другим элементам управления (или инициируются в коде). В настоящее время Visual Studio не предусматривает поддержки создания анимаций во время проектирования. Теме анимации посвящена глава 21. Большинство разработчиков WPF используют комбинацию приемов, разрабатывая часть пользовательского интерфейса с помощью инструмента проектирования (Visual Studio или Expression Blend), а затем проводя тонкую настройку посредством редактирования кода разметки вручную. Однако вы, вероятно, решите, что писать вручную код XAML легче всего, до тех пор, пока не узнаете о контейнерах компоновки из главы 4. Это потому, что для правильного размещения множества элементов управления в окне лучше использовать контейнер компоновки.
Особенности XAML Разработчики давно поняли, что легче всего разрабатывать сложные, графически насыщенные приложения, если отделить графическую часть от лежащего в основе кода. Таким образом, художники могут заниматься графикой, а разработчики — кодом. Обе части могут проектироваться и совершенствоваться по отдельности, без проблем с версиями.
Графический интерфейс пользователя до WPF В традиционных технологиях отображения не существовало простого способа отделить графическое содержимое от кода. Ключевая проблема приложений Windows Forms состоит в том, что каждая форма, которую вы создаете, целиком определяется в коде C#. Когда вы помещаете элементы управления на поверхность проектирования и конфигурируете их, Visual Studio молча вносит изменения в код соответствующего класса формы. К сожалению, графические дизайнеры не имеют инструментов, которые могут работать с кодом C#. Вместо этого художники вынуждены создавать и экспортировать свой продукт в формате битовой карты. Эти битовые карты затем могут использоваться для оформ-
Book_Pro_WPF-2.indb 46
19.05.2008 18:09:38
XAML
47
ления окон, кнопок и других элементов управления. Такой подход хорошо работает с простыми интерфейсами, которые мало изменяются с течением времени, но весьма ограничен в других сценариях. К его проблемам можно отнести перечисленные ниже.
• Каждый графический элемент (фон, кнопка и т.п.) должен быть экспортирован как отдельная битовая карта. Это ограничивает возможности их комбинирования и использования динамических эффектов, таких как сглаживание, прозрачность и тени.
• Значительная часть логики пользовательского интерфейса должна быть встроена в код разработчиком. Сюда относятся размеры кнопок, позиционирование, эффекты от перемещения мыши и анимация. Графический дизайнер не может контролировать эти детали.
• Не существует внутренней связи между разными графическими элементами, так что легко создать несоответствующие друг другу наборы изображений. Отслеживание всех этих элементов привносит дополнительную сложность.
• Битовые карты не могут изменяться в размерах без потерь качества. По этой причине пользовательский интерфейс на основе битовой карты зависит от разрешения. Это значит, что он не может быть адаптирован к большим мониторам и дисплеям высокого разрешения, что нарушает основы философии дизайна WPF. Если вам когда-либо приходилось проходить через процесс проектирования приложений Windows Forms с использованием специальной графики в командной среде, вы, несомненно, сталкивались с массой разочарований. Даже если интерфейс спроектирован с нуля графическим дизайнером, вам нужно воссоздать его в коде C#. Обычно графическому дизайнеру просто приходится подготавливать макет, который вам затем нужно транслировать в создаваемое приложение. WPF решает эту проблему посредством XAML. При проектировании приложения WPF в Visual Studio создаваемое вами окно не транслируется в код. Вместо этого оно сериализуется в набор дескрипторов 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, что управляющие дескрипторы для Webприложений ASP.NET. Отличие состоит в том, что синтаксис дескрипторов ASP.NET задуман похожим на HTML, так что дизайнеры могут создавать Web-страницы, используя обычные приложения для Web-дизайна, такие как FrontPage и Dreamweaver. Как и в WPF, сам код Web-страницы ASP.NET обычно помещается в отдельном файле, чтобы облегчить дизайн.
Book_Pro_WPF-2.indb 47
19.05.2008 18:09:38
48
Глава 2
Варианты XAML Существует несколько разных способов использования термина XAML. До сих пор мы применяли его, чтобы ссылаться на весь язык XAML, предлагающий основанный на XML синтаксис для представления дерева объектов .NET. (Эти объекты могут быть кнопками и текстовыми полями в окне, или же специальным классом, определенным вами. Фактически XAML даже может быть использован на других платформах, чтобы представлять объекты, не имеющие отношения к .NET.) Существует несколько подмножеств XAML.
• WPF XAML включает элементы, описывающие содержимое WPF вроде векторной графики, элементов управления и документов. В настоящее время это наиболее важное применение XAML, и именно это его подмножество мы будем рассматривать в этой книге.
• XPS XAML — часть WPF XAML, определяющая XML-представление форматированных электронных документов. Она опубликована как отдельный стандарт XML Paper Specification (XPS). Вы узнаете о XPS в главе 19.
• Silverlight XAML — подмножество WPF XAML, предназначенное для Silverlight-приложений. Silverlight — это межплатформенный браузерный подключаемый модуль, позволяющий создавать богатое Web-содержимое c двумерной графикой, анимацией, аудио и видео. Дополнительную информацию о Silverlight вы найдете в главе 1. Можете также посетить сайт http://silverlight.net, чтобы ознакомиться с деталями.
• WF XAML включает элементы, описывающие содержимое Windows Workflow Foundation (WF). Дополнительная информация о WF доступна на сайте http://
wf.netfx3.com.
Компиляция XAML Создатели WPF знали, что XAML не только нужен для решения проблемы кооперации дизайна, он также должен быть быстрым. И хотя такие основанные на XML форматы, как XAML, гибки и легко переносимы на другие инструменты и платформы, они не всегда являются наиболее эффективным выбором. XML задуман как непротиворечивый, читабельный и прямолинейный, но не компактный формат. WPF преодолевает этот недостаток посредством BAML (Binary Application Markup Language — двоичный язык разметки приложений). BAML — это не что иное, как двоичное представление XAML. Когда вы компилируете приложение WPF в Visual Studio, все ваши файлы XAML преобразуются в код BAML, и этот код BAML затем встраивается в виде ресурса в финальную сборку DLL или EXE. BAML поддерживает лексемы, а это значит, что длинные куски XAML заменены короткими лексемами. И код BAML не только существенно меньше, но он также оптимизирован таким образом, что он быстрее интерпретируется во время выполнения. Большинству разработчиков не приходится беспокоиться о преобразовании XAML в BAML, потому что компилятор это делает “за кулисами”. Однако можно использовать XAML без предварительной компиляции. Это может иметь смысл в сценариях, когда часть пользовательского интерфейса должна быть применена прямо во время выполнения (например, извлечена из базы данных в виде блока дескрипторов XAML). Ниже, в разделе “Загрузка и компиляция XAML” настоящей главы, вы увидите, как это работает.
Book_Pro_WPF-2.indb 48
19.05.2008 18:09:38
XAML
49
Основы XAML Стандарт XAML достаточно очевиден, если понять несколько его основополагающих правил.
• Каждый элемент в документе XAML отображается на экземпляр класса .NET. Имя элемента соответствует имени класса в точности. Например, элемент сообщает WPF, что должен быть создан объект Button.
• Как и любой документ XML, код XAML допускает вложение одного элемента внутрь другого. Как вы увидите, XAML дает каждому классу гибкость в принятии решения относительно того, как справиться с ситуацией. Однако вложение обычно является способом выразить включение (containment). Другими словами, если вы видите элемент Button внутри элемента Grid, ваш пользовательский интерфейс, вероятно, включает Grid, содержащий внутри себя Button.
• Вы можете устанавливать свойства каждого класса через атрибуты. Однако в некоторых ситуациях атрибуты не достаточно мощны, чтобы справиться с этой работой. В этих случаях вам понадобятся вложенные дескрипторы со специальным синтаксисом. Совет. Если вы — полный новичок в XML, то вам лучше изучить его основы и только затем заняться XAML. Чтобы быстро это сделать, обратитесь к бесплатному Web-руководству по адресу http://www.w3schools.com/xml. Прежде чем продолжить, взгляните на следующий простейший документ XAML, представляющий новое пустое окно (как оно создано в Visual Studio). Строки пронумерованы для облегчения ссылок на них. 1 5 6 7 8
Этот документ включает всего два элемента — элемент верхнего уровня Window, который представляет все окно, и Grid, куда вы можете поместить свои элементы управления. Хотя вы можете использовать любой элемент верхнего уровня, приложение WPF полагается только на несколько из них:
• Window; • Page (похож на Window, но используется для приложений с навигацией); • Application (который определяет ресурсы приложения и начальные установки). Как во всех документах XML, может существовать только один элемент верхнего уровня. В предыдущем примере это означает, что как только вы закрываете элемент Window дескриптором , вы завершаете документ. Никакое дополнительное содержание уже не допускается. Если вы посмотрите на стартовый дескриптор элемента Window, то найдете там несколько интересных атрибутов, включая имя класса и два пространства имен XML (описанных в последующих разделах). Также вы найдете три свойства, показанных ниже: 4
Book_Pro_WPF-2.indb 49
Title="Window1" Height="300" Width="300">
19.05.2008 18:09:38
50
Глава 2
Каждый атрибут соответствует отдельному свойству в классе Window. В конце концов, это инструктирует WPF о необходимости создать окно с заголовком Window1 размером 300×300 единиц. На заметку! Как вам известно из главы 1, WPF использует относительную систему измерения, которая не похожа на то, чего ожидают большинство разработчиков Windows. Вместо того чтобы позволить задавать размеры в физических пикселях, WPF использует независимые от устройства единицы, которые могут масштабироваться для заполнения разных разрешений монитора и определены, как 1/96 часть дюйма. Это значит, что окно размером 300×300 единиц из предыдущего примера будет визуализировано как окно в 300×300 пикселей, если ваша системная установка DPI составляет стандартные 96 dpi. Однако в системах с увеличенным системным DPI будет использовано больше пикселей. Подробности вы найдете в главе 1.
Пространства имен XAML Ясно, что недостаточно просто указать имя класса. Анализатору XAML также нужно знать пространство имен .NET, где находится этот класс. Например, класс Window может находиться в нескольких пространствах имен — он может ссылаться на класс System.Windows.Window, на класс в компоненте от независимого разработчика, или же на класс, определенный в вашем приложении. Чтобы определить, какой именно класс нужен вам на самом деле, анализатор XAML проверяет пространство имен XML, к которому относится элемент. Вот как это работает. В примере документа, показанном ранее, определено два пространства имен: 2 3
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
На заметку! Пространства имен объявляются посредством атрибутов. Эти атрибуты могут помещаться внутри начального дескриптора любого элемента. Однако согласно принятым соглашениям, все пространства имен, которые вам нужно использовать в документе, должны быть объявлены в самом первом дескрипторе, как это сделано в данном примере. Как только пространство имен объявлено, оно может использоваться в любом месте вашего документа.
xmlns — это специализированный атрибут в мире XML, который зарезервирован для объявления пространств имен. Этот фрагмент кода разметки объявляет два пространства имен, которые будут присутствовать в каждом создаваемом вами документе WPF XAML.
• http://schemas.microsoft.com/winfx/2006/xaml/presentation — основное пространство имен WFP. Оно охватывает все классы WPF, включая элементы управления, которые вы применяете для построения пользовательских интерфейсов. В этом примере это пространство имен объявлено без префикса — пространства имен, поэтому становится пространством имен по умолчанию для всего документа. Другими словами, каждый элемент автоматически помещается в это пространство имен, если только вы не укажете иного.
• http://schemas.microsoft.com/winfx/2006/xaml — пространство имен XAML. Оно включает различные служебные свойства XAML, которые позволяют вам влиять на то, как интерпретируется ваш документ. Это пространство имен отображается на префикс x. Это значит, что вы можете применять его, помещая префикс пространства имен перед именем элемента (как в ).
Book_Pro_WPF-2.indb 50
19.05.2008 18:09:39
XAML
51
Как видите, пространство имен XML не соответствует никакому конкретному пространству имен .NET. Есть несколько причин, по которым создатели XML выбрали такой дизайн. По существующему соглашению пространства имен XML часто имеют форму URI (как и в данном примере). Эти URI выглядят так, будто указывают на некоторое место в Web, хотя на самом деле это не так. Формат 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 . Если вы попытаетесь применить эти значения для установки содержимого элемента, то столкнетесь с проблемой, поскольку
Book_Pro_WPF-2.indb 62
19.05.2008 18:09:40
XAML
63
анализатор XAML предположит, что вы пытаетесь сделать что-то другое — например, создать вложенный элемент. Предположим, что вы хотите создать кнопку, которая содержит текст . Следующий код разметки работать не будет:
Проблема в том, что это выглядит так, как будто вы пытаетесь создать элемент по имени Click с атрибутом Me. Решение состоит в замене сомнительных символов сущностными ссылками — специфическими кодами, которые анализатор XAML интерпретирует правильно. В табл. 2.1 перечислены символьные сущности, которые вы можете использовать. Обратите внимание, что символьная сущность типа кавычки требуется только при установке значений с использованием атрибута, так как кавычка обозначает начало и конец значения атрибута.
Таблица 2.1. Символьные сущности XML Специальный символ
Символьная сущность
Меньше ()
>
Амперсанд (&)
&
Кавычка (")
"
Ниже приведен правильный код разметки, использующий соответствующие символьные сущности:
Когда анализатор XAML читает это, он правильно понимает, что вы хотите добавить текст , и передает строку с этим содержимым, дополняя ее угловыми скобками, свойству Button.Content. На заметку! Это ограничение — деталь XAML, которая не коснется вас, если вы захотите установить свойство Button.Content в коде. Конечно, C# имеет свой собственный специальный символ (обратный слэш), который нужно защищать в строковых литералах по той же причине. Специальные символы — не единственная тонкость, с которой вы столкнетесь в XAML. Другая проблема — обработка пробелов. По умолчанию XML сокращает все пробелы, а это значит, что длинная строка пробелов, знаков табуляции и жестких переводов строки превращается в единственный пробел. Более того, если вы добавите пробел перед или после содержимого вашего элемента, этот пробел будет полностью проигнорирован. Вы можете наблюдать это в примере EightBall. Текст на кнопке и два текстовых поля отделены от дескрипторов XAML жестким переводом строки и табуляцией, которые повышают читабельность разметки. Однако этот дополнительный пробел не появляется в пользовательском интерфейсе. Иногда это не то, что вам нужно. Например, вы можете пожелать включать серии из нескольких пробелов в текст вашей кнопки. В этом случае вам следует использовать атрибут xml:space="preserve" в вашем элементе.
Book_Pro_WPF-2.indb 63
19.05.2008 18:09:40
64
Глава 2
Атрибут xml:space — часть стандарта XML, являющаяся установкой в духе “все или ничего”. Однажды включив его, вы предохраните все пробелы внутри элемента. Например, рассмотрим следующую разметку: [There is a lot of space inside these quotation marks " ".]
В этом примере текст в текстовом поле будет включать жесткий перенос строки и знак табуляции перед текстом. Также он содержит серии пробелов внутри текста и перенос строки после текста. Если вы хотите только предохранить пробелы внутри, то вам придется применить менее читабельную разметку: [There is a lot of space inside these quotation marks " ".]
Трюк здесь состоит в том, что нужно убедиться в том, чтобы не было пробелов между открывающим > и вашим содержимым, или между вашим содержимым и закрывающим 0) { string file = e.Args[0]; if (System.IO.File.Exists(file)) { // Конфигурировать главное окно. win.LoadFile(file); } } else { // (Выполнить альтернативную инициализацию // когда никаких аргументов командной строки не указано.) } // Это окно будет автоматически установлено, как Application.MainWindow. win.Show(); } }
Этот метод инициализирует главное окно, которое затем отображается по завершении метода App_Startup(). Этот код предполагает, что в классе FileViewer имеется
Book_Pro_WPF-2.indb 82
19.05.2008 18:09:43
Класс Application
83
общедоступный метод (добавленный вами) по имени LoadFile(). Приведем один возможный пример этого метода, который просто читает (и отображает) текст указанного вами файла: public partial class FileViewer : Window { ... public void LoadFile(string path) { this.Content = File.ReadAllText(path); this.Title = path; } }
Вы можете испытать пример этой техники, используя код примеров для этой главы. На заметку! Если вы — опытный программист Windows Forms, то код метода LoadFile() может вам показаться несколько странным. Он устанавливает свойство Content текущего Window, что определяет то, что окно отобразит в своей клиентской области. Довольно интересно, что окна WPF на самом деле являются разновидностью элемента управления содержимым (в том смысле, что наследуются от класса ContentControl). В результате этого они содержат (и отображают) единственный объект. Вы определяете, является ли этот объект строкой, элементом управления или (что полезнее) панелью, которая может содержать множество элементов управления. В последующих главах вы узнаете больше о модели содержимого, принятой в WPF.
Доступ к текущему приложению Вы можете получить экземпляр текущего приложения из любой точки его кода, обратившись к свойству Application.Current. Это обеспечивает возможность простейшего взаимодействия между окнами, поскольку любое окно может получить доступ к текущему объекту Application и через него — к ссылке на главное окно: Window main = Application.Current.MainWindow; MessageBox.Show("The main window is " + main.Title);
Конечно, если вы хотите получить доступ к любым методам, свойствам или событиям, которые вы добавили к своему специальному классу главного окна, вам придется выполнить приведение объекта окна к соответствующему типу. Если главное окно представляет собой экземпляр специального класса MainWindow, вы можете использовать следующий код: MainWindow main = (MainWindow)Application.Current.MainWindow; main.DoSomething();
Окно может также проверить содержимое коллекции Application.Windows, которая содержит ссылки на все открытые в данный момент окна: foreach (Window window in Application.Current.Windows) { MessageBox.Show(window.Title + " is open."); }
На практике большинство приложений предпочитают использовать более структурированную форму взаимодействия между окнами. Если у вас есть несколько долго выполняющихся окон, которые открыты одновременно, и которым нужно как-то взаимодействовать между собой, имеет больше смысла хранить ссылки на эти окна в специальном классе приложения. Таким образом, вы всегда сможете найти именно то окно,
Book_Pro_WPF-2.indb 83
19.05.2008 18:09:43
84
Глава 3
которое вам нужно. Аналогично, если у вас приложение, основанное на документах, вы можете предпочесть создание коллекции, отслеживающей только окна документов и ничего больше. Эту технику мы рассмотрим в следующем разделе. На заметку! Окна (включая главное) добавляются в коллекцию Windows по мере отображения и удаляются из нее при закрытии. По этой причине положение окон в коллекции может изменяться, и вы не можете рассчитывать найти определенный объект окна в определенной позиции.
Взаимодействие между окнами Как вы уже видели, специальный класс приложения — отличное место для размещения кода, реагирующего на разные события приложения. Есть еще одно предназначение, которое замечательно подходит классу Application: хранение ссылок на важные окна, так чтобы одно окно могло обращаться к другому. Совет. Эта техника имеет смысл, когда у вас есть немодальное окно, существующее в течение длительного времени и доступное нескольким разным классам (а не только классу, создавшему его). Если вы просто отображаете модальное диалоговое окно как часть приложения, такая техника чрезмерна (“из пушки по воробьям”). В этой ситуации окно не может существовать очень долго, и только код, создающий окно, нуждается в доступе к нему. (Чтобы прояснить разницу между модальными окнами, которые прерывают поток выполнения приложения до тех пор, пока они не закрыты, и немодальными окнами, которые этого не делают, обратитесь к главе 8.) Например, предположим, что вы хотите отслеживать все окна документов, с которыми работает ваше приложение. В этом случае вы можете создать выделенную коллекцию в вашем специальном классе приложения. Приведем пример, использующий обобщенную коллекцию List для хранения группы специальных оконных объектов. В этом примере каждое окно документа представлено экземпляром класс по имени Document. public partial class App : Application { private List documents = new List(); public List Documents { get { return documents; } set { documents = value; } } }
Теперь, когда вы создаете новый документ, то должны просто не забыть добавить его к коллекции Documents. Вот обработчик события, который реагирует на щелчок кнопки и выполняет эту задачу: private void cmdCreate_Click(object sender, RoutedEventArgs e) { Document doc = new Document(); doc.Owner = this; doc.Show(); ((App)Application.Current).Documents.Add(doc); }
Альтернативно вы могли бы реагировать на событие вроде Window.Loaded в классе Document, чтобы гарантировать, что объект документа всегда зарегистрирует себя в коллекции Documents при его создании.
Book_Pro_WPF-2.indb 84
19.05.2008 18:09:43
85
Класс Application
На заметку! Этот код также устанавливает свойство Window.Owner таким образом, что все окна документов отображаются “поверх” главного окна, которое их создает. Вы узнаете больше о свойстве Owner, когда мы будем детально рассматривать окна в главе 8. Теперь вы можете использовать эту коллекцию в любом месте вашего кода, чтобы проходить циклом по всем документам и использовать их общедоступные члены. В этом случае класс Document включает специальный метод SetContent() для обновления дисплея: private void cmdUpdate_Click(object sender, RoutedEventArgs e) { foreach (Document doc in ((App)Application.Current).Documents) { doc.SetContent("Refreshed at " + DateTime.Now.ToLongTimeString() + "."); } }
Внешний вид этого приложения показан на рис. 3.1. Конечный результат не столь уж впечатляющ, но взаимодействие достойно внимания — оно демонстрирует безопасный дисциплинированный способ взаимодействия ваших окон через специальный класс приложения. Это великолепно — использовать свойство Windows, поскольку оно строго типизировано и содержит только окна Document (а не коллекцию вообще всех окон вашего приложения). Оно также дает вам возможность категоризации всех окон другим, более удобным способом, например, в коллекции Dictionary с ключевыми именами для облегчения поиска. В приложении на основе документов вы можете индексировать окна в коллекции по имени файлов.
Рис. 3.1. Обеспечение взаимодействия окон На заметку! При взаимодействии между окнами не забывайте об объектно-ориентированных принципах — всегда используйте слой специальных методов, свойств и событий, которые вы добавляете к классам окон. Никогда не открывайте прямой доступ к полям или элементам управления формы другим частям вашего кода. Если вы сделаете это, то быстро придете к тесно связанному интерфейсу, в котором одно окно глубоко вмешивается в работу другого, и вы не сможете расширять класс, не нарушая “нечеткой” взаимной зависимости между ними.
Book_Pro_WPF-2.indb 85
19.05.2008 18:09:43
86
Глава 3
Приложение одного экземпляра Обычно вы можете запустить столько копий приложения WPF, сколько вы хотите. В некоторых сценариях этот дизайн совершенно оправдан. Однако в других случаях это может стать проблемой, особенно при построении приложений, основанных на документах. Например, рассмотрим Microsoft Word. Независимо от того, сколько документов вы открываете (или как вы открываете их), только единственный экземпляр winword.exe может быть загружен в каждый момент времени. При открытии новых документов они появляются в новых окнах, но единственное приложение управляет всеми окнами документов. Такой дизайн представляет собой наилучший подход, если вы хотите сократить накладные расходы вашего приложения, централизовать определенные средства (например, создать единый диспетчер очереди печати) или интегрировать разнородные окна (например, предоставить средство, которое упорядочит все текущие открытые окна документов, расположив их рядом друг с другом). В WPF не предусмотрено “родного” решения для приложений одного экземпляра, но вы можете использовать несколько обходных маневров. Базовая техника заключается в проверке существования другого запущенного экземпляра вашего приложения при возникновении события Application.Startup. Простейший путь сделать это состоит в использовании системного мьютекса (объекта синхронизации, предоставляемого операционной системой, позволяющей межпроцессное взаимодействие). Этот подход прост, но ограничен — важнее всего то, что при этом не существует возможности взаимодействия нового экземпляра приложения с уже существующим. Это представляет проблему для приложений, основанных на документах, потому что новому экземпляру может понадобиться сообщить существующему экземпляру о необходимости открытия определенного документа, если он передан в командной строке. (Например, когда вы выполняете двойной щелчок на файле .doc в Windows Explorer, а Word уже запущен, то вы ожидаете, что Word загрузит затребованный файл.) Это взаимодействие более сложно и обычно осуществляется через Remoting или Windows Communication Foundation (WCF). Правильная реализация требует включения способа нахождения удаленного сервера и использования его для передачи аргументов командной строки. Но простейший подход, рекомендованный командой WPF, заключается в использовании встроенной поддержки, которая предоставляется в Windows Forms, и изначально предназначалась для приложений Visual Basic. Этот подход обрабатывает все запутанные детали “за rekbcfvb”. Итак, каким образом вы можете использовать средство, предназначенное для Windows Forms и Visual Basic, для управления приложением WPF на C#? По сути, класс приложения старого стиля служит оболочкой для вашего класса приложения WPF. Когда запускается ваше приложение, вы создаете класс приложения старого стиля, который затем создаст класс приложения WPF. Класс приложения старого стиля осуществляет управление экземплярами, в то время как класс приложения WPF обслуживает реальное приложение. На рис. 3.2 показано взаимодействие этих частей. WindowsFormsApplicationBase (в Microsoft.VisualBasic.ApplicationServices)
Application (в System.Windows)
Window Создать
OnStartup()
OnStartupNextInstance()
Создать
Активизировать
Window
OnStartup()
LoadDocument() (пользовательский метод)
Создать
Рис. 3.2. Упаковка приложения WPF в оболочку класса WindowsFormsApplicationBase
Book_Pro_WPF-2.indb 86
19.05.2008 18:09:43
Класс Application
87
Первый шаг к применению этого подхода заключается в добавлении ссылки на сборку Microsoft.VisualBasic.dll и наследовании специального класса от класса Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase. Этот класс обеспечивает три важных члена, которые вы используете для управления экземплярами.
• Свойство IsSingleInstance позволяет создать приложение одного экземпляра. Вы устанавливаете это свойство в true в конструкторе.
• Метод OnStartup() инициируется при старте приложения. Вы переопределяете этот метод и создаете в этой точке объект приложения WPF.
• Метод OnStartupNextInstance() инициируется, когда запускается другой экземпляр приложения. Этот метод обеспечивает доступ к аргументам командной строки. В этой точке вы, вероятно, вызовете метод класса вашего приложения WPF, чтобы отобразить новое окно, не создавая другого объекта приложения. Ниже приведен код специального класса, унаследованного от WindowsForms
ApplicationBase. public class SingleInstanceApplicationWrapper : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase { public SingleInstanceApplicationWrapper() { // Включить режим одного экземпляра. this.IsSingleInstance = true; } // Создать класс приложения WPF. private WpfApp app; protected override bool OnStartup( Microsoft.VisualBasic.ApplicationServices.StartupEventArgs e) { app = new WpfApp(); app.Run(); return false; } // Обработка попытки создания более одного экземпляра. protected override void OnStartupNextInstance( Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs e) { if (e.CommandLine.Count > 0) { app.ShowDocument(e.CommandLine[0]); } } }
Когда запускается приложение, этот класс создает экземпляр WpfApp, представляющего собой специальный класс приложения WPF (класс, унаследованный от System. Windows.Application). Класс WpfApp включает некоторую стартовую логику, которая отображает главное окно, наряду со специальным методом ShowDocument(), который загружает окно документа для заданного файла. Каждый раз, когда имя файла передается SingleInstanceApplicationWrapper через командную строку, SingleInstanceApplicationWrapper вызывает WpfApp.ShowDocument(). Вот как выглядит код класса WpfApp:
Book_Pro_WPF-2.indb 87
19.05.2008 18:09:43
88
Глава 3 public class WpfApp : System.Windows.Application { protected override void OnStartup(System.Windows.StartupEventArgs e) { base.OnStartup(e); WpfApp.current = this; // Загрузить главное окно. DocumentList list = new DocumentList(); this.MainWindow = list; list.Show(); // Загрузить документ, специфицированный в качестве аргумента. if (e.Args.Length > 0) ShowDocument(e.Args[0]); } public void ShowDocument(string filename) { try { Document doc = new Document(); doc.LoadFile(filename); doc.Owner = this.MainWindow; doc.Show(); // Если приложение уже загружено, оно может быть невидимым. // Здесь выполняется попытка передать фокус новому окну. doc.Activate(); } catch { MessageBox.Show("Could not load document."); } } }
Единственная деталь, которой здесь не хватает (помимо окон DocumentList и Document) — это точка входа приложения. Поскольку приложение должно создать класс SingleInstanceApplicationWrapper перед классом App, приложение должно стартовать с традиционного метода Main() вместо файла App.xaml. Ниже показан необходимый код. public class Startup { [STAThread] public static void Main(string[] args) { SingleInstanceApplicationWrapper wrapper = new SingleInstanceApplicationWrapper(); wrapper.Run(args); } }
Эти три класса — SingleInstanceApplicationWrapper, WpfApp и Startup — формируют базу для приложения WPF одного экземпляра. Используя эту основу, можно создать более изощренный пример. Например, в загружаемом коде для этой главы класс WpfApp модифицируется так, что поддерживает список открытых документов (как было показано ранее). Используя привязку данных WPF (средство, описанное в главе 16), окно DocumentList отображает текущие открытые документы. На рис. 3.3 показан пример приложения с тремя открытыми документами.
Book_Pro_WPF-2.indb 88
19.05.2008 18:09:43
Класс Application
89
Рис. 3.3. Приложение одного экземпляра с центральным окном И, наконец, пример SingleInstanceApplication включает класс FileRegistrationHelper, который регистрирует расширение файла, используя классы из пространства имен Microsoft.Win32: string extension = ".testDoc"; string title = "SingleInstanceApplication"; string extensionDescription = "A Test Document"; FileRegistrationHelper.SetFileAssociation( extension, title + "." + extensionDescription);
Этот код должен быть выполнен только однажды. После того, как регистрация завершена, всякий двойной щелчок на файле с расширением .testDoc запускает SingleInstanceApplication, и файл передается в виде аргумента командной строки. Если SingleInstanceApplication уже запущен, то вызывается метод SingleInstance ApplicationWrapper.OnStartupNextInstance(), и существующим приложением загружается новый документ. На заметку! Поддержка приложение одного экземпляра, в конце концов, появится в будущей версии WPF. А пока этот обходной путь предоставляет ту же функциональность, требуя лишь небольшой дополнительной работы.
Windows Vista и UAC Регистрация файла — это задача, которая обычно выполняет программой установки. Проблема с включением ее в код вашего приложения состоит в том, что она требует повышенных привилегий, которых может не иметь пользователь, запустивший приложение. Это, в частности, представляет
Book_Pro_WPF-2.indb 89
19.05.2008 18:09:44
90
Глава 3
проблему со средством User Account Control (UAC — контроль учетных записей пользователей) в Windows Vista. Фактически по умолчанию этот код завершится сбоем с генерацией исключения, связанного с безопасностью. С точки зрения UAC все приложения имеют один из трех уровней выполнения.
• asinvoker. Приложение наследует маркер процесса от родительского процесса (процесса, запустившего его). Приложение не получит административных привилегий, если только пользователь специально не запросит их — даже если пользователь зарегистрирован как администратор. Это принимается по умолчанию.
• requireAdministrator. Если текущий пользователь является членом группы Administrators (Администраторы), появится диалог подтверждения UAC. Как только пользователь примет подтверждение, приложение получит административные привилегии. Если же пользователь не является членом группы Administrators, появится диалоговое окно, где пользователь сможет ввести имя и пароль учетной записи, обладающей административными привилегиями.
• highestAvailable. Приложение получает максимальные привилегии согласно членству в группах. Например, если текущий пользователь — член группы Administrators, то приложение получает административные привилегии (как только примет подтверждение UAC). Преимущество этого уровня выполнения в том, что приложение продолжит выполнение, если административные привилегии недоступны, в отличие от requireAdministrator. Обычно ваше приложение выполняется с уровнем asinvoker. Чтобы запросить административные привилегии, при запуске вы должны выполнить щелчок правой кнопкой мыши на EXE-файле и выбрать в контекстном меню команду Run As Administrator (Запуск от имени администратора). Чтобы получить административные привилегии при тестировании вашего приложения в среде Visual Studio, вы должны выполнить щелчок правой кнопкой мыши на ярлыке Visual Studio и выбрать в контекстном меню команду Run As Administrator. Если вашему приложению требуются административные привилегии, вы можете запросить их посредством уровня выполнения requireAdministrator или highestAvailable. В любом случае вы должны создать манифест — файл с блоком XML, который будет встроен в вашу скомпилированную сборку. Чтобы добавить манифест, выполните щелчок правой кнопкой мыши на вашем проекте в Solution Explorer и выберите в контекстном меню команду AddNew Item (ДобавитьНовый элемент). Укажите шаблон Application Manifest File (Файл манифеста приложения) и щелкните на кнопке Add (Добавить). Содержимое файла манифеста представлено относительно простым блоком XML, показанным ниже: Чтобы изменить уровень выполнения, просто модифицируйте атрибут уровня элемента . Допустимыми значениями являются asInvoker , requireAdministrator и highestAvailable.
Book_Pro_WPF-2.indb 90
19.05.2008 18:09:44
Класс Application
91
В некоторых случаях вы можете запросить административные привилегии в определенных сценариях. В примере с регистрацией файла вы можете предпочесть запрашивать административные привилегии только при первом запуске приложения, когда оно нуждается в регистрации. Это позволит избежать ненужных предупреждений UAC. Простейший способ реализовать этот шаблон заключается в размещении кода, который требует повышенных привилегий, в отдельном исполняемом модуле, который вы можете вызывать при необходимости.
Резюме В этой главе был дан поверхностный экскурс в модель приложения WPF. Чтобы управлять простым приложением WPF, вам не нужно делать ничего, кроме создания класса Application и вызова метода Run(). Однако большинство приложений идут дальше и наследуют специальный класс от Application. И как вы видели, этот специальный класс является идеальным инструментом для обработки событий приложения и идеальным местом для отслеживания окон вашего приложения или реализации шаблона приложения одного экземпляра. Пока мы не погружались в изучение всех богатств класса Application — еще нужно рассмотреть коллекцию Resources, где вы можете определять объекты, которые хотите многократно использовать по всему вашему приложению, такие как стили, которые могут быть применимы к элементам управления во множестве окон. Однако пока все эти детали нужно отложить до главы 11, когда вы ознакомитесь с моделью ресурсов WPF более подробно. Вместо этого в следующей главе мы поговорим о том, как организовать элементы управления в реалистичных окнах с помощью панелей компоновки WPF.
Book_Pro_WPF-2.indb 91
19.05.2008 18:09:44
ГЛАВА
4
Компоновка П
оловина всех усилий при проектировании пользовательского интерфейса уходит на организацию содержимого, чтобы она была привлекательной, практичной и гибкой. Но реальный вызов состоит в том, чтобы убедиться в том, что ваша компоновка элементов интерфейса сможет успешно адаптироваться к разным размерам окна. В WPF вы разделяете компоновку, используя разные контейнеры. Каждый контейнер имеет свою собственную логику компоновки — некоторые складывают элементы в стопку, другие распределяют их по ячейкам сетки и т.д. Если вы программировали с применением Windows Forms, то будете удивлены, что компоновка на основе координат в WPF не рекомендована к использованию. Вместо этого упор делается на создание более гибких компоновок, которые могут адаптироваться к изменяющемуся содержимому, разным языкам и широкому разнообразию размеров окон. Для большинства разработчиков переход к WPF с его новой системой компоновки становится большим сюрпризом — и первой реальной сложностью. В этой главе вы увидите, как работает модель компоновки WPF, и начнете использовать базовые контейнеры компоновки. Также вы рассмотрите несколько примеров распространенной компоновки — от базового диалогового окна к разделенному окну изменяемого размера, — чтобы изучить основы компоновки WPF.
Понятие компоновки в WPF Модель компоновки WPF отражает существенные изменения подхода разработчиков Windows к проектированию пользовательских интерфейсов. Чтобы понять новую модель компоновки WPF, стоит посмотреть на то, что ей предшествовало. В .NET 1.0 каркас Windows Forms представил весьма примитивную систему компоновки. Элементы управления были фиксированы на месте по жестко закодированным координатам. Единственным удобством были привязка (anchoring) и стыковка (docking) — два средства, которые позволяли элементам управления перемещаться и изменять свои размеры вместе с их контейнером. Привязка и стыковка были незаменимы для создания простых окон изменяемого размера, например, с привязкой кнопок OK и Cancel (Отмена) к нижнему правому углу окна, либо когда нужно было заставить элемент TreeView разворачиваться для заполнения всей формы. Однако они не могли справиться с более сложными задачами компоновки. Например, привязка и стыковка не позволяли организовать пропорциональное изменение размеров двухпанельных окон (с равномерным разделением дополнительного пространства между двумя областями). Они также не слишком помогали в случае высокодинамичного содержимого, например, когда нужно было дать возможность метке расширяться, чтобы вместить больше текста, что приводило к перекрытию соседних элементов управления. В .NET 2.0 каркас Windows Forms заполнил пробел, благодаря двум новым контейнерам компоновки: FlowLayoutPanel и TableLayoutPanel. Используя эти элементы
Book_Pro_WPF-2.indb 92
19.05.2008 18:09:44
Компоновка
93
управления, вы можете создавать более изощренные интерфейсы в стиле Web. Оба контейнера компоновки позволяли содержащимся в них элементам управления увеличиваться, расталкивая соседние элементы. Это облегчило задачу создания динамического содержимого, создания модульных интерфейсов и локализации вашего приложения. Однако панели компоновки выглядели дополнением к основной системе компоновки Windows Forms, использовавшей фиксированные координаты. Панели компоновки были элегантным решением, но все-таки несколько чужеродным. WPF предлагает новую систему компоновки, которая была навеяна опытом разработки в Windows Forms. Эта система возвращает модель .NET 2.0 (координатную компоновку с необязательными потоковыми панелями компоновки), сделав потоковую (flow-based) компоновку стандартной и предоставив лишь рудиментарную поддержку координатной компоновки. Преимущества подобного сдвига огромны. Разработчики могут теперь создавать независящие от разрешения и от размера интерфейсы, которые масштабируются на разных мониторах, подгоняют себя при изменении содержимого и легко обрабатывают перевод на другие языки. Однако прежде чем вы воспользуетесь преимуществом этих изменений, вам следует перестроить ваш образ мышления относительно компоновки.
Философия компоновки WPF Окно WPF может содержать только один элемент. Чтобы разместить более одного элемента и создать более практичный пользовательский интерфейс, вам нужно поместить в окно контейнер и затем добавлять элементы в этот контейнер. На заметку! Это ограничение обусловлено тем фактом, что класс Window наследуется от ContentControl, который вы изучите более подробно в главе 5. В WPF компоновка определяется используемым контейнером. Хотя есть несколько контейнеров, среди которых можно выбирать, “идеальное” окно WPF следует описанным ниже ключевым принципам.
• Элементы (такие как элементы управления) не должны иметь явно установленных размеров. Вместо этого они растут, чтобы вместить их содержимое. Например, кнопка увеличивается, когда вы добавляете в нее текст. Вы можете ограничить элементы управления приемлемыми размерами, устанавливая максимальное и минимальное их значение.
• Элементы не указывают свою позицию в экранных координатах. Вместо этого они упорядочиваются своим контейнером на основе размера, порядка и (необязательно) другой информации, специфичной для контейнера компоновки. Если вы хотите добавить пробел между элементами, то используете для этого свойство Margin. На заметку! Жестко закодированные размеры позиции — зло, потому что они ограничивают ваши возможности по локализации интерфейса и значительно затрудняют работу с динамическим содержимым.
• Контейнеры компоновки “разделяют” доступное пространство между своими дочерними элементами. Они пытаются предоставить каждому элементу его предпочтительный размер (на основе его содержимого), если позволяет свободное пространство. Они могут также выделять дополнительное пространство одному или более дочерним элементам.
• Контейнеры компоновки могут быть вложенными. Типичный пользовательский интерфейс начинается с Grid — наиболее развитого контейнера, и содержит дру-
Book_Pro_WPF-2.indb 93
19.05.2008 18:09:44
94
Глава 4 гие контейнеры компоновки, которые организуют меньшие группы элементов, такие как текстовые поля с метками, элементы списка, пиктограммы в панели инструментов, колонки кнопок и т.д.
Хотя из этих правил существуют исключения, они отражают общие цели проектирования WPF. Другими словами, если вы последуете этим руководствам при построении WPF-приложения, то получите лучший, более гибкий пользовательский интерфейс. Если же вы нарушаете эти правила, то получите пользовательский интерфейс, который не будет хорошо подходить для WPF и его будет значительно сложнее сопровождать.
Процесс компоновки Компоновка WPF происходит за две стадии: стадия измерения и стадия расстановки. На стадии измерения контейнер выполняет проход в цикле по дочерним элементам и опрашивает их предпочтительные размеры. На стадии расстановки контейнер помещает дочерние элементы в соответствующие позиции. Конечно, элемент не может всегда иметь свой предпочтительный размер — иногда контейнер недостаточно велик, чтобы обеспечить его. В этом случае контейнер должен усекать такой элемент для того, чтобы вместить его в видимую область. Как вы убедитесь, часто можно избежать такой ситуации, устанавливая минимальный размер окна. На заметку! Контейнеры компоновки не поддерживают прокрутку. Вместо этого прокрутка обеспечивается специализированным элементом управления содержимым — ScrollViewer, — который может быть использован почти где угодно. В главе 5 вы узнаете больше о ScrollViewer.
Контейнеры компоновки Все контейнеры компоновки WPF являются панелями, которые унаследованы от абстрактного класса System.Windows.Controls.Panel (рис. 4.1). Класс Panel добавляет небольшой набор членов, включая три общедоступных свойства, описанные в табл. 4.1.
DispatcherObject
DependencyObject
Условные обозначения Абстрактный класс Конкретный класс
Visual
UIElement
FrameworkElement
Panel Рис. 4.1. Иерархия класса Panel
Book_Pro_WPF-2.indb 94
19.05.2008 18:09:44
Компоновка
95
Таблица 4.1. Общедоступные свойства класса Panel Имя
Описание
Background
Кисть, используемая для рисования фона панели. Вы должны устанавливать это свойство в отличное от null значение, если хотите принимать события мыши. (Если вы хотите принимать события мыши, но не желаете отображать сплошной фон, просто установите прозрачный цвет фона — Transparent.) Из главы 7 вы узнаете больше о базовых кистях (о более развитых кистях читайте в главе 13).
Children
Коллекция элементов, находящихся в панели. Это первый уровень элементов — другими словами, это элементы, которые сами могут содержать в себе другие элементы.
IsItemsHost
Булевское значение, равное true, если панель используется для показа элементов, ассоциированных с ItemsControl (такие как узлы из TreeView или элементы списка ListBox). Большую часть времени вы даже не будете знать о том, что элемент-список используется “за кулисами” панелью для управления компоновкой элементов. Однако эта деталь становится более важной, если вы хотите создать специальный список, который будет располагать свои дочерние элементы другим способом (например, ListBox, отображающий графические изображения). Вы используете эту технику в главе 17.
На заметку! Класс Panel также имеет некоторый внутренний механизм, который вы можете использовать, если хотите создать собственный контейнер компоновки. Но важнее то, что вы можете переопределить методы MeasureOverride() и ArrangeOverride(), унаследованные от FrameworkElement, для изменения способа обработки панелью стадий измерения и расстановки при организации дочерних элементов. Вы узнаете, как создать специальную панель в главе 24. Сам по себе базовый класс Panel — это не что иное, как начальная точка для построения других более специализированных классов. WPF предлагает ряд производных от Panel классов, которые вы можете использовать для организации компоновки. Самые основные перечислены в табл. 4.2. Как и все элементы управления WPF, а также большинство визуальных элементов, эти классы находятся в пространстве имен System.Windows.Controls.
Таблица 4.2. Основные панели компоновки Имя
Описание
StackPanel
Размещает элементы в горизонтальный или вертикальный стек. Этот контейнер компоновки обычно используется в небольших секциях крупного более сложного окна.
WrapPanel
Размещает элементы в сериях строк с переносом. В горизонтальной ориентации WrapPanel располагает элементы в строке слева направо, затем переходит к следующей строке. В вертикальной ориентации WrapPanel располагает элементы сверху вниз, используя дополнительные колонки для дополнения оставшихся элементов.
DockPanel
Выравнивает элементы по краю контейнера.
Grid
Выстраивает элементы в строки и колонки невидимой таблицы. Это один из наиболее гибких и широко используемых контейнеров компоновки.
Book_Pro_WPF-2.indb 95
19.05.2008 18:09:44
96
Глава 4 Окончание табл. 4.2
Имя
Описание
UniformGrid
Помещает элементы в невидимую таблицу, устанавливая одинаковый размер для всех ячеек. Данный контейнер компоновки используется нечасто.
Canvas
Позволяет элементам позиционироваться абсолютно — по фиксированным координатам. Этот контейнер компоновки более всего похож на традиционный компоновщик Windows Forms, но не предусматривает средств привязки и стыковки. В результате это неподходящий выбор для окон переменного размера, если только вы не собираетесь взвалить на свои плечи значительный объем работы.
Наряду с этими центральными контейнерами есть еще несколько более специализированных панелей, которые вы встретите во многих элементах управления. К ним относятся панели, предназначенные для хранения дочерних элементов определенного элемента управления, такого как TabPanel (вкладки в TabControl), ToolbarPanel (кнопки в Toolbar) и ToolbarOverflowPanel (команды в выпадающем меню Toolbar). Имеется еще VirtualizingStackPanel, чей привязанный к данным элемент-список используется для минимизации накладных расходов, а также InkCanvas, который подобен Canvas, но имеет поддержку перьевого ввода на TabletPC. (Например, в зависимости от выбранного режима, InkCanvas поддерживает рисование с указателем для выбора экранных элементов. Хотя это не очень удобно, но вы можете использовать InkCanvas на обычном компьютере с мышью.)
Простая компоновка с помощью StackPanel StackPanel — один из простейших контейнеров компоновки. Он просто укладывает свои дочерние элементы в одну строку или колонку. Например, рассмотрим следующее окно, которое содержит стек из трех кнопок: A Button Stack Button 1 Button 2 Button 3 Button 4
На рис. 4.2 показанное полученное в результате окно.
Использование StackPanel в Visual Studio Этот пример сравнительно просто создать, используя дизайнер Visual Studio. Начните с удаления корневого элемента Grid (если он есть). Затем перетащите StackPanel в окно. После этого перетащите другие элементы (метку и четыре кнопки) в окно, в желаемом порядке сверху вниз. Если вы хотите реорганизовать элементы в StackPanel, вы не можете просто перетаскивать их. Вместо этого выполните щелчок правой кнопкой мыши на нужном элементе и выберите в контекстном меню команду Order (Упорядочить). Опции упорядочивания соответствует поряд-
Book_Pro_WPF-2.indb 96
19.05.2008 18:09:45
Компоновка
97
ку элементов в разметке, с первым элементом, занимающим последнюю позицию, и последним элементом, занимающим первую позицию. Таким образом, вы можете перемещать элемент вниз панели StackPanel (используя Bring to Front (На передний план)), вверх (используя Send to Back (На задний план)) либо на одну позицию вверх или вниз (используя Bring Forward (Вперед) и Send Backward (Назад)). При создании пользовательского интерфейса в Visual Studio вы должны учитывать некоторые нюансы. Когда вы перетаскиваете элементы из панели инструментов в окно, Visual Studio добавляет некоторые детали в вашу разметку. Среда Visual Studio автоматически присваивает имя каждому новому элементу управления (что безвредно, но излишне). Также добавляются жестко закодированные значения Width и Height, что ограничивает намного больше. Как уже говорилось ранее, явные размеры ограничивают гибкость вашего пользовательского интерфейса. Во многих случаях лучше позволить элементам управления самостоятельно устанавливать свои размеры, подгоняя их к контейнеру. В данном примере фиксированные размеры представляют собой оправданный подход, чтобы установить для всех кнопок согласованную ширину. Однако более удачное решение состояло бы в том, чтобы позволить самой большой кнопке устанавливать свой размер самостоятельно, вмещая свое содержимое, а все остальные кнопки — растянуть до размера большой, чтобы они соответствовали друг другу. (Такой дизайн, требующий применения Grid, описан далее в этой главе.) Но независимо от того, какой подход вы используете с кнопкой, почти наверняка вы захотите избавиться от жестко закодированных величин Width и Height для StackPanel, чтобы она могла растягиваться и сжиматься, заполняя доступное пространство окна.
Рис. 4.2. StackPanel в действии По умолчанию StackPanel располагает элементы сверху вниз, устанавливая высоту каждого из них такой, которая необходима для отображения его содержимого. В данном примере это значит, что размер меток и кнопок устанавливается достаточно большим для комфортабельного размещения текста внутри них. Все элементы растягиваются на полную ширину StackPanel, которая равна ширине окна. Если вы расширите окно, StackPanel также расширится, и кнопки растянутся, чтобы заполнить ее. StackPanel может также использоваться для упорядочивания элементов в горизонтальном направлении посредством установки свойства Orientation:
Теперь элементы получают свою минимальную ширину (достаточную, чтобы вместить их текст) и растягиваются до полной высоты, чтобы заполнить содержащую их панель. В зависимости от текущего размера окна, это может привести к тому, что некоторые элементы не поместятся, как показано на рис. 4.3.
Book_Pro_WPF-2.indb 97
19.05.2008 18:09:45
98
Глава 4
Рис. 4.3. StackPanel с горизонтальной ориентацией Ясно, что это не обеспечивает достаточной гибкости, необходимой реальному приложению. К счастью, вы можете тонко настраивать способ работы StackPanel и других контейнеров компоновки посредством свойств компоновки, как описано ниже.
Свойства компоновки Хотя компоновка определяется контейнером, дочерние элементы тоже могут сказать свое слово. Фактически панели компоновки взаимодействуют со своими дочерними элементами через небольшой набор свойств компоновки, перечисленных в табл. 4.3.
Таблица 4.3. Свойства компоновки Наименование
Описание
HorizontalAlignment
Определяет позиционирование дочернего элемента внутри контейнера компоновки, когда доступно дополнительное пространство по горизонтали. Вы можете выбрать Center, Left, Right или Stretch.
VerticalAlignment
Определяет позиционирование дочернего элемента внутри контейнера компоновки, когда доступно дополнительное пространство по вертикали. Вы можете выбрать Center, Top, Bottom или Stretch.
Margin
Добавляет немного места вокруг элемента. Свойство Margin — это экземпляр структуры System.Windows.Thickness, с отдельными компонентами для верхней, нижней, левой и правой граней.
MinWidth и MinHeight
Устанавливает минимальные размеры элемента. Если элемент слишком велик, чтобы поместиться в его контейнер компоновки, он будет усечен.
MaxWidth и MaxHeight
Устанавливает максимальные размеры элемента. Если контейнер имеет свободное пространство, элемент не будет увеличен сверх указанных пределов, даже если свойства HorizontalAlignment и VerticalAlignment установлены в Stretch.
Width и Height
Явно устанавливают размеры элемента. Эта установка переопределяет значение Stretch для свойств HorizontalAlignment и VerticalAlignment. Однако этот размер не будет установлен, если выходит за пределы, заданные в MinWidth, MinHeight, MaxWidth и MaxHeight.
Book_Pro_WPF-2.indb 98
19.05.2008 18:09:45
Компоновка
99
Все эти свойства наследуются от базового класса FrameworkElement, и потому поддерживается всеми графическими элементами (виджетами), которые вы можете использовать в окне WPF. На заметку! Как вам известно из главы 2, различные контейнеры компоновки могут представлять прикрепленные свойства к их дочерним элементам. Например, все дочерние элементы объекта Grid получают свойства Row и Column, которые позволяют им выбирать ячейку, в которой они должны разместиться. Прикрепленные свойства позволяют устанавливать информацию, специфичную для определенного контейнера компоновки. Однако свойства компоновки из таблицы носят достаточно общий характер, чтобы применяться ко многим панелям компоновки. Таким образом, эти свойства определены как часть базового класса FrameworkElement. Этот список свойств замечателен тем, чего он не содержит. Если вы ищете знакомые свойства позиционирования, такие как Top, Right и Location, вы не найдете их там. Это потому, что большинство контейнеров компоновки (кроме Canvas) используют автоматическую компоновку и не дают вам возможности явного позиционирования элементов.
Выравнивание Чтобы понять, как работают эти свойства, еще раз взглянем на простую StackPanel, показанную на рис. 4.2. В этом примере со StackPanel с вертикальной ориентацией свойство VerticalAlignment не имеет эффекта, потому что каждый элемент получает такую высоту, которая ему нужна, и не более. Однако свойство HorizontalAlignment имеет значение. Оно определяет место, где располагается каждый элемент в строке. Обычно HorizontalAlignment по умолчанию равно Left для меток и Stretch — для кнопок. Вот почему каждая кнопка целиком занимает ширину колонки. Однако вы можете изменить эти детали: A Button Stack Button 1 Button 2 Button 3 Button 4
На рис. 4.4 показан результат. Первые две кнопки получают минимальные размеры и выравниваются соответственно, в то время как две нижние кнопки растянуты на всю StackPanel. Если вы измените размер окна, то увидите, что метка остается в середине, а первые две кнопки будут прижаты каждая к своей стороне.
Рис. 4.4. StackPanel с выровненными кнопками
Book_Pro_WPF-2.indb 99
19.05.2008 18:09:45
100
Глава 4
На заметку! StackPanel также имеет собственные свойства HorizontalAlignment и VerticalAlignment. По умолчанию оба они установлены в Stretch, и потому StackPanel заполняет свой контейнер полностью. В данном примере это значит, что StackPanel заполняет окно. Если вы используете другие установки, максимальный размер StackPanel будет определяться самым широким элементом управления, содержащимся в нем.
Поля В текущей форме примера StackPanel присутствует очевидная проблема. Хорошо спроектированное окно должно содержать не только элементы; оно также содержит немного дополнительного пространства между элементами. Чтобы представить это дополнительное пространство и сделать пример StackPanel менее “зажатым”, вы можете устанавливать поля вокруг элементов управления. При установке полей вы можете установить одинаковую ширину для всех сторон, как здесь: Button 3
Альтернативно вы можете установить разные поля для каждой стороны элемента управления в порядке левое, верхнее, правое, нижнее: Button 3
В коде поля устанавливаются в структуре Thickness: cmd.Margin = new Thickness(5);
Определение правильных полей вокруг элементов управления — отчасти искусство, потому что вы должны учитывать, каким образом установки полей соседних элементов управления влияют друг на друга. Например, если у вас есть две кнопки, сложенные одна на другую, и самая верхняя кнопка имеет нижнее поле размером 5, а самая нижняя кнопка имеет верхнее поле размером 5, то у вас получится пространство в 10 единиц между двумя кнопками. В идеале вы сможете сохранять разные установки полей насколько возможно согласованными и избегать разных значений для полей разных сторон. Например, в примере со StackPanel имеет смысл использовать одинаковые поля для кнопок и самой панели, как показано ниже: A Button Stack Button 1 Button 2 Button 3 Button 4
Таким образом, общее пространство между двумя кнопками (сумма полей двух кнопок) получается таким же, как общее пространство между кнопкой и краем окна (сумма поля кнопки и поля StackPanel). На рис. 4.5 показано наиболее приемлемое окно, а на рис. 4.6 — как его изменяют установки полей.
Минимальный, максимальный и явный размеры И, наконец, каждый элемент включает свойства Height и Width, которые позволяют вам установить явный размер. Однако предпринимать такой шаг — не слишком хорошая идея.
Book_Pro_WPF-2.indb 100
19.05.2008 18:09:45
Компоновка
101
Рис. 4.5. Добавление полей между элементами
Window
Button1.Margin.Top StackPanel.Margin.Left
Button1 Button1.Margin.Bottom Button2.Margin.Top Button2 Button2.Margin.Right
StackPanel.Margin.Right
StackPanel.Margin.Bottom
Рис. 4.6. Как комбинируются поля Вместо этого при необходимости используйте свойства минимального и максимального размеров, чтобы зафиксировать ваш элемент управления в нужных пределах размеров. Совет. Подумайте дважды, прежде чем устанавливать явные размеры в WPF. В хорошо спроектированной компоновке в этом не должно быть необходимости. Если вы добавляете информацию о размерах, то рискуете создать хрупкую компоновку, которая не может адаптироваться к изменениям (таким как разные языки и размеры окон) и усекает ваше содержимое. Например, вы можете решить, что кнопки в вашей StackPanel должны растягиваться для ее заполнения, но быть не более 200 единиц и не меньше 100 единиц в ширину. (По умолчанию, кнопки начинаются с минимальной ширины в 75 единиц.) Вот какая разметка вам для этого понадобится: A Button Stack
Book_Pro_WPF-2.indb 101
19.05.2008 18:09:45
102
Глава 4
Button
1 2 3 4
Совет. Здесь вы можете спросить: а нет ли более простого способа установки свойств, стандартизованных для нескольких элементов, таких как поля кнопок в этом примере? Ответом могут служить стили — средство, позволяющее вам повторно использовать установки свойств и даже применять их автоматически. Подробнее о стилях вы узнаете из главы 12. Когда StackPanel изменяет размеры кнопки, он учитывает несколько единиц информации.
• Минимальный размер. Каждая кнопка всегда будет не меньше минимального размера.
• Максимальный размер. Каждая кнопка всегда будет меньше максимального размера (если только вы не установите неправильно максимальный размер меньше минимального).
• Содержимое. Если содержимое внутри кнопки требует большей ширины, то StackPanel попытается увеличить кнопку. (Вы можете определить размер, который нужен кнопке, проверив свойство DesiredSize, которое вернет минимальную ширину или ширину содержимого — в зависимости от того, что больше.)
• Размер контейнера. Если минимальная ширина больше, чем ширина StackPanel, то часть кнопки будет усечена. В противном случае кнопке не позволено будет расти шире, чем позволит StackPanel, даже несмотря на то, что она не сможет вместить весь текст на своей поверхности.
• Горизонтальное выравнивание. Поскольку кнопка использует HorizontalAlignment, равный Stretch (по умолчанию), StackPanel попытается увеличить кнопку, чтобы она заполнила полную ширину StackPanel. Сложность понимания этого процесса заключается в том, что минимальный и максимальный размер устанавливает абсолютные пределы. Без этих пределов StackPanel пытается обеспечить желаемый размер кнопки (чтобы вместить ее содержимое) и установки выравнивания. Рис. 4.7 проливает некоторый свет на то, как это работает в StackPanel.
Рис. 4.7. Ограничение изменения размеров кнопок
Book_Pro_WPF-2.indb 102
19.05.2008 18:09:45
Компоновка
103
Слева представлено окно в минимальном размере. Кнопки имеют размер по 100 единиц каждая, и окно не может быть сужено, чтобы сделать их меньше. Если вы попытаетесь сжать окно от этой точки, то правая часть каждой кнопки будет усечена. (Вы можете предотвратить такую возможность применением свойства MinWidth к самому окну, так что окно нельзя будет сузить меньше минимальной ширины.) При увеличении окна кнопки также растут, пока не достигнут своего максимума в 200 единиц. Если после этого вы продолжите увеличивать окно, то с каждой стороны от кнопок будет добавлено дополнительное пространство (как показано на рисунке справа). На заметку! В некоторых ситуациях вы можете использовать код, проверяющий, насколько велик элемент в окне. Свойства Height и Width не помогают, потому что указывают желаемые установки размера, которые могут не соответствовать действительному визуализируемому размеру. В идеальном сценарии вы позволите размерам ваших элементов вмещать их содержимое, и тогда свойства Height и Width вообще устанавливать не надо. Однако вы можете узнать действительный размер, используемый для визуализации элемента, прочитав свойства ActualHeight и ActualWidth. Но помните, что эти значения могут меняться при изменении размера окна или содержимого элементов.
Окна с автоматически устанавливаемыми размерами В данном примере есть один элемент с жестко закодированным размером: окно верхнего уровня, которое содержит в себе StackPanel (и всем прочим внутри). По ряду причин жестко кодировать размеры окна имеет смысл. • Во многих случаях вы хотите сделать окно меньше, чем диктует желаемый размер его дочерних элементов. Например, если ваше окно включает контейнер с прокручиваемым текстом, вы захотите ограничить размер этого контейнера, чтобы прокрутка была возможна. Вы не захотите показывать это окно нелепо большим, так чтобы не было потребности в прокрутке, чего требует контейнер. (Подробнее о прокрутке вы узнаете из главы 5.) • Минимальный размер окна может быть удобен, но при этом не иметь наиболее привлекательных пропорций. Некоторые размеры окна просто лучше выглядят. • Автоматическое изменение размеров окна не ограничено размером дисплея вашего монитора. Так что окно с автоматически установленным размером может оказаться слишком большим для просмотра. Однако окна с автоматически устанавливаемым размером возможны, и они имеют смысл, когда вы конструируете простое окно с динамическим содержимым. Чтобы включить автоматическую установку размеров окна, удалите свойства Height и Width и установите Window. SizeToContent равным WidthAndHeight. Окно сделает себя достаточно большим, чтобы вместить его содержимое. Вы можете также позволить окну изменять свой размер только в одном измерении, используя значение SizeToContent для Width или Height.
WrapPanel и DockPanel Очевидно, что только StackPanel не может помочь вам в создании реалистичного пользовательского интерфейса. Чтобы довершить картину, StackPanel должен работать с другими более развитыми контейнерами компоновки. Только так вы сможете создать полноценное окно. Наиболее изощренный контейнер компоновки — это Grid, который мы рассмотрим далее в этой главе. Но сначала стоит взглянуть на WrapPanel и DockPanel — два самых
Book_Pro_WPF-2.indb 103
19.05.2008 18:09:45
104
Глава 4
простых контейнера компоновки, предоставленных WPF. Они дополняют StackPanel другим поведением компоновки.
WrapPanel WrapPanel располагает элементы управления в доступном пространстве — по одной строке или колонке за раз. По умолчанию WrapPanel.Orientation устанавливается в Horizontal; элементы управления располагаются слева направо, затем — в следующих строках. Однако вы можете использовать Vertical для размещения элементов в нескольких колонках. Совет. Подобно StackPanel, панель WrapPanel действительно предназначена для управления мелкими деталями пользовательского интерфейса, а не компоновкой всего окна. Например, вы можете использовать WrapPanel для удержания вместе кнопок в элементе управления типа панели инструментов. Приведем пример, определяющий серии кнопок с разными выравниваниями и помещением их в WrapPanel: Top Button Tall Button 2 Bottom Button Stretch Button Centered Button
На рис. 4.8 показано, как переносятся кнопки, чтобы заполнить текущий размер
WrapPanel (определяемый размером окна, содержащего его). Как демонстрирует этот пример, WrapPanel в горизонтальном режиме создает серии воображаемых строк, высота каждой из которых определяется высотой самого крупного содержащегося в ней элемента. Другие элементы управления могут быть растянуты для заполнения строки или выровнены в соответствии со значением свойства VerticalAlignment. В примере, представленном слева на рис. 4.8, все кнопки выстроены в одну строку, причем некоторые растянуты, а другие выровнены по этой строке. В примере справа несколько кнопок выталкиваются на вторую строку. Поскольку вторая строка не включает слишком высоких кнопок, высота строки равна минимальной высоте кнопок. В результате не имеет значения, какую установку VerticalAlignment используют кнопки в этой строке. На заметку! WrapPanel — единственная панель, которая не может дублироваться хитрым использованием Grid.
Рис. 4.8. Перенос кнопок
Book_Pro_WPF-2.indb 104
19.05.2008 18:09:46
Компоновка
105
DockPanel DockPanel обеспечивает более интересный вариант компоновки. Эта панель растягивает элементы управления вдоль одной из внешних границ. Простейший способ визуализировать это — вообразить линейку инструментов, которая присутствует в верхней части многих приложений Windows. Такие линейки инструментов прикрепляются к вершине окна. Как и в случае StackPanel, прикрепленные элементы должны выбрать один аспект компоновки. Например, если вы прикрепите кнопку к вершине DockPanel, она растянется на всю ширину DockPanel, но получит высоту, которая ей потребуется (на основе ее содержимого и свойства MinHeight). С другой стороны, если вы прикрепите кнопку к левой стороне контейнера, ее высота будет растянута для заполнения контейнера, но ширина будет установлена по необходимости. Возникает очевидный вопрос: как дочерние элементы выбирают сторону, к которой они хотят стыковаться? Ответ — через прикрепленное свойство по имени Dock, которое может быть установлено в Left, Right, Top или Bottom. Каждый элемент, помещаемый внутри DockPanel, автоматически получает это свойство. Ниже приведен пример, который помещает одну кнопку на каждую сторону DockPanel: Top Button Bottom Button Left Button Right Button Remaining Space
В этом примере также устанавливается LastChildFill в true , что указывает DockPanel о необходимости отдать оставшееся пространство последнему элементу. Результат показан на рис. 4.9.
Рис. 4.9. Стыковка к каждой стороне Ясно, что при такой стыковке элементов управления важен порядок. В данном примере верхняя и нижняя кнопки получают всю ширину DockPanel, поскольку они стыкованы первыми. Когда затем стыкуются левая и правая кнопки, они помещаются между первыми двумя. Если поступить наоборот, то левая и правая кнопки получат полную
Book_Pro_WPF-2.indb 105
19.05.2008 18:09:46
106
Глава 4
высоту сторон панели, а верхняя и нижняя станут уже, потому что им придется размещаться уже между боковыми кнопками. Вы можете стыковать несколько элементов к одной стороне. В этом случае элементы просто выстраиваются вдоль этой стороны в том порядке, в котором они объявлены в вашей разметке. И, если вам не нравится поведение в отношении растяжения и промежуточных пробелов, вы можете подкорректировать свойства Margin, HorizontalAlignment и VerticalAlignment, как делали это со StackPanel. Ниже для целей демонстрации приведена модифицированная версия предыдущего примера. A Stretched Top Button A Centered Top Button A Left-Aligned Top Button Bottom Button Left Button Right Button Remaining Space
Поведение в отношении стыковки остается прежним. Сначала стыкуются верхние кнопки, затем — нижняя кнопка и, наконец, оставшееся пространство делится между боковыми кнопками, а последняя кнопка помещается в середине. На рис. 4.10 можно видеть полученное в результате окно.
Рис. 4.10. Стыковка нескольких элементов к вершине
Вложение контейнеров компоновки StackPanel, WrapPanel и DockPanel редко используются сами по себе. Вместо этого они применяются для формирования частей вашего интерфейса. Например, вы можете использовать DockPanel для размещения разных контейнеров StackPanel и WrapPanel в соответствующих областях окна. Например, предположим, что вы хотите создать стандартное диалоговое окно с кнопками OK и Cancel (Отмена) в нижнем правом углу, расположив большую область содержимого в остальной части окна. Есть несколько способов смоделировать этот ин-
Book_Pro_WPF-2.indb 106
19.05.2008 18:09:46
Компоновка
107
терфейс в WPF, но простейший вариант, использующий панели, которые вы видели до сих пор, выглядит следующим образом. 1. Создать горизонтальную StackPanel для размещения рядом кнопок OK и Cancel. 2. Поместить StackPanel в DockPanel и использовать ее для стыковки к нижней части окна. 3. Установить DockPanel.LastChildFill в true, чтобы вы могли использовать остаток окна для заполнения прочим содержимым. Вы можете добавить сюда другой элемент управления компоновкой либо просто обычный элемент управления TextBox (как в примере). 4. Установить значения полей, чтобы распределить пустое пространство. Вот как выглядит итоговая разметка: OK Cancel This is a test.
На рис. 4.11 показано довольно скучное диалоговое окно, полученное в результате.
Рис. 4.11. Базовое диалоговое окно На заметку! В этом примере Padding добавляет некоторый минимальный зазор между рамкой кнопки и ее внутренним содержимым (словом “OK” или “Cancel”). Вы узнаете больше о Padding, когда речь пойдет об элементах управления содержимым в главе 5. На первый взгляд может показаться, что все это требует больше работы, чем точное размещение с использованием координат в традиционном приложении Windows Forms. И во многих случаях так оно и есть. Однако большие затраты времени на установку компенсируются легкостью, с которой вы можете в будущем изменять пользовательский интерфейс. Например, если вы решите, что вам нужно поместить кнопки OK и Cancel в центре нижней части окна, вам достаточно просто изменить выравнивание содержащей их StackPanel:
Book_Pro_WPF-2.indb 107
19.05.2008 18:09:46
108
Глава 4
Этот дизайн — простое окно с отцентрированными кнопками — уже демонстрирует результат, который был невозможен в Windows Forms из .NET 1.x (по крайней мере, невозможен без написания кода), и который требовал специализированных контейнеров компоновки в Windows Forms из .NET 2.0. И если вы когда-либо видели код, сгенерированный дизайнером процессом сериализации Windows Forms, то согласились бы, что разметка, используемая здесь, яснее, проще и компактнее. Если вы добавите стиль к этому окну (глава 12), то сможете еще более усовершенствовать его и удалить другие излишние детали (вроде установки полей), чтобы создать действительно адаптируемый пользовательский интерфейс. Совет. Если вы имеете плотное дерево элементов, легко потерять представление об общей структуре. Visual Studio предлагает удобное средство, показывающее древовидное представление ваших элементов и позволяющее выбирать в нем нужный элемент, который вы хотите увидеть (или модифицировать). Это средство — окно Document Outline (Эскиз документа), которое вы можете отобразить, выбрав ViewOther WindowsDocument Outline (ВидДругие окна Эскиз документа) из меню.
Grid Grid — наиболее мощный контейнер компоновки в WPF. Большая часть того, что вы можете достичь с другими элементами управления компоновкой, также возможно и в Grid. Контейнер Grid является идеальным инструментом для разбиения вашего окна на меньшие области, которыми вы можете управлять с помощью других панелей. Фактически Grid настолько удобен, что когда вы добавляете новый документ XAML для окна в Visual Studio, он автоматически добавляет дескрипторы Grid в качестве контейнера первого уровня, вложенного внутрь корневого элемента Window. Grid располагает элементы в невидимой сетке строк и колонок. Хотя в отдельную ячейку этой сетки можно поместить более одного элемента (и тогда они перекрываются), обычно имеет смысл помещать в каждую ячейку по одному элементу. Конечно, этот элемент сам может быть другим контейнером компоновки, который организует свою собственную группу содержащихся в нем элементов управления. Совет. Хотя Grid задуман как невидимый, вы можете установить свойство Grid.ShowGridLines в true, чтобы получить наглядное представление о нем. Это средство на самом деле не предназначено для того, чтобы украсить окно. В действительности это средство, облегчающее отладку, предназначенное для того, чтобы наглядно показать, как Grid разделяет пространство на отдельные области. Это средство важно, потому что благодаря ему, вы имеете возможность точно контролировать то, как Grid выбирает ширину колонок и высоту строк. Создание компоновки на основе Grid — двухшаговый процесс. Сначала вы выбираете количество колонок и строк, которые вам нужны. Затем назначаете соответствующую строку и колонку каждому содержащемуся элементу, тем самым помещая его в правильное место. Вы создаете колонки и строки, заполняя объектами коллекции Grid.ColumnDefinitions и Grid.RowDefinitions. Например, если вы решите, что вам нужно две строки и три колонки, вы должны добавить следующие дескрипторы:
04_Pro-WPF2.indd 108
20.05.2008 16:13:02
Компоновка
109
...
Как демонстрирует этот пример, не обязательно указывать какую-либо информацию в элементах RowDefinition или ColumnDefinition. Если вы оставите их пустыми (как показано здесь), то Grid поровну разделит пространство между всеми строками и колонками. В данном примере каждая ячейка будет одного и того же размера, который зависит от размера включающего окна. Чтобы поместить индивидуальные элементы в ячейку, вы используете прикрепленные свойства Row и Column. Оба эти свойства принимают числовое значение индекса, начиная с 0. Например, вот как вы можете создать частично заполненную кнопками сетку: ... Top Left Middle Left Bottom Right Bottom Middle
Каждый элемент должен быть помещен в свою ячейку явно. Это позволяет вам помещать в одну ячейку более одного элемента (что редко имеет смысл), или же оставлять определенные ячейки пустыми (что часто бывает полезным). Это также означает, что вы можете объявлять ваши элементы в любом порядке, как это сделано с последними двумя кнопками в этом примере. Однако у вас получится более ясная разметка, если вы определите элементы управления строку за строкой, а в каждой строке — слева направо. Существует одно исключение. Если вы не специфицируете свойство Grid.Row, то Grid предполагает его равным 0. То же самое касается и свойства Grid.Column. Таким образом, если вы пропускаете оба атрибута элемента, он помещается в первую ячейку Grid. На заметку! Grid помещает элементы в предопределенные строки и колонки. Это отличает его от таких контейнеров компоновки, как WrapPanel и StackPanel, создающих неявные строки и колонки в процессе размещения дочерних элементов. Если вы хотите создать сетку, состоящую из более чем одной строки и одной колонки, вы должны определить ваши строки и колонки явно, используя объекты RowDefinition и ColumnDefinition. На рис. 4.12 показано, как эта простая сетка выглядит в разных размерах. Обратите внимание, что свойство ShowGridLines установлено в true, так что вы можете видеть границы между колонками и строками. Как можно было бы ожидать, Grid предоставляет базовый набор свойств компоновки, перечисленных в табл. 4.3. Это значит, что вы можете добавлять поля вокруг содержимого ячейки, можете менять режим изменения размера, чтобы элемент не рос, заполняя ячейку целиком, а также вы можете выравнивать элемент по одному из граней ячейки. Если вы заставите элемент иметь размер, превышающий тот, что может вместить ячейка, часть содержимого будет отсечена.
Book_Pro_WPF-2.indb 109
19.05.2008 18:09:46
110
Глава 4
Рис. 4.12. Простая сетка
Использование 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 выглядит достаточно прямолинейным, но в большинстве случаев он приводит не к тому результату, который вам нужен. Обычно вам необходима более гибкая потоковая (flow) компоновка, которая позволяет некоторым элементам расширяться динамически, “расталкивая” соседей. В этом сценарии вы сочтете жесткое кодирование позиции свойством Margin совершенно негибким. Проблема усугубляется, когда вы добавляет множественные элементы, потому что Visual Studio не добавляет автоматически новых ячеек. В результате все такие элементы помещаются в одну и ту же ячейку. Разные элементы могут выравниваться по разным углам Grid, что заставит их перемещаться относительно друг друга (и даже перекрывать друг друга) при изменении размера окна. Однажды поняв, как работает Grid, вы сможете исправлять эти проблемы. Первый трюк заключается в конфигурировании вашего Grid перед тем, как вы начнете добавление элементов, посредством определения новых строк и колонок. (Вы можете редактировать коллекции RowDefinitions и ColumnDefinitions, используя окно Properties (Свойства).) Однажды настроив Grid, вы можете перетаскивать в него нужные вам элементы и конфигурировать их настройки полей и выравнивание в окне Properties, редактируя XAML вручную.
Book_Pro_WPF-2.indb 110
19.05.2008 18:09:47
Компоновка
111
Тонкая настройка строк и колонок Если бы Grid был просто коллекцией строк и колонок пропорциональных размеров, от него было бы мало толку. К счастью, он не таков. Чтобы открыть полный потенциал Grid, вы можете изменять способы изменения размеров каждой строки и колонки. Grid поддерживает следующие стратегии изменения размеров.
• Абсолютные размеры. Вы выбираете точный размер, используя независимые от устройства единицы измерения. Это наименее удобная стратегия, поскольку она недостаточно гибка, чтобы справиться с изменением размеров содержимого, изменением размеров контейнера или локализацией.
• Автоматические размеры. Каждая строка и колонка получает в точности то пространство, которое нужно, и не более. Это один из наиболее удобных режимов изменения размеров.
• Пропорциональные размеры. Пространство разделяется между группой строк и колонок. Это стандартная установка для всех строк и колонок. Например, на рис. 4.12 вы увидите, что все ячейки увеличиваются пропорционально при расширении Grid. Для максимальной гибкости вы можете смешивать и сочетать эти разные режимы изменения размеров. Например, часто удобно создать несколько автоматически изменяющих размер строк и затем позволить одной или двум остальным строкам поделить между собой оставшееся пространство через пропорциональную установку размеров. Вы устанавливаете режим изменения размеров, используя свойство Width объекта ColumnDefinition или свойство Height объекта RowDefinition, присваивая им некоторое число или строку. Например, вот как вы можете установить абсолютную ширину в 100 независимых от устройства единиц:
Чтобы использовать пропорциональное изменение размеров, указывается значение
Auto:
И, наконец, чтобы использовать пропорциональное изменение размеров, вы задаете звездочку (*):
Этот синтаксис пришел из мира Web, где он применяется на страницах HTML с фреймами. Если вы используете смесь пропорциональной установки размеров с другими режимами, то пропорционально изменяемая строка или колонка получит все оставшееся пространство. Если вы хотите разделить оставшееся пространство неравными частями, вы можете присвоить вес (weight), который следует поместить перед звездочкой. Например, если у вас есть две строки пропорционального размера, и вы хотите, чтобы высота первой была равна половине высоты второй, вы можете разделить оставшееся пространство следующим образом:
Это сообщит Grid о том, что высота второй строки должна быть вдвое больше высоты первой строки. Вы можете указывать любые числа, чтобы делить дополнительное пространство.
Book_Pro_WPF-2.indb 111
19.05.2008 18:09:47
112
Глава 4
На заметку! Легко организовать программно взаимодействие между объектами ColumnDefinition и RowDefinition. Вам просто нужно знать, что свойства Width и Height — это объекты типа GetLength. Чтобы создать GetLength, представляющий определенный размер, просто передайте соответствующее значение конструктору GridLength. Чтобы создать GridLength, представляющий пропорциональный размер (*), передайте число конструктору GridLength и передайте GridUnitType.Start в качестве второго аргумента конструктора. Чтобы обозначить автоматическое изменение размера, используйте статическое свойство GridLength.Auto. Используя эти режимы установки размеров, вы можете продублировать тот же пример диалогового окна, показанного на рис. 4.11, используя контейнер Grid верхнего уровня для разделения окна на две строки вместо использования DockPanel. Вот какая разметка вам для этого понадобится: This is a test. OK Cancel
Совет. Этот Grid не объявляет каких-либо колонок. Такое сокращение вы можете применять, если ваш Grid использует только одну колонку и размер этой колонки устанавливается пропорционально (так что заполняет всю ширину Grid). Этот код разметки немного длиннее, но он имеет то преимущество, что объявляет элементы управления в порядке их появления, что облегчает его понимание. В этом случае выбор такого подхода — просто вопрос предпочтений. И если вы хотите, то можете заменить его вложенным StackPanel с однострочным, одноколоночным Grid. На заметку! Вы можете создать почти любой интерфейс, используя вложенные контейнеры Grid. (Единственное исключение — строки с переносом колонок, использующие WrapPanel.) Однако когда вы имеете дело c небольшими разделами пользовательского интерфейса или расположением небольшого количества элементов, то часто проще применить более специализированные контейнеры StackPanel и DockPanel.
Объединение строк и колонок Вы уже видели, как помещаются элементы в ячейки с использованием прикрепленных свойств Row и Column. Вы можете также использовать еще два прикрепленных свойства, чтобы растянуть элемент на несколько ячеек: RowSpan и ColumnSpan. Эти свойства принимают количество строк или колонок, которые должен занять элемент. Например, следующая кнопка займет все место, доступное в первой и второй ячейках первой строки: Span Button
А эта кнопка растянется всего на четыре ячейки, охватив две колонки и две строки: Span Button
Book_Pro_WPF-2.indb 112
19.05.2008 18:09:47
Компоновка
113
Объединение нескольких строк и колонок позволяет достичь некоторых интересных эффектов, и особенно удобно, когда вы хотите вместить в табличную структуру элементы, которые меньше или больше имеющихся ячеек. Используя объединение колонок, вы можете переписать пример простого диалогового окна на рис. 4.11, используя при этом единственный Grid. Этот Grid делит окно на три колонки, растягивая текстовое поле на все три, и использует последние две колонки для выравнивания кнопок OK и Cancel (Отмена). This is a test. OK Cancel
Большинство разработчиков согласятся с утверждением, что такая компоновка неясна и непонятна. Ширины колонок определяются размером двух кнопок окна, что затрудняет добавление нового содержимого к существующей структуре Grid. Если вы захотите сделать даже минимальное дополнение к этому окну, вы, вероятно, будете вынуждены создать для этого новый набор колонок. Как видим, когда вы выбираете для своего окна контейнер компоновки, то вам не просто нужно добиться корректного поведения компоновки — вам также нужно построить структуру компоновки, которую легко сопровождать и расширять в будущем. Хорошее эмпирическое правило заключается в использовании меньших контейнеров компоновки, подобных StackPanel для одноразовых задач компоновки, таких как организация группы кнопок. С другой стороны, если вам нужно применить согласованную структуру к более чем одной области вашего окна (как с колонкой текстового поля, показанной ниже, на рис. 4.20), то в этом случае Grid — незаменимый инструмент для стандартизации вашей компоновки.
Разделенные окна Каждый пользователь Windows видел разделительные полосы — перемещаемые разделители, отделяющие одну часть окна от другой. Например, когда вы используете проводник Windows, то видите слева список папок, а справа — список файлов. Вы можете перетаскивать разделительную полосу, устанавливая пропорции между этими двумя панелями в окне. В WPF полосы разделителей представлены классом GridSplitter и являются средствами Grid. Добавляя GridSplitter к Grid, вы предоставляете пользователю возможность изменения размеров строк и колонок. На рис. 4.13 показано окно, в котором GridSplitter находится между двумя колонками. Перетаскивая полосу разделителя, пользователь может менять относительные ширины обеих колонок.
Book_Pro_WPF-2.indb 113
19.05.2008 18:09:47
114
Глава 4
Рис. 4.13. Перемещение полосы разделителя Большинство программистов считают GridSplitter наиболее интуитивно понятной частью WPF. Чтобы разобраться, как использовать его для получения требуемого эффекта, нужно лишь немного поэкспериментировать. Вот несколько подсказок.
• GridSplitter должен быть помещен в ячейку Grid . Вы можете поместить GridSplitter в ячейку с существующим содержимым — тогда вам следует настроить установки полей, чтобы они не перекрывались. Лучший подход заключается в резервировании специальной колонки или строки для GridSplitter, со значениями Height или Width, равными Auto.
• GridSplitter всегда изменяет размер всей строки или колонки (в не отдельной ячейки). Чтобы сделать внешний вид GridSplitter соответствующим такому поведению, вы должны растянуть GridSplitter по всей строке или колонке, а не ограничиваться единственной ячейкой. Чтобы достичь этого, вы используете свойства RowSpan или ColumnSpan, которые мы рассмотрели ранее. Например, GridSplitter на рис. 4.13 имеет значение RowSpan, равное 2. В результате он растягивается на всю колонку. Если вы не добавите эту установку, он появится только в верхней строке (где помещен), даже несмотря на то, что перемещение разделительной полосы изменило бы размер всей колонки.
• Изначально GridSplitter настолько мал, что его не видно. Чтобы сделать его удобным, вам нужно задать его минимальный размер. В случае вертикальной разделяющей полосы (вроде той, что представлена на рис. 4.13), вам нужно установить VerticalAlignment в Stretch (чтобы он заполнил всю высоту доступной области), а Width — в фиксированный размер (например, в 10 независимых от устройства единиц). В случае горизонтальной разделительной полосы вам нужно установить HorizontalAlignment в Stretch, а Height — в фиксированный размер.
• Выравнивание GridSplitter также определяет, будет ли разделительная полоса горизонтальной (используемой для изменения размеров строк) или вертикальной (для изменения размеров колонок). В случае горизонтальной разделительной полосы вы должны установить VerticalAlignment в Center (что принято по умолчанию), указав тем самым, что перетаскивание разделителя изменит размеры строк, находящихся выше и ниже. В случае вертикальной разделительной полосы (как на рис. 4.13) вы должны установить HorizontalAlignment в Center, чтобы изменять размеры соседних колонок.
Book_Pro_WPF-2.indb 114
19.05.2008 18:09:47
Компоновка
115
На заметку! Вы можете изменить поведение изменения размеров, используя свойства ResizeDirection и ResizeBehavior объекта GridSplitter. Однако проще поставить это поведение в зависимость от установок выравнивания, что и принято по умолчанию. Еще не запутались? Чтобы закрепить эти правила, стоит взглянуть на реальный код разметки примера, показанного на рис. 4.13. В следующем листинге детали GridSplitter выделены полужирным. Left Right Left Right
Совет. Чтобы создать правильный GridSplitter, не забудьте присвоить значения свойствам VerticalAlignment, HorizontalAlignment и Width (или Height). Эта разметка включает одну дополнительную деталь. Когда объявляется GridPlitter, свойство ShowPreview устанавливается в false. В результате при перетаскивании полосы разделителя от одной стороны к другой колонки изменяют свой размер немедленно. Но если вы установите ShowPreview в true, то при перетаскивании вы увидите лишь серую тень, следующую за вашим курсором мыши, которая покажет, где разделитель окажется после того, как кнопка мыши будет отпущена. Колонки не изменят своего размера вплоть до этого момента. Можно также использовать клавиши со стрелками для изменения размера GridSplitter после того, как он получит фокус. ShowPreview — не единственное свойство GridSplitter, которое вы можете устанавливать. Вы можете также изменить свойство DragIncrement, если хотите заставить полосу разделителя перемещаться “шагами” (например, по 10 единиц за раз). Если вы хотите контролировать минимальный и максимальный допустимые размеры колонок, вы просто устанавливаете соответствующие свойства в разделе ColumnDefinitions, как показано в предыдущем примере. Совет. Вы можете изменить заливку GridSplitter, чтобы она не выглядела просто серым прямоугольником. Трюк заключается в использовании свойства Background, которое принимает значения простых цветов и более сложных кистей. Подробнее об этом будет сказано в главе 7. Обычно Grid содержит не более одного GridSplitter. Однако вы можете вкладывать один Grid в другой, и при этом каждый из них будет иметь собственный GridSplitter. Это позволит создавать окна, которые разделены на две области (например, на левую и правую панель), одна из которых (например, правая), в свою очередь, также будет раз-
Book_Pro_WPF-2.indb 115
19.05.2008 18:09:47
116
Глава 4
делена еще на два раздела (на верхний и нижний с изменяемыми размерами). Пример показан на рис. 4.14.
Рис. 4.14. Разбиение окна с помощью двух разделителей Создать такое окно довольно просто, хотя управление тремя контейнерами Grid, которые здесь присутствуют, требует некоторых усилий: общий Grid, вложенный Grid слева и вложенный Grid справа. Единственный трюк состоит в том, чтобы установить GridSplitter в правильную ячейку и задать ему правильное выравнивание. Ниже показана полная разметка. Top Left Bottom Left Top Right Bottom Right
Book_Pro_WPF-2.indb 116
19.05.2008 18:09:48
Компоновка
117
Совет. Помните, что если Grid имеет всего одну строку или колонку, вы можете опустить раздел RowDefinition. Также элементы, которые не имеют явно установленной позиции строки, предполагают значение Grid.Row, равное 0, и помещаются в первой строке. То же самое справедливо в отношении элементов, для которых не указано Grid.Column.
Группы с общими размерами Как вы уже видели, Grid содержит коллекцию строк и колонок, размер которых устанавливается явно, пропорционально или на основе размеров их дочерних элементов. Есть только один способ изменить размер строки или колонки — приравнять его размеру другой строки или колонки. Это выполняется при помощи средства, называемого группами с общими размерами (shared size groups). Цель таких групп — поддержание согласованности между различными частями вашего пользовательского интерфейса. Например, вы можете установить размер одной колонки в соответствии с ее содержимым, а размер другой колонки — в точности равным размеру первой. Однако реальное преимущество групп с общими размерами заключается в обеспечении одинаковых пропорций различным элементам управления Grid. Чтобы понять, как это работает, рассмотрим пример, показанный на рис. 4.15. Это окно оснащено двумя объектами Grid — один в верхней части окна (с тремя колонками) и один в его нижней части (с двумя колонками). Размер левой крайней колонки первого Grid устанавливается пропорционально ее содержимому (длинной текстовой строке). Левая крайняя колонка второго Grid имеет в точности ту же ширину, хотя имеет меньшее содержимое. Дело в том, что они входят в одну размерную группу. Независимо от того, какое содержимое вы поместите в первую колонку первого Grid, первая колонка второго Grid останется синхронизированной.
Рис. 4.15. Два элемента Grid, разделяющие одно определение колонки
Book_Pro_WPF-2.indb 117
19.05.2008 18:09:48
118
Глава 4
Как демонстрирует этот пример, колонки с общими размерами могут принадлежать к разным Grid. В этом примере верхний Grid имеет на одну колонку больше и потому оставшееся пространство в нем распределяется иначе. Аналогично колонки с общими размерами могут занимать разные позиции, так что вы можете создать отношение между первой колонкой одного Grid и второй колонкой другого. И очевидно, что колонки при этом могут иметь совершенно разное содержимое. Когда вы используете группу с общими размерами, это все равно, как если бы вы создали одно определение колонки (или строки), используемое в более чем одном месте. Это не просто однонаправленная копия одной колонки в другую. Вы можете убедиться в этом в предыдущем примере, изменив содержимое разделенной колонки второго Grid. Теперь колонка в первом Grid будет удлинена для сохранения соответствия (рис. 4.16).
Рис. 4.16. Колонки, разделяющие общий размер, остаются синхронизированными Вы можете даже добавить GridSplitter к одному из объектов Grid. Когда пользователь будет изменять размер колонки в одном Grid, то соответствующая разделенная колонка из второго Grid также будет синхронно менять свой размер. Создать группы с общими размерами просто. Вам нужно лишь установить свойство SharedSizeGroup в обеих колонках, используя строку соответствия. В текущем примере обе колонки используют группу по имени TextLabel. A very long bit of text More text A text box ... Short A text box
Book_Pro_WPF-2.indb 118
19.05.2008 18:09:48
Компоновка
119
Остается упомянуть еще одну деталь. Группы с общими размерами не являются глобальными для всего вашего приложения, потому что более одного окна могут нечаянно использовать одно и то же имя. Вы можете предположить, что группы с общими размерами ограничены текущим окном, но на самом деле WPF еще более строг в этом отношении. Чтобы разделить группу, вы должны явно установить прикрепленное свойство Grid.IsSharedSizeScope в true в контейнере высшего уровня, содержащем объекты Grid с колонками с общими размерами. В текущем примере верхний и нижний Grid входят в другой Grid, предназначенный для этой цели, хотя вы столь же просто можете использовать другой контейнер, такой как DockPanel или StackPanel. Ниже показана разметка Grid верхнего уровня. ... Some text in between the two grids... ...
Совет. Вы могли бы использовать группу с общими размерами для синхронизации отдельных Grid с заголовками колонок. Ширина каждой колонки может быть затем определена ее содержимым, которое разделит заголовок. Вы можете даже поместить GridSplitter в заголовок, и тогда пользователь сможет перетаскивать его для изменения размера заголовка и всей лежащей ниже колонки.
UniformGrid Существует элемент типа сетки, который нарушает все правила, изученные вами до сих пор — это UniformGrid. В отличие от Grid, элемент UniformGrid не требует (и даже не поддерживает) предопределенных колонок и строк. Вместо этого вы просто устанавливаете свойства Rows и Columns для установки его размеров. Каждая ячейка всегда имеет одинаковый размер, потому что доступное пространство делится поровну. И, наконец, элементы помещаются в соответствующую ячейку на основе порядка их определения. Нет никаких прикрепленных свойств Row и Column, и никаких пустых ячеек. Приведем пример, наполняющий UniformGrid четырьмя кнопками: Top Left Top Right Bottom Left Bottom Right
UniformGrid используется намного реже, чем Grid. Элемент Grid — это инструмент общего назначения для создания компоновки окон, от самых простых до самых сложных. UniformGrid намного более специализированный контейнер компоновки, который в первую очередь предназначен для размещения элементов в жесткой сетке (например, для построения игрового поля для некоторых игр). Многие программисты WPF никогда не пользуются UniformGrid.
Book_Pro_WPF-2.indb 119
19.05.2008 18:09:48
120
Глава 4
Координатная компоновка с помощью Canvas Единственный контейнер компоновки, который мы еще не рассмотрели — это
Canvas. Он позволяет размещать элементы, используя точные координаты, что вообще-то является плохим выбором при проектировании богатых управляемых данными форм и стандартных диалоговых окон, но ценным инструментом, если вам нужно построить нечто другое (вроде поверхности рисования для инструмента построения диаграмм). Canvas также является наиболее легковесным из контейнеров компоновки. Это объясняется тем, что он не включает в себя никакой сложной логики компоновки, согласовывающей размерные предпочтения своих дочерних элементов. Вместо этого он просто располагает их в указанных вами позициях с точными размерами, которые вам нужны. Чтобы позиционировать элемент на Canvas, вы устанавливаете прикрепленные свойства Canvas.Left и Canvas.Top. Свойство Canvas.Left задает количество единиц измерения между левой гранью вашего элемента и левой границей Canvas. Свойство Canvas.Top устанавливает количество единиц измерения между вершиной вашего элемента и левой границей Canvas. Как всегда, эти значения задаются в независимых от устройства единицах измерения, которые соответствуют обычным пикселям, когда системная установка DPI составляет 96 dpi. На заметку! Альтернативно вы можете использовать Canvas.Right вместо Canvas.Left, чтобы расположить элемент относительно правого края Canvas, и Canvas.Bottom вместо Canvas.Top — чтобы расположить его относительно низа. Вы не можете одновременно использовать Canvas.Right и Canvas.Left или Canvas.Top и Canvas.Bottom. Дополнительно вы можете устанавливать размер вашего элемента явно, используя его свойства Width и Height. Это чаще применяется при использовании Canvas, чем с другими панелями, потому что Canvas не имеет собственной логики компоновки. (К тому же вы часто будете применять Canvas, когда вам понадобится точный контроль расположения комбинации элементов.) Если вы не устанавливаете свойства Width и Height, ваш элемент получит желательный для него размер; другими словами, он станет достаточно большим, чтобы вместить свое содержимое. Ниже приведен пример простого Canvas, включающего четыре кнопки. (10,10) (120,30) (60,80) (70,120)
На рис. 4.17 показан результат. Если вы измените размеры окна, то Canvas растянется для заполнения всего доступного пространства, но ни один из элементов управления на его поверхности не изменит своего положения и размера. Canvas не включает никаких средств привязки или стыковки, которые имеются в координатных компоновках Windows Forms. Отчасти это объясняется легковесностью Canvas.
Book_Pro_WPF-2.indb 120
Рис. 4.17. Явно позиционированные кнопки в Canvas
19.05.2008 18:09:48
Компоновка
121
Другая причина в том, чтобы предотвратить использование Canvas для тех целей, для которых он не предназначен (например, для компоновки стандартного пользовательского интерфейса). Подобно любому другому контейнеру компоновки, Canvas может вкладываться внутрь пользовательского интерфейса. Это значит, что вы можете применять Canvas для рисования более детализированного содержимого в части вашего окна, используя более стандартные панели WPF для остальной части ваших элементов. Совет. Если вы используете Canvas в стороне от ваших элементов, вы можете установить его свойство ClipToBounds в true. Таким образом, элементы внутри Canvas, которые выходят за его пределы, будут усечены на грани Canvas. (Это предотвратит перекрытие ими других элементов в других местах вашего окна.) Все прочие контейнеры компоновки всегда усекают свои дочерние элементы, выходящие за их границы, независимо от установки ClipToBounds.
Z-порядок Если у вас более одного перекрывающегося элемента, вы можете установить прикрепленное свойство Canvas.ZIndex для управления их расположением. Обычно все элементы, которые вы добавляете, имеют одинаковый ZIndex — 0. Когда элементы имеют одинаковый ZIndex, они отображаются в том порядке, в каком они представлены в коллекции Canvas.Children, который основан на порядке их определения в разметке XAML. Элементы, объявленные позже в разметке — такие как кнопка (70,120) — отображаются поверх элементов, объявленный ранее — таких как кнопка (120,30). Однако вы можете передвинуть любой элемент на более высокий уровень, увеличив его ZIndex. Это объясняется тем, что элементы с большими ZIndex всегда появляются поверх элементов c меньшими ZIndex. Используя эту технику, вы можете обратить компоновку из предыдущего примера: (60,80) (70,120)
На заметку! Действительные значения, которые вы используете для свойства Canvas.ZIndex, не важны. Важно отношение значений ZIndex разных элементов между собой. Вы можете установить ZIndex в любое положительное или отрицательное целое число. Свойство ZIndex в частности удобно, если вам нужно изменить позицию элемента программно. Просто вызовите Canvas.SetZIndex() и передайте ему элемент, который хотите модифицировать, и новое значение ZIndex. К сожалению, не предусмотрено метода BringToFront() или SendToBack(), так что на вас возлагается задача отслеживать максимальное и минимальное значения ZIndex, если вы захотите реализовать это поведение.
InkCanvas WPF также включает элемент InkCanvas, который подобен Canvas в одних отношениях и совершенно отличается в других. Подобно Canvas, элемент InkCanvas определяет четыре прикрепленных свойства, которые вы можете применить к дочерним элементам для координатного позиционирования (Top, Left, Bottom и Right). Однако лежащий в его основе механизм существенно отличается. Фактически InkCanvas не на-
Book_Pro_WPF-2.indb 121
19.05.2008 18:09:48
122
Глава 4
следуется от Canvas, и даже не наследуется от базового класса Panel. Вместо этого он наследуется непосредственно от FrameworkElement. Главное предназначение InkCanvas заключается в обеспечении перьевого ввода. Перо (stylus) — это подобное карандашу устройство ввода, используемое в наладонных ПК. Однако InkCanvas работает с мышью точно так же, как и с пером. Поэтому пользователь может рисовать линии или выбирать и манипулировать элементами в InkCanvas с применением мыши. InkCanvas в действительности содержит две коллекции дочернего содержимого. Знакомая вам коллекция Children содержит произвольные элементы — как и Canvas. Каждый элемент может быть позиционирован на основе свойств Top, Left, Bottom и Right. Коллекция Strokes содержит объекты System.Windows.Ink.Stroke, представляющие графический ввод, который рисует пользователь в InkCanvas. Каждая линия или кривая, которую рисует пользователь, становится отдельным объектом Stroke. Благодаря этим двум коллекциям, вы можете использовать InkCanvas для того, чтобы позволить пользователю аннотировать содержимое (хранящееся в коллекции Children) пометками (хранящимися в коллекции Strokes). Например, на рис. 4.18 показан элемент InkCanvas, содержащий картинку, аннотированную дополнительными пометками. Вот разметка InkCanvas из этого примера, которая определяет изображение:
Пометки нарисованы пользователем во время выполнения.
Рис. 4.18. Добавление пометок в InkCanvas
Book_Pro_WPF-2.indb 122
19.05.2008 18:09:49
123
Компоновка
InkCanvas может применяться несколькими существенно отличающимися способами, в зависимости от значения, которое вы установите для свойства InkCanvas. EditingMode. Возможные варианты этого значения перечислены в табл. 4.4. Таблица 4.4. Значения перечисления InkCanvasEditingMode Имя
Описание
Ink
InkCanvas позволяет пользователю рисовать аннотации. Это режим по умолчанию. Когда пользователь рисует мышью или пером, появляются штрихи.
GestureOnly
InkCanvas не позволяет пользователю рисовать аннотации, но привлекает внимание к некоторым предопределенным жестам (gestures), таким как перемещение пера в одном направлении или подчеркивание содержимого. Полный список жестов определен в перечислении System.Windows. Ink.ApplicationGesture.
InkAndGesture
InkCanvas позволяет пользователю рисовать штриховые аннотации и также распознает предопределенные жесты.
EraseByStroke
InkCanvas удаляет штрих при щелчке. Если у пользователя есть перо, он может переключиться в этот режим, используя его обратный конец. (Вы можете определить текущий режим, проверив значение доступного только для чтения свойства ActiveEditingMode, и вы можете изменить режим, используемый обратным концом пера, изменив свойство EditingModeInverted.)
EraseByPoint
InkCanvas удаляет часть штриха (точку штриха) при щелчке по соответствующей его части.
Select
InkCanvas позволяет пользователю выбирать элементы, хранящиеся в коллекции Children. Чтобы выбрать элемент, пользователь должен щелкнуть на нем или обвести “лассо” выбора вокруг него. Как только элемент выбран, его можно перемещать, изменять размер или удалять.
None
InkCanvas игнорирует ввод с помощью мыши или пера.
InkCanvas инициирует события при изменении режима редактирования (Active EditingModeChanged), обнаружении жеста в режимах GestureOnly или InkAndGesture (Gesture), рисовании штриха (StrokeCollected), стирании штриха (StrokeErasing и StrokeErased), а также при выборе элемента или изменении его в режиме Select (SelectionChanging , SelectionChanged , SelectionMoving , SelectionMoved , SelectionResizing и SelectionResized). События, оканчивающиеся на ing, представляют действие, которое начинается, но может быть отменено установкой свойства Cancel объекта EventArgs. В режиме Select элемент InkCanvas предоставляет довольно удобную поверхность проектирования для перетаскивания содержимого и различных манипуляций им. На рис. 4.19 показан элемент управления Button в InkCanvas, когда он был выбран (слева) и затем перемещен и увеличен (справа). Как бы ни был интересен режим Select, он не совсем подходит для построения рисунков или диаграмм. Вы увидите лучший пример того, как создается поверхность рисования, в главе 14.
Примеры компоновки Мы с вами уже потратили достаточно времени на исследование интерфейсов контейнеров компоновки WPF. Но, учитывая относительно небольшой уровень знаний, сто-
Book_Pro_WPF-2.indb 123
19.05.2008 18:09:49
124
Глава 4
ит взглянуть на несколько завершенных примеров компоновки. Это даст вам лучшее представление о том, как работают различные концепции компоновки WPF (такие как размер по содержимому, растягивание и вложение) в реальных окнах приложений.
Рис. 4.19. Перемещение и изменение размеров элемента в InkCanvas
Колонка настроек Контейнеры компоновки, подобные Grid, значительно упрощают задачу создания общей структуры окна. Например, рассмотрим окно с настройками, показанное на рис. 4.20. Это окно располагает свои индивидуальные компоненты — метки, текстовые поля и кнопки — в табличной структуре.
Рис. 4.20. Настройки папки в колонке Чтобы создать эту таблицу, вы начинаете с определения строк и колонок сетки. Строки достаточно просты — размер каждой просто определяется по высоте содержимого. Это значит, что вся строка получит высоту самого большого элемента, которым в данном случае является кнопка Browse (Обзор) из третьей колонки.
Book_Pro_WPF-2.indb 124
19.05.2008 18:09:49
Компоновка
125
...
Далее вам нужно создать колонки. Размер первой и последней колонки определяется так, чтобы вместить их содержимое (текст метки и кнопку Browse соответственно). Средняя колонка получает все оставшееся пространство, а это значит, что она будет расти при увеличении размера окна, предоставляя вам больше места, чтобы видеть выбранную папку. (Если вы хотите ограничить ее ширину, можете указать свойство MaxWidth при определении колонки, как это делается с индивидуальными элементами). ... ...
Совет. Grid требует некоторого минимального пространства — достаточного, чтобы вместить полный текст метки, кнопку просмотра и несколько пикселей в средней колонке, показав текстовое поле. Если вы уменьшите включающее окно до размера меньше этого, то некоторое содержимое будет усечено. Как всегда, имеет смысл использовать свойства окна MinWidth и MinHeight, чтобы предотвратить такую ситуацию. Имея базовую структуру, вам просто нужно разместить элементы по правильным ячейкам. Однако вам также следует тщательно продумать поля и выравнивание. Каждый элемент нуждается в базовом поле (подходящим значением для него будет 3 единицы), чтобы создать небольшой отступ от края окна. Вдобавок метка и текстовое поле должно быть центрировано по вертикали, потому что их высота меньше, чем у кнопки Browse. И, наконец, текстовое поле должно использовать режим автоматической установки размера, растягиваясь для того, чтобы вместить всю колонку. Ниже показана разметка, которая понадобится вам для определения первой строки сетки. ... Home: Browse ...
Эту разметку можно повторить, добавляя все строки, при этом просто увеличивая значение атрибута Grid.Row. Один факт, который не сразу очевиден, связан с тем, насколько гибким является это окно благодаря использованию элемента управления Grid. Ни один из индивидуальных элементов — ни метки, ни текстовые поля, ни кнопки — не имеют жестко закодированных позиций и размеров. В результате вы можете легко вносить изменения в сетку, просто изменяя элементы ColumnDefinition. Более того, если вы добавите строку, которая имеет более длинный текст метки (что потребует расширения первой колонки), вся сет-
Book_Pro_WPF-2.indb 125
19.05.2008 18:09:49
126
Глава 4
ка будет откорректирована автоматически, сохраняя согласованность, включая строки, которые были добавлены ранее. И если вы захотите добавить элементы между существующими строками, такие как разделительные линии, чтобы отделить друг от друга разные разделы окна, вы можете сохранить те же колонки, но использовать свойство ColumnSpan для растяжения единственного элемента на большую область.
Динамическое содержимое Как демонстрирует показанная колонка настроек, окна, использующие контейнеры компоновки WPF, легко поддаются изменениям и адаптации по мере развития приложения. И преимущество этой гибкости проявляется не только во время проектирования. Это также ценное приобретение, если вам нужно отобразить содержимое, изменяющееся динамически. Примером может служить локализованный текст — текст, который появляется в вашем пользовательском интерфейсе и нуждается в переводе на разные языки для разных географических регионов. В приложениях старого стиля, опирающихся на координатные системы, изменение текста может разрушить внешний вид окна — в частности, потому, что краткие предложения английского языка становятся существенно длиннее на многих других языках. Даже если элементам позволено изменять свои размеры, чтобы вместить больший текст, это может нарушить общий баланс окна. На рис. 4.21 показано, как можно избежать этих неприятностей, если разумно использовать контейнеры компоновки WPF. В этом примере пользовательский интерфейс имеет опции краткого и длинного текста. Когда используется длинный текст, кнопки, содержащие текст, изменяют свой размер автоматически, расталкивая соседнее содержимое. И поскольку кнопки измененного размера разделяют один и тот же контейнер компоновки (в данном случае — колонку таблицы), весь раздел пользовательского интерфейса изменяет свой размер. В результате получается, что кнопки сохраняют согласованный размер — размер самой большой из них.
Рис. 4.21. Самонастраивающееся окно Чтобы заставить это работать, окно оснащено таблицей из двух колонок и двух строк. Колонка слева принимает кнопки изменяемого размера, в то время как колонка справа принимает текстовое поле. Нижняя строка используется для кнопки Close (Закрыть). Она находится в той же таблице, поэтому изменяет свой размер вместе с верхней строкой.
Book_Pro_WPF-2.indb 126
19.05.2008 18:09:49
Компоновка
127
Так выглядит полная разметка: Prev Next Show Long Text 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. Close
Модульный пользовательский интерфейс Многие из контейнеров компоновки успешно “заливают” содержимое в доступное пространство — так поступают StackPanel, DockPanel и WrapPanel. Одно из преимуществ этого подхода заключается в том, что он позволяет вам строить действительно модульные интерфейсы. Другими словами, вы можете подключать разные панели с соответствующими секциями пользовательского интерфейса, которые вы хотите показать, и пропускать те, которые в данный момент не нужны. Все приложение может подстраивать себя соответствующим образом — подобно портальному сайту в Web. Вы можете видеть это на рис. 4.22. Здесь в WrapPanel помещается несколько отдельных панелей. Пользователь может выбрать те панели, которые должны быть видимыми, используя флажки в верхней части окна. На заметку! Хотя вы можете установить фон панели компоновки, вы не можете установить границу вокруг него. Этот пример преодолевает это ограничение, помещая каждую панель в оболочку элемента Border, очерчивающего точные размеры. В следующей главе вы узнаете, как использовать Border и другие подобные специализированные контейнеры. Поскольку другие панели скрыты, оставшиеся реорганизуют себя, заполняя доступное пространство (и порядок, в котором они объявлены). На рис. 4.23 показана другая организация панелей. Чтобы скрыть или показать индивидуальные панели, нужен небольшой фрагмент кода, обрабатывающего щелчки на флажках. Хотя вы еще не рассматривали детально модель обработки событий WPF (этой теме будет посвящена глава 6), забегая вперед, скажем, что трюк состоит в установке свойства Visibility: panel.Visibility = Visibility.Collapsed;
Book_Pro_WPF-2.indb 127
19.05.2008 18:09:49
128
Глава 4
Рис. 4.22. Серии панелей в WrapPanel
Рис. 4.23. Сокрытие некоторых панелей Свойство Visibility — это часть базового класса UIElement, и потому поддерживается почти всеми объектами, которые вы помещаете в окно WPF. Оно принимает одно из трех значений перечисления System.Windows.Visibility, описанных в табл. 4.5.
Book_Pro_WPF-2.indb 128
19.05.2008 18:09:50
Компоновка
129
Таблица 4.5. Значения перечисления Visibility Значение
Описание
Visible
Элемент появляется в окне в нормальном виде.
Collapsed
Элемент не отображается и не занимает места.
Hidden
Элемент не отображается, но место за ним резервируется. (Другими словами, там, где он должен появиться, отображается пустое пространство.) Эта установка удобна, если вы хотите скрывать и показывать элементы, не меняя компоновки и относительного положения элементов в остальной части вашего окна.
На заметку! Вы можете использовать свойство Visibility для динамической подгонки вариантов интерфейса. Например, вы можете сделать сворачиваемую панель, которая может появляться сбоку вашего окна. Все, что потребцуется сделать для этого — поместить содержимое этой панели в некоторого рода контейнер компоновки и соответственно устанавливать его свойство Visibility. Остальное содержимое будет автоматически реорганизовано, чтобы заполнить доступное пространство.
Резюме В этой главе был дан детальный тур по новой модели компоновки WPF и показано, как размещать элементы в стеки, сетки и другие структуры. Мы построили более сложные компоновки, используя вложенные комбинации контейнеров компоновки, добавив GridSplitter для создания разделенных окон изменяемого размера. По ходу дела мы уделили особое внимание причинам, вызвавшим все эти значительные изменения, а именно — преимуществам, которые вы получаете при поддержке, расширении и локализации вашего пользовательского интерфейса. История компоновок далека от завершения. В следующих главах вы увидите много новых примеров, использующих контейнеры компоновки для организации групп элементов. Вы также узнаете о нескольких дополнительных средствах, позволяющих организовать содержимое окна.
• Специализированные контейнеры. Border, ScrollViewer и Expander предоставляют вам возможность создания содержимого, имеющего рамки, допускающего прокрутку и которое может быть свернуто и убрано с глаз долой. В отличие от панелей компоновки эти контейнеры могут содержать только один фрагмент содержимого. Однако вы легко можете использовать их в сочетании с панелями компоновки, чтобы получить тот эффект, который вам нужен. Мы попробуем эти контейнеры в действии в главе 5.
• Контейнер Viewbox. Нужен способ изменения размера графического содержимого (такого как графические изображения и векторная графика)? Viewbox — это еще один специализированный контейнер, который поможет вам в этом, обладая встроенным масштабированием. Первое знакомство с Viewbox произойдет в главе 5.
• Компоновка текста. WPF добавляет инструменты для компоновки крупных блоков стилизованного текста. Вы можете использовать плавающие фигуры и списки, применять выравнивание, колонки и изощренную технологию переносов, чтобы получить замечательно красивый результат. Как это делается, вы узнаете из главы 19.
Book_Pro_WPF-2.indb 129
19.05.2008 18:09:50
ГЛАВА
5
Содержимое В
предыдущей главе речь шла о системе компоновки в WPF, которая позволяет компоновать окно, помещая компоненты в специализированные компоновочные контейнеры. Благодаря этой системе, даже простое окно разбивается на вложенные серии контейнеров Grid, StackPanel и DockPanel. Пройдя всю серию вложений, вы найдете в конечном итоге видимые элементы (интерфейсные элементы, такие как кнопки, метки и текстовые окна) внутри различных контейнеров. Впрочем, контейнеры компоновки не являются единственным примером вложенных элементов. На самом деле, WPF построена на новой модели содержимого, позволяющей помещать компоненты в другие элементы, которые в любом другом случае используются как обычные элементы. Благодаря этой технологии вы можете брать многие простые элементы управления, такие как кнопки, и помещать в них картинки, векторные формы и даже контейнеры компоновки. Эта модель содержимого является одной из особенностей WPF, которые придают ей высокую степень гибкости. В этой главе мы рассмотрим базовый класс ContentControl, который поддерживает эту модель. Вы узнаете также о том, как используются специализированные наследники класса ContentControl, благодаря которым можно сделать панели прокручивающимися и сворачивающимися.
Элементы управления содержимым В главе 1 была показана иерархия классов, которая образует основу WPF. Вы узнали также о различиях, существующих между собственно элементами (к которым относится все, что вы помещаете в окно WPF) и элементами управления (к которым относятся специализированные элементы, являющиеся наследниками класса System.Windows. Controls.Control). В мире WPF элемент управления обычно описывается как элемент, который может получать фокус и принимать данные, вводимые пользователем — в качестве примера можно привести текстовое поле или кнопку. Однако отличие иногда бывает очень расплывчатым. ToolTip считается элементом управления, поскольку он появляется и исчезает в зависимости от перемещений указателя мыши. Label считается элементом управления, поскольку он поддерживает мнемонические команды (клавиши быстрого доступа, передающие фокус связанным элементам управления). Элементы управления содержимым (content control) — это специализированный тип элементов управления, которые могут хранить (и отображать) какую-то порцию содержимого. С технической точки зрения элемент управления содержимым является элементом управления, который может включать один вложенный элемент. Этим он отличается от контейнера компоновки, который может хранить сколь угодно много вложенных элементов.
Book_Pro_WPF-2.indb 130
19.05.2008 18:09:50
Содержимое
131
Совет. Естественно, вы можете поместить большой объем содержимого в один элемент управления содержимым — для этого потребуется упаковать все содержимое в один контейнер, такой как StackPanel или Grid. Например, класс Window сам является элементом управления содержимым. Очевидно, что окна часто хранят большие объемы содержимого, которое, однако, помещается в один контейнер верхнего уровня. (Как правило, таким контейнером является Grid.) Как было сказано в предыдущей главе, все контейнеры компоновки WPF являются наследниками класса Panel, что позволяет им хранить множество элементов. Точно так же, все элементы управления содержимым являются наследниками абстрактного класса ContentControl. Иерархия классов показана на рис. 5.1.
DispatcherObject
Условные обозначения Абстрактный класс
DependencyObject Конкретный класс Visual
UIElement
FrameworkElement
Control
ContentControl
Label
ScrollViewer
ButtonBase
UserControl
HeaderedContentControl
GroupBox
ToolTip
Window
TabItem
Expander
Рис. 5.1. Иерархия элементов управления содержимым
Book_Pro_WPF-2.indb 131
19.05.2008 18:09:50
132
Глава 5
Как можно видеть на рис. 5.1, некоторые элементы управления на самом деле являются элементами управления содержимым, в том числе Label и ToolTip. Кроме того, все типы кнопок являются элементами управления содержимым, включая хорошо знакомые Button, RadioButton и CheckBox. Существует еще несколько специализированных элементов управления содержимым, такие как классы ScrollViewer (он позволяет создавать прокручивающиеся панели) и UserControl (он позволяет повторно использовать специальное группирование элементов управления). Класс Window, который служит для представления каждого окна в вашем приложении, сам по себе является элементом управления содержимым. Кроме того, существует еще ряд элементов управления содержимым, которые являются наследниками класса HeaderedContentControl. Эти элементы управления имеют область содержимого и область заголовка, которые могут применяться для отображения некоторой разновидности заголовка. К этим элементам управления относятся GroupBox, TabItem (страница в TabControl) и Expander. На заметку! На рис. 5.1 кое-что упущено. На нем не показан элемент Frame, который используется для навигации содержимого (см. главу 9), и несколько элементов, применяемых внутри других элементов управления (например, окна списков и панели состояния).
Свойство Content В то время как класс Panel добавляет коллекцию Children для хранения вложенных элементов, класс ContentControl добавляет свойство Content, которое принимает один объект. Свойство Content поддерживает любой тип объектов, хотя все объекты оно разделяет на две группы, каждая из которых обрабатывается по-разному.
• Объекты, которые не являются наследниками класса UIElement. Элемент управления содержимым вызывает метод ToString() для получения текста для этих элементов управления, после чего отображает этот текст.
• Объекты, которые являются наследниками класса UIElement. Эти объекты (к ним относятся все визуальные элементы, которые являются частью WPF) отображаются внутри элемента управления содержимым с помощью метода UIElement. OnRender(). На заметку! С технической точки зрения метод OnRender() не рисует объект — он просто генерирует графическое представление, которое WPF отображает на экране по мере необходимости. Чтобы понять, как это работает, рассмотрим простую кнопку. В примерах с кнопками, которые мы видели до настоящего момента, была такая строка: Text content
Эта строка задается как содержимое кнопки и отображается на поверхности кнопки. Однако задачу можно усложнить, поместив в кнопку другие элементы. Например, с помощью класса Image в нее можно поместить изображение:
Как вариант, можно комбинировать текст и изображения, поместив их в контейнер компоновки вроде StackPanel:
Book_Pro_WPF-2.indb 132
19.05.2008 18:09:50
Содержимое
133
Image and text button Courtesy of the StackPanel
Обратите внимание, что в этом примере вместо элемента управлении Label используется элемент управления TextBlock (хотя работать будет любой из них). TextBlock представляет собой облегченный текстовый элемент, поддерживающий компоновку текста, но не поддерживающий использование клавиш быстрого доступа. В отличие от метки Label, TextBlock не является элементом управления содержимым. В главе 7 элементы управления TextBlock и Label будут рассмотрены более подробно. На заметку! Помещать текстовое содержимое внутрь элемента управления содержимым допускается потому, что синтаксический анализатор XAML преобразует его в строковый объект и использует для задания свойства Content. Поместить строку содержимого непосредственно в контейнер компоновки нельзя. Вместо этого вы должны упаковать его в класс, являющийся наследником UIElement, такой как TextBlock и Label. Если вы хотите создать действительно экзотическую кнопку, вы можете поместить в нее другие элементы управления содержимым, такие как текстовые поля и кнопки (а в них можно разместить другие элементы). Маловероятно, что подобный вариант интерфейса будет иметь смысл, но сама возможность построения такого интерфейса существует. На рис. 5.2 показаны некоторые образцы кнопок.
Рис. 5.2. Кнопки с разными типами вложенного содержимого Это та же модель содержимого, которую вы видели в окнах. Как и Button, класс Window допускает использование вложенных элементов, в качестве которых могут выступать порции текста, произвольные объекты или элемент.
Book_Pro_WPF-2.indb 133
19.05.2008 18:09:50
134
Глава 5
На заметку! Одним из нескольких элементов, которые нельзя поместить внутрь элемента управления содержимым, является Window. Когда вы создаете Window, он проверяет, не является ли он контейнером верхнего уровня. Если он помещен внутрь другого элемента, Window сгенерирует исключение. Помимо свойства Content класс ContentControl определяет еще кое-что. Он включает свойство HasContent, которое возвращает значение true, если в элементе управления имеется содержимое, и свойство ContentTemplate, которое позволяет создать шаблон, сообщающий элементу управления о том, как нужно отображать объект, который в любом другом случае не будет распознан. С помощью ContentTemplate вы можете более интеллектуальным способом отображать объекты, не являющиеся наследниками класса UIElement. Вместо того чтобы просто вызывать метод ToString() для получения строки, вы можете задать разные значения свойства, чтобы упорядочить их в более сложной разметке. О привязке данных в WPF речь пойдет в главе 16, а о шаблонах данных — в главе 17.
Выравнивание содержимого В главе 4 речь шла о том, как осуществляется выравнивание разных элементов управления в контейнере с помощью свойств HorizontalAlignment и VerticalAlignment, которые определены в базовом классе FrameworkElement. Однако после того как элемент управления получает содержимое, сразу возникает вопрос об его организации. Вам нужно решить, как будет выравниваться содержимое внутри вашего элемента управления. Для этой цели используются свойства HorizontalContentAlignment и VerticalContentAlignment. Свойства HorizontalContentAlignment и VerticalContentAlignment поддерживают те же значения, что и свойства HorizontalAlignment и VerticalAlignment. Это означает, что вы можете выровнять содержимое вдоль какого-нибудь края (Top, Bottom, Left или Right) или по центру (Center), либо можете растянуть его так, чтобы заполнить все доступное пространство (Stretch). Эти настройки применяются непосредственно к вложенному элементу содержимого, хотя вы можете задать множество уровней вложения, получив более изощренную компоновку. Например, если StackPanel вложить в элемент Label, то Label.HorizontalContentAlignment определит, где будет находиться элемент управления StackPanel, а оставшаяся компоновка будет определена опциями выравнивания и размеров элемента управления StackPanel и его потомков. В главе 4 вы познакомились также со свойством Margin, которое позволяет добавлять пустое пространство между соседними элементами. Элементы управления содержимым используют дополнительное свойство Padding, которое вставляет пустое пространство между краями элемента управления и краями содержимого. Чтобы посмотреть разницу, сравним следующие две кнопки: Absolutely No Padding Well Padded
В кнопке, в которой нет заполнения (по умолчанию так и есть), текст начинается от самого края кнопки. В кнопке, в которой с каждого края заполнены три единицы пространства, текст выглядит более привлекательно. Эту разницу можно увидеть на рис. 5.3. На заметку! Свойства HorizontalContentAlignment , VerticalContentAlignment и Padding определены как часть класса Control , а не как часть специфического класса ContentControl. Это связано с тем, что могут существовать элементы управления, которые не являются элементами управления содержимым, но все равно имеют некоторую разновидность содержимого. Одним из примеров является TextBox — содержащийся в нем текст (он хранится в свойстве Text) подчиняется заданными вами настройками выравнивания и заполнения.
Book_Pro_WPF-2.indb 134
19.05.2008 18:09:50
Содержимое
135
Рис. 5.3. Заполнение содержимого кнопки
Модель содержимого в WPF В настоящий момент у вас может возникнуть сомнение относительно того, действительно ли модель содержимого, используемая в WPF, стоит внимания. В конце концов, можно поместить изображение внутрь кнопки, а внедрять другие элементы управления и целые панели компоновки вряд ли имеет смысл. И все-таки в пользу этой модели имеются подходящие доводы. Рассмотрим пример, показанный на рис. 5.2, на котором элемент Image находится внутри элемента Button. Этот подход не является идеальным, поскольку битовые образы очень сильно зависят от разрешения. На экране монитора с высоким разрешением битовый образ может выглядеть размытым, так как WPF добавляет большое количество пикселей при интерполяции с целью сохранения корректных размеров. В более изощренных интерфейсах WPF битовые образы не используются, а вместо них применяется комбинация векторных форм (для создания кнопок с особым внешним видом) и других графических элементов (об этом мы поговорим в главе 13). Этот подход хорошо уживается вместе с моделью элементов управления содержимым. Поскольку класс Button является элементом управления содержимым, вы можете помещать в него не только фиксированные битовые образы, но и можете включать в него содержимое другого типа. Например, с помощью классов из пространства имен System.Windows.Shapes можно нарисовать векторное изображение внутри кнопки. Ниже показан пример, в котором создается кнопка с двумя ромбообразными формами (рис. 5.4):
Понятно, что в данном случае гораздо проще использовать модель вложенного содержимого, чем добавлять дополнительные свойства в класс Button для поддержки различных типов содержимого. Модель вложенного содержимого не просто более гибкая — она позволяет упростить интерфейс класса Button. А поскольку все элементы управления содержимым поддерживают вложение содержимого одинаковым образом, то отпадает необходимость добавлять различные свойства содержимого во многие классы. (В версии .NET 2.0 были улучшены классы Button и Label — в них была доработа-
Book_Pro_WPF-2.indb 135
19.05.2008 18:09:51
136
Глава 5
на поддержка изображений и смешанного содержимого, состоящего из изображений и текста.)
Рис. 5.4. Кнопка, имеющая содержимое в виде графических форм В сущности, модель вложенного содержимого является неким компромиссом. Она упрощает модель классов для элементов, поскольку в ее случае не нужно использовать дополнительные уровни наследования, чтобы добавить свойства для различного типа содержимого. Тем не менее, необходимо работать с чуть более сложной моделью объектов — элементами, которые могут быть построены из других вложенных элементов. На заметку! Вы не всегда сможете получить желаемый результат, заменяя содержимое элемента управления. Например, даже если вы можете поместить любое содержимое в кнопку, некоторые детали все равно никогда не изменятся, такие как затененный фон кнопки, скругленные границы и эффект при наведении указателя мыши, при котором поверхность кнопки осветляется, когда над ней находится указатель. Тем не менее, изменить встроенные детали можно путем применения нового шаблона элементов управления. В главе 15 будет показано, как с помощью шаблонов можно изменить все аспекты внешнего вида элемента управления.
Специализированные контейнеры В главе 7 рассматриваются все базовые возможности элементов управления и простые элементы управления содержимым, такие как Label и Button. А пока что мы займемся анализом некоторых более изощренных элементов управления содержимым: ScrollViewer, GroupBox, TabItem и Expander. Каждый из этих элементов управления создан для того, чтобы крупным порциям вашего пользовательского интерфейса можно было придать форму. Однако в связи с тем, что эти элементы управления могут содержать только один элемент, вы будете применять их в сочетании с контейнером компоновки.
Элемент управления ScrollViewer В предыдущей главе у вас была возможность познакомиться с некоторыми контейнерами. Однако ни один из них не обеспечивает поддержку прокрутки, которая будет являться ключевой возможностью, если вы захотите помещать большие объемы содержимого в ограниченный объем пространства. В WPF поддержку прокрутки обеспечить несложно, однако для этого потребуется специальный ингредиент — элемент управления содержимым ScrollViewer. Чтобы обеспечить поддержку прокрутки, вам нужно упаковать содержимое, которое вы хотите прокручивать, в ScrollViewer. Несмотря на то что этот элемент управления может хранить что угодно, обычно он используется для упаковки контейнера компоновки.
Book_Pro_WPF-2.indb 136
19.05.2008 18:09:51
Содержимое
137
Например, в главе 4 был показан пример, в котором элемент Grid применялся для создания таблицы с тремя столбцами текста, текстовых окон и кнопок. Для прокрутки элемента Grid потребуется лишь упаковать Grid в ScrollViewer, как это показано в следующей сокращенной разметке: ... ... Home: Browse ...
Результаты можно видеть на рис. 5.5.
Рис. 5.5. Прокручивающееся окно Если в этом примере изменить размеры окна, чтобы оно могло уместить в себе все содержимое, полоса прокрутки станет неактивной, хотя ее по-прежнему можно будет видеть. Вы можете управлять этим поведением с помощью свойства VerticalScroll BarVisibility, которое принимает значение из перечисления ScrollBarVisibility. Значение Visible, используемое по умолчанию, задает вертикальную линейку прокрутки. Значение Auto необходимо для того, чтобы линейка прокрутки появлялась по мере необходимости и исчезала, если она не нужна. Значение Disabled используется в том случае, если линейку прокрутки вообще не нужно отображать. На заметку! Можно также использовать значение Hidden , действие которого похоже на Disabled, хоть и с некоторыми отличиями. Во-первых, при скрытой линейке прокрутки содержимое все равно можно прокручивать. (Например, содержимое можно прокручивать с помощью клавиш управления курсором.) Во-вторых, содержимое в элементе управления ScrollViewer размещается по-другому. Когда вы присваиваете значение Disabled, это означает, что содержимое будет занимать столько места, сколько его есть в элементе управления ScrollViewer. С другой стороны, если вы присвоите значение Hidden, содержимое
Book_Pro_WPF-2.indb 137
19.05.2008 18:09:51
138
Глава 5
будет занимать неограниченное пространство. Это означает, что содержимое может выйти за пределы области прокрутки. Обычно значение Hidden используется, когда нужно обеспечить другой механизм прокрутки (например, с помощью специальных кнопок для прокрутки, о которых речь пойдет ниже). Значение Disabled применяется только в том случае, если вам нужно временно заблокировать работу элемента управления ScrollViewer.
ScrollViewer поддерживает также горизонтальную прокрутку, хотя свойство HorizontalScrollBarVisibility по умолчанию имеет значение Hidden. Чтобы использовать горизонтальную прокрутку, нужно вместо этого значения указать Visible или Auto.
Программная прокрутка Чтобы прокрутить содержимое окна, показанного на рис. 5.5, вы можете щелкнуть на линейке прокрутки, перетащить ползунок, прокрутить колесико мыши, можете воспользоваться клавишей табуляции для перехода от одного элемента управления к другому, а можете щелкнуть где-нибудь на пустом месте в сетке и нажимать клавиши управления курсором (а именно, клавиши со стрелками вверх и вниз). Если же этих возможностей вам не хватает, можете использовать методы класса ScrollViewer для прокрутки содержимого программным способом.
• Наиболее очевидными методами являются LineUp() и LineDown(), которые эквивалентны щелчку на кнопках со стрелками, расположенных на вертикальной линейке прокрутки, что приводит к однократной прокрутке содержимого вверх или вниз.
• Вы можете также использовать методы PageUp() и PageDown(), которые позволяют прокручивать все содержимое на экране вниз или вверх, что равносильно щелчку на поверхности линейки прокрутки, выше или ниже ползунка.
• Похожие методы позволяют прокручивать содержимое по горизонтали: LineLeft(), LineRight(), PageLeft() и PageRight().
• И, наконец, вы можете использовать методы ScrollToXxx(), чтобы перейти в какое-то определенное место. Для вертикальной прокрутки используются методы ScrollToEnd() и ScrollToHome(), которые переносят вас в заданную позицию. Существуют также и “горизонтальные” версии этих методов, к которым относятся ScrollToLeftEnd(), ScrollToRightEnd() и ScrollToHorizontalOffset(). На рис. 5.6 показан пример, в котором несколько специальных кнопок позволяют перемещаться по содержимому в элементе управления ScrollViewer. Каждая кнопка запускает простой обработчик события, который использует один из вышеупомянутых методов.
Рис. 5.6. Программная прокрутка
Book_Pro_WPF-2.indb 138
19.05.2008 18:09:51
Содержимое
139
Специальная прокрутка Встроенный вариант прокрутки элемента управления ScrollViewer является довольно полезным. Он позволяет плавно прокручивать любое содержимое, начиная со сложных векторных рисунков и заканчивая сеточными элементами. Однако одной из интригующих особенностей элемента управления ScrollViewer является возможность участия содержимого в процессе прокрутки. Что это означает, вы сейчас поймете.
• Вы помещаете прокручиваемый элемент внутрь элемента управления ScrollViewer. Это может быть любой элемент, реализующий интерфейс IScrollInfo.
• Вы сообщаете элементу управления ScrollViewer, что содержимое “знает” о способе прокрутки, присвоив свойству ScrollViewer.CanContentScroll значение true.
• Когда вы взаимодействуете с элементом управления ScrollViewer (посредством линейки прокрутки, колесика мыши, методов прокрутки и т.д.), он вызывает соответствующие методы при помощи интерфейса IScrollInfo. После этого элемент выполняет свою собственную прокрутку. На заметку! Интерфейс IScrollInfo определяет набор методов, которые отвечают на разные действия прокрутки. Например, он включает множество методов прокрутки, которыми обладает элемент управления ScrollViewer, таких как LineUp(), LineDown(), PageUp() и PageDown(). Кроме этого, он определяет методы, умеющие работать с колесиком мыши. Интерфейс IScrollInfo реализуют всего несколько элементов. Одним из них является контейнер StackPanel. Его реализация интерфейса IScrollInfo реализует логическую прокрутку — прокрутку, которая осуществляет переход от элемента к элементу, а не от строки к строке. Если вы поместите элемент управления StackPanel в ScrollViewer и не зададите свойство CanContentScroll, то получите обычное поведение. При прокрутке вверх или вниз будет происходить перемещение одновременно нескольких пикселей. А если свойству CanContentScroll присвоить значение true, то при каждом щелчке будет осуществляться переход к началу следующего элемента: 1 2 3 4
Вы обнаружите (а может, и нет), что система логической прокрутки контейнера
StackPanel будет полезна в приложении. Однако она будет крайне необходимой, если требуется создать специальную панель со специальным поведением прокрутки.
GroupBox и TabItem: элементы управления содержимым, имеющие заголовки Одним из наследников класса ContentControl является класс HeaderedContentControl. Его роль простая — он представляет контейнер, который может иметь как одноэлементное содержимое (хранится в свойстве Content), так и одноэлементный заголовок (хранится в свойстве Header). У класса ContentControl есть три наследника: GroupBox, TabItem и Expander. Элемент управления GroupBox является самым простым из них. Он отображается в
Book_Pro_WPF-2.indb 139
19.05.2008 18:09:51
140
Глава 5 виде окна со скругленными углами и заголовком. Ниже показан его пример (рис. 5.7). One Two Three Save
Обратите внимание на то, что для элемента управления GroupBox необходим контейнер (например, StackPanel), который поможет упорядочить его содержимое. GroupBox часто используется для группироваРис. 5.7. Базовое групповое окно ния небольших наборов связанных элементов управления, таких как переключатели. Однако этот элемент управления не имеет встроенных функций, поэтому вы можете применять его для любых целей. (Объекты RadioButton группируются посредством помещения их в любую панель. Элемент управления GroupBox использовать не обязательно, если только вам не нужна рамка со скругленными углами, имеющая заголовок.) TabItem представляет страницу в элементе управления TabControl. Класс TabItem добавляет одно важное свойство IsSelected, которое показывает, отображается ли в данный момент вкладка в элементе управления TabControl. Ниже представлена разметка, необходимая для того, чтобы создать простой пример, показанный на рис. 5.8. Setting One Setting Two Setting Three ...
Совет. Вы можете использовать свойство TabStripPlacement для того, чтобы вкладки отображались сбоку элемента TabControl, а не сверху, как это обычно бывает. Как и свойство Content, свойство Header может принимать любой тип объекта. Оно отображает классы-наследники UIElement, визуализируя их и используя метод ToString() для внутристрочного текста и всех других объектов. Это означает, что вы можете создать групповое окно или вкладку с графическим содержимым или произвольными элементами в заголовке. Ниже показан пример. Image and Text Tab Title
Book_Pro_WPF-2.indb 140
19.05.2008 18:09:51
141
Содержимое Setting One Setting Two Setting Three
Результаты показаны на рис. 5.9.
Рис. 5.8. Набор вкладок
Рис. 5.9. Экзотический заголовок вкладки
Элемент управления Expander Самым экзотическим элементом управления содержимым является Expander. Он упаковывает область содержимого, которую пользователь может показывать или скрывать, щелкая на небольшой кнопке со стрелкой. Эта технология используется часто в оперативных справочных системах, а также на Web-страницах, чтобы они могли включать большие объемы содержимого, не перегружая пользователей информацией, которую им не хочется видеть. На рис. 5.10 показано два представления окна с тремя расширителями. В версии, показанной слева, все три расширителя свернуты. В версии, показанной справа, все расширители развернуты. (Естественно, каждый пользователь может сворачивать или разворачивать любую комбинацию расширителей.) Применять элемент Expander очень просто — вам нужно всего лишь упаковать содержимое, которое вы хотите сделать разворачивающимся. Как правило, каждый элемент управления Expander сначала находится в свернутом состоянии, однако это можно изменить в разметке (или в коде), установив свойство IsExpanded. Ниже показана разметка, которая создает пример, представленный на рис. 5.10. Hidden Button One
Book_Pro_WPF-2.indb 141
19.05.2008 18:09:52
142
Глава 5
Lorem ipsum dolor sit amet, consectetuer adipiscing elit ... Hidden Button Two
Рис. 5.10. Скрытие содержимого в расширяемых областях Вы можете также выбрать направление, в котором будет развертываться расширитель. На рис. 5.10 используется стандартное значение (Down), хотя вы можете присвоить свойству ExpandDirection значение Up, Left или Right. Если расширитель свернут, стрелки всегда будут указывать на направление, в котором он будет разворачиваться. Поведение элемента управления Expander можно разнообразить за счет использования разных значений свойства ExpandDirection, потому что результат в остальной части вашего пользовательского интерфейса будет зависеть от типа контейнера. Некоторые контейнеры, такие как WrapPanel, просто растягивают другие элементы. Другие, подобные Grid, используют пропорциональную или автоматическую подгонку размеров. На рис. 5.11 показан пример сетки, насчитывающей четыре ячейки, с разными степенями расширения. В каждой ячейке расширитель имеет отличающееся направление. Размеры столбцов подбираются с сохранением пропорций, вследствие чего производится укладка текста в элементе управления Expander. (Столбец с автоматическим выбором размеров будет просто растянут, чтобы уместить текст, становясь при этом больше самого окна.) Для строк выбран автоматический выбор размеров, поэтому они растягиваются, чтобы уместить дополнительное содержимое. Элемент управления Expander особенно подходит для использования в WPF, так как WPF “поощряет” применение модели потоковой компоновки, которая может с легкостью обрабатывать области содержимого, растягивающиеся или сокращающиеся динамическим образом.
Book_Pro_WPF-2.indb 142
19.05.2008 18:09:52
Содержимое
143
Рис. 5.11. Развертывание в разных направлениях Если вам необходимо синхронизировать другие элементы управления с элементом управления Expander, вы можете для этой цели обрабатывать события Expanded и Collapsed. Вопреки тому, что подразумевается под этими событиями, они возникают как раз перед тем, как содержимое появляется или исчезает. Благодаря этому вы можете реализовать так называемую “ленивую” загрузку. Например, если процесс создания содержимого в элементе управления Expander является слишком дорогим, вы можете подождать до тех пор, пока оно не будет показано, и только затем сможете извлечь его. Или, возможно, вы захотите обновить содержимое перед тем, как оно будет показано. В любом случае, вы можете реагировать на событие Expanded для выполнения необходимой работы. На заметку! Если вас устраивают функции элемента управления Expander, но вам не нравится его стандартный внешний вид, не огорчайтесь. С помощью системы шаблонов в WPF вы можете полностью настроить стрелки разворачивания и сворачивания, чтобы они соответствовали стилю всего вашего приложения. Об этом мы будем говорить в главе 15.
Book_Pro_WPF-2.indb 143
19.05.2008 18:09:52
144
Глава 5
Как правило, когда вы разворачиваете Expander, его размеры увеличиваются, чтобы он мог уместить все содержимое. При этом, однако, может возникнуть проблема, если ваше окно не является достаточно большим, чтобы оно могло уместить все содержимое при развертывании. С этой проблемой можно справиться благодаря нескольким стратегиям.
• Вы можете задать минимальный размер окна (с помощью свойств MinWidth и MinHeight), чтобы оно могло уместить все содержимое, даже если окно будет иметь самые маленькие размеры.
• Вы можете задать свойство SizeToContent окна, чтобы окно развертывалось автоматически, когда вы будете открывать или закрывать Expander. Как правило, свойство SizeToContent имеет значение Manual, однако вы можете использовать значения Width или Height, чтобы развернуть его или свернуть до любого размера, достаточного для того, чтобы уместить содержимое.
• Вы можете ограничить размеры Expander, жестко закодировав его свойства Height и Width. К сожалению, это наверняка приведет к усечению содержимого, если оно окажется слишком большим.
• Вы можете создать прокручиваемую и разворачиваемую область с помощью элемента управления ScrollViewer. По большому счету, эти технологии являются довольно простыми. Единственное, что требует дальнейшего разъяснения — это комбинированное использование элементов управления Expander и ScrollViewer. Чтобы этот подход мог работать, вам нужно жестко закодировать размеры элемента управления ScrollViewer. В противном случае он будет просто развернут, чтобы уместить его содержимое. Ниже показан пример. ...
Хорошо иметь такую систему, в которой Expander сможет задавать размеры своей области содержимого, основываясь на доступном пространстве в окне. Однако это может породить очередные сложности. (Например, каким образом будет распределяться пространство между несколькими областями при разворачивании элемента управления Expander?) В качестве решения можно предложить контейнер компоновки Grid, но, к сожалению, он плохо интегрируется с элементом управления Expander. Если вы все же попробуете его использовать, вы получите неравномерно расставленные строки, которые не будут изменять свою высоту во время сворачивания элемента управления Expander.
Декораторы До настоящего времени мы говорили о нескольких контейнерах, предназначенных для того, чтобы вы могли управлять другими частями содержимого, включая ScrollViewer, GroupBox и Expander. Сейчас самое время сделать паузу и рассмотреть другую ветвь элементов, подобных контейнерам, которые не являются элементами управления содержимым. Речь идет о декораторах, которые обычно служат для того, чтобы графически разнообразить и украсить область вокруг объекта.
Book_Pro_WPF-2.indb 144
19.05.2008 18:09:52
Содержимое
145
Все декораторы являются наследниками класса System.Windows.Controls. Decorator. Большинство декораторов предназначено для использования вместе с определенными элементами управления. Например, элемент управления Button применяет декоратор ButtonChrome, чтобы создать свой “фирменный” скругленный угол и затененный фон, а элемент управления ListBox использует декоратор ListBoxChrome. Чтобы изменить внешний вид этих элементов управления, можно заменить их декораторы (об этом речь пойдет в главе 15). Есть еще два общих декоратора, применять которые имеет смысл при создании пользовательских интерфейсов: Border и Viewbox.
Декоратор Border Класс Border очень прост. Он принимает отдельную порцию вложенного содержимого (которым часто является панель компоновки) и добавляет к нему фон или рамку. Для управления декоратором Border предлагаются свойства, перечисленные в табл. 5.1.
Таблица 5.1. Свойства класса Border Имя
Описание
Background
Задает фон, который отображается за всем содержимым в рамке с помощью объекта Brush. Вы можете использовать сплошной цвет или что-то экзотическое.
BorderBrush и BorderThickness
Эти свойства задают цвет рамки, которая отображается по краю объекта Border, используя объект Brush, и ширину рамки, соответственно. Чтобы показать рамку, нужно задать оба свойства.
CornerRadius
Позволяет изящно закруглить углы рамки. Чем больше значение CornerRadius, тем более выразительным будет эффект закругления.
Padding
Добавляет пустое пространство (промежуток) между рамкой и содержимым, находящимся внутри. (В отличие от этого свойства, Margin добавляет пустое пространство за пределами рамки.)
Ниже показан пример простой рамки со слегка скругленными краями вокруг кнопок в контейнере StackPanel: One Two Three
На рис. 5.12 можно видеть результат. В главе 7 будет более подробно рассказано о кистях и цветах, которые вы можете использовать для задания BorderBrush и Background. Рис. 5.12. Базовая рамка
Book_Pro_WPF-2.indb 145
19.05.2008 18:09:52
146
Глава 5
На заметку! Элементы управления содержимым уже имеют свойства рамки. Так, например, элементы управления Expander, показанные на рис. 5.10 и 5.11, используют их для прорисовки контура вокруг разворачиваемой области. (Единственным исключением является элемент управления Button, который не использует свойства рамки, поскольку он работает с декоратором ButtonChrome.) Элемент Border предназначен для того, чтобы добавить рамку вокруг элементов, которые не имеют ее — т.е. это контейнеры компоновки, рассмотренные нами в предыдущей главе.
Декоратор Viewbox Viewbox — еще более экзотический декоратор. Его назначение вы сможете понять только тогда, когда ознакомитесь со специальным рисованием, о котором рассказывается в главе 13. Однако принцип, заложенный в основу Viewbox, понять несложно. Любое содержимое, которое вы помещаете в декоратор Viewbox, масштабируется таким образом, чтобы оно могло уместиться в этом декораторе. Процесс масштабирования, выполняемый декоратором Viewbox, более изощренный, чем настройка параметров выравнивания, о которых речь шла в главе 4. Когда вы растягиваете элемент, вы просто изменяете пространство, доступное для данного элемента. Это изменение не будет иметь никакого эффекта для большей части векторного содержимого, поскольку при рисовании векторов обычно используются фиксированные координаты. Например, рассмотрим пример кнопки с формами, которую вы уже видели ранее. Эта форма помещается в элемент Grid, который подгоняет свои размеры, чтобы уместить все многоугольники. Если придать кнопке большие размеры, форма не изменится — она просто будет центрирована внутри кнопки (рис. 5.13). Это связано с тем, что размер каждого многоугольника задается в абсолютных координатах.
Рис. 5.13. Графическая кнопка с измененными размерами Масштабирование, выполняемое декоратором Viewbox, подобно масштабированию, которое можно встретить в WPF, если вы увеличите системные настройки DPI. При масштабировании пропорционально изменяется каждый элемент экрана, включая изобра-
Book_Pro_WPF-2.indb 146
19.05.2008 18:09:53
Содержимое
147
жения, текст, линии и формы, а также рамки обычных элементов, таких как кнопки. Если вернуться к примеру с кнопкой и формами, и поместить сетку Grid в декоратор Viewbox, вы получите результат, показанный на рис. 5.14.
Рис. 5.14. Графическая кнопка с измененными размерами, которая использует декоратор Viewbox Несмотря на то что многоугольники в Grid используют жестко закодированные координаты, Viewbox знает, как их нужно преобразовывать. Способ преобразования координат выбирается путем сравнения требуемых размеров Grid (это размеры, которые должна принять сетка с учетом содержимого форм) и доступного размера. Например, если Viewbox в два раза больше требуемого размера Grid, то Viewbox масштабирует все свое содержимое с коэффициентом 2. На заметку! Как правило, вы будете использовать декоратор Viewbox для векторной графики, а не для обычных элементов и элементов управления. По умолчанию Viewbox выполняет пропорциональное масштабирование, которое сохраняет коэффициент пропорциональности своего содержимого. Это означает, что даже если будет изменена форма кнопки, форма внутри не изменится. (Наоборот, Viewbox прмиеняет самый большой коэффициент масштабирования, который подходит для внутренней части доступного пространства.) Однако это поведение можно изменить благодаря свойству Viewbox.Stretch. По умолчанию оно имеет значение Uniform.
Book_Pro_WPF-2.indb 147
19.05.2008 18:09:53
148
Глава 5
Если ему присвоить значение Fill, содержимое внутри декоратора Viewbox будет растянуто в обоих направлениях, чтобы занять все доступное пространство, даже если при этом будет искажен первоначальный рисунок. Кроме этого, вы можете использовать свойство StretchDirection. По умолчанию оно имеет значение Both, однако если ему присвоить значение UpOnly, то содержимое будет растянуто только вверх, а не по ширине, а если значение DownOnly — то содержимое будет растянуто только по ширине, а не вверх. Совет. Если вам нужны другие возможности управления, такие, например, как определение верхнего и нижнего пределов размеров содержимого, попробуйте ограничить размеры Viewbox (или его контейнера) с помощью свойств MaxHeight, MinHeight, MaxWidth и MinWidth.
Резюме Как вы могли видеть, WPF поддерживает несколько моделей содержимого. В предыдущей главе речь шла о панелях, которые могут содержать множество элементов и применять логику компоновки. В этой главе рассматривались элементы управления содержимым, которые содержат один элемент. Они бывают как базовыми (метки и кнопки), так и специализированными контейнерами, которые создают прокручиваемые и разворачиваемые области. Мы говорили также о декораторах, которые позволяют добавлять рамки и обладают свойствами динамического масштабирования. Однако на этом ресурсы WPF не исчерпываются. В последующих главах вы узнаете об элементах управления, построенных на другой модели содержимого — они могут хранить множество элементов, каждый из которых отображается специальным образом (в окне списка, дереве, меню и т.д.). Однако перед тем как рассматривать их, в следующей главе мы поговорим об изменениях, произведенных в системе событий WPF, и новом типе свойства.
Book_Pro_WPF-2.indb 148
19.05.2008 18:09:53
ГЛАВА
6
Свойства зависимостей и маршрутизируемые события К
аждый программист, работающий с .NET, знаком со свойствами и событиями, которые являются основными компонентами объектной абстракции .NET. Лишь немногие полагали, что с появлением WPF, которая является технологией пользовательских интерфейсов, произойдет изменение какого-либо из этих компонентов. Однако в конечном итоге, что весьма удивительно, именно это и произошло. Во-первых, вместо обычных свойств .NET в WPF появилось средство более высокого уровня — свойства зависимостей (dependency property). Свойства зависимостей гораздо эффективнее потребляют память и поддерживают такие высокоуровневые возможности, как уведомление об изменениях и наследование значений свойств (это способность распространять значения, используемые по умолчанию, вниз по дереву элементов). Свойства зависимостей являются также основой для определенного количества ключевых возможностей WPF, к числу которых можно отнести анимацию, привязку данных и стили. К счастью, несмотря на изменения, заложенные в самой основе, в своем коде вы по-прежнему можете считывать и устанавливать свойства зависимостей точно так же, как и при использовании традиционных свойств .NET. Во-вторых, вместо обычных событий .NET стали применяться события более высокого уровня — маршрутизируемые события (routed event). Маршрутизируемые события — это события, которые обладают большим, если можно так выразиться, “запасом энергии”, необходимой для их перемещения. Их суть простая — они могут спускаться или подниматься по дереву элементов, и по ходу своего путешествия попадают к обработчикам событий. Маршрутизируемые события позволяют выполнять обработку события в одном элементе (например, в метке), несмотря на то, что это событие может возникнуть в совершенно другом элементе (скажем, в изображении внутри этой метки). Как и свойства зависимостей, маршрутизируемые события могут использоваться обычным способом — путем подключения к обработчику событий, имеющему правильную сигнатуру. Однако прежде чем вы сможете разобраться с их возможностями, вы должны понять принципы их работы. Эту главу мы начнем с рассмотрения свойств зависимостей. Вы увидите, как они определяются, и какие возможности поддерживают. Затем мы перейдем к изучению системы событий WPF и узнаем, как генерируются и обрабатываются маршрутизируемые события. В конце главы мы поговорим о событиях, возникающих при работе с мышью и клавиатурой.
Book_Pro_WPF-2.indb 149
19.05.2008 18:09:53
150
Глава 6
Свойства зависимостей Свойства зависимостей являются совершенно новым воплощением свойств. Без них вы не сможете работать с основными средствами WPF, такими как анимация, привязка данных и стили. Большинство свойств, которыми обладают элементы WPF, являются свойствами зависимостей. Во всех примерах, которые были приведены до настоящего момента, вы использовали свойства зависимостей, даже не подозревая об этом. Это объясняется тем, что свойства зависимостей разработаны таким образом, чтобы с ними можно было работать как с обычными свойствами. И все же свойства зависимостей не являются обычными. Лучше всего представлять себе эти свойства как обычные (определяемые в .NET обычным образом), но обладающие дополнительным набором возможностей WPF. В концептуальном отношении поведение свойств зависимостей не отличается от поведения обычных свойств, однако “за кулисами” просматривается иная реализация. Причина здесь проста — производительность. Если бы разработчики WPF просто внесли дополнительные возможности в систему свойств .NET, то им пришлось бы создать сложный и громоздкий слой для вашего кода. Рядовые свойства не могут поддерживать все характеристики свойств зависимостей, не перегружая при этом систему. Свойства зависимостей являются специфическим детищем WPF. Тем не менее, в библиотеках WPF они всегда заключены в оболочки обычных процедур свойств .NET. Это позволяет использовать их обычным образом даже в том коде, который не имеет понятия о системе свойств зависимостей WPF. Может показаться странным, что старая технология упаковки теперь считается новой, однако именно так WPF может изменить основополагающий ингредиент, коим являются свойства, не нарушая структуру остального мира .NET.
Определение и регистрация свойства зависимостей Вы будете тратить больше времени на использование свойств зависимостей, нежели на их создание. Тем не менее, существует множество причин, вследствие которых вам нужно будет создавать собственные свойства зависимостей. Очевидно, что они будут являться ключевым ингредиентом при создании специального элемента WPF. Однако кроме этого они понадобятся в тех случаях, когда вам будет необходимо добавить привязку данных, анимацию или какую-то другую возможность WPF во фрагмент кода, который в любом другом случае не смог бы поддерживать их. Так, первый пример необходимого использования свойств зависимостей вы увидите в главе 9, когда мы будем сохранять специальную информацию в приложении со страничной организацией. Создать свойство зависимостей не очень сложно, хотя к синтаксису нужно привыкнуть. Он полностью отличается от синтаксиса обычного свойства .NET. Первым делом потребуется определить объект, который будет представлять ваше свойство. Это будет экземпляр класса DependencyProperty. Информация о вашем свойстве должна быть доступна постоянно и, возможно, ее нужно будет предоставлять разным классам (что является обычным делом для элементов WPF). По этой причине ваш объект DependencyProperty должен быть определен как статическое поле в связанном классе. Например, класс FrameworkElement определяет свойство Margin, которым пользуются все элементы. Следовательно, Margin — это свойство зависимостей. Это означает, что оно определяется в классе FrameworkElement следующим образом: public class FrameworkElement : UIElement, ... { public static readonly DependencyProperty MarginProperty; ... }
Book_Pro_WPF-2.indb 150
19.05.2008 18:09:53
Свойства зависимостей и маршрутизируемые события
151
Согласно условию, поле, представляющее свойство зависимостей, имеет имя обычного свойства плюс слово Property в конце. Таким образом, вы можете отделить определение свойства зависимостей от имени свойства. Поле определяется с ключевым словом readonly, что означает, что его можно задать только в статическом конструкторе для класса FrameworkElement. Определение объекта DependencyProperty является всего лишь первым шагом. Чтобы его можно было полностью задействовать, вы должны зарегистрировать свойство зависимостей в 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 и обратный вызов проверки. В следующих двух разделах речь пойдет как раз о них.
Book_Pro_WPF-2.indb 151
19.05.2008 18:09:53
152
Глава 6
Проверка правильности свойства Обратный вызов проверки позволяет произвести проверку, которую вы обычно добавляете в установочную часть процедуры свойства. Определяемый вами обратный вызов должен указывать на метод, который принимает объектный параметр и возвращает булевское значение. Значение true возвращается, чтобы принять объект как правильный, а значение false — чтобы отклонить его. Проверка свойства FrameworkElement.Margin не представляет ничего особо интересного, поскольку она основана на методе Thickness.IsValid(). Этот метод проверяет, является ли объект Thickness правильным в его нынешнем использовании (представляя минимум). Например, можно создать такой объект Thickness, который невозможно будет применить для задания минимума. В качестве примера можно привести объект Thickness с отрицательными измерениями. Если предоставленный объект Thickness не годится для границы, свойство IsMarginValid возвращает значение false. private static bool IsMarginValid(object value) { Thickness thickness1 = (Thickness) value; return thickness1.IsValid(true, false, true, false); }
Обратные вызовы проверки имеют одно ограничение: они являются статическими методами, не имеющими доступа к проверяемому объекту. Все, что вы получаете — это новое значение. С одной стороны это упрощает их повторное использование, зато с другой становится невозможным создать подпрограмму проверки, учитывающую остальные свойства. Классическим примером является элемент со свойствами Maximum и Minimum. Понятно, что вы не можете присвоить свойству Maximum значение, которое будет меньше значения свойства Minimum. Тем не менее, вы не сможете реализовать эту логику в обратном вызове проверки, поскольку вы сможете иметь доступ только к одному свойству одновременно. На заметку! Эту проблему лучше всего решать при помощи приведения значения. Приведение (coercion) — это такое действие, которое происходит перед самой проверкой, и позволяет изменить значение так, чтобы можно было расширить возможности его применения (например, увеличить значение Maximum, чтобы оно равнялось, по крайней мере, значению Minimum), или же чтобы вообще запретить изменение. Приведение обрабатывается в другом обратном вызове, который присоединяется к объекту FrameworkPropertyMetadata, рассматриваемому в следующем разделе.
Упаковщик свойства На завершающем этапе нужно заключить ваше свойство WPF в традиционную оболочку свойства .NET. Однако в то время как процедуры обычного свойства получают или задают значение приватного поля, процедуры свойства WPF используют методы GetValue() и SetValue(), определенные в классе DependencyObject. Ниже показан пример этих методов: public Thickness Margin { set { SetValue(MarginProperty, value); } get { return (Thickness)GetValue(MarginProperty); } }
Book_Pro_WPF-2.indb 152
19.05.2008 18:09:53
Свойства зависимостей и маршрутизируемые события
153
Когда вы создаете упаковщик свойства, вы должны включить только вызов методов
SetValue() и GetValue(), как в предыдущем примере. Вам не нужно будет добавлять какой-то дополнительный код для проверки значений, возбуждения событий и т.п. Это связано с тем, что остальные средства WPF могут пропускать упаковщик свойства и напрямую обращаться к методам SetValue() и GetValue(). (В качестве примера можно привести синтаксический анализ скомпилированного файла XAML во время выполнения.) Методы SetValue() и GetValue() являются общедоступными. На заметку! Упаковщик свойства не предназначен для проверки правильности данных или возбуждения события. Тем не менее, WPF предлагает специальное место для такого кода — обратные вызовы свойства зависимостей. Проверку следует выполнять в DependencyProperty. ValidateValueCallback, как было показано в предыдущем примере, а возбуждение событий — из FrameworkPropertyMetadata.PropertyChangedCallback, как будет демонстрироваться в следующем примере. Теперь у вас есть полностью готовое свойство зависимостей, которое можно задавать подобно любому другому свойству .NET с помощью упаковщика свойства: myElement.Margin = new Thickness(5);
Здесь следует отметить одну особенность. Свойства зависимостей подчиняются строгим правилам предшествования (старшинства) при определении текущих значений. Даже если вы не устанавливаете напрямую свойство зависимостей, оно уже может иметь значение — возможно, оно было присвоено во время привязки данных, определении стиля или анимации, или было унаследовано через дерево элементов. (О правилах предшествования речь пойдет в разделе “Как WPF использует свойства зависимостей” далее в главе.) Однако если вы установите значение напрямую, оно перезапишет существующее значение. Через некоторое время после этого вам может понадобиться удалить локальную настройку значения и сделать так, чтобы значение свойства определялось, даже если бы вы никогда его не задавали. Очевидно, что этого нельзя сделать, присваивая новое значение. Взамен вам нужно будет воспользоваться другим методом, являющимся наследником DependencyObject — ClearValue(). Ниже показано, как он работает: myElement.ClearValue(FrameworkElement.MarginProperty);
Метаданные свойств С технической точки зрения вам не нужно создавать объект FrameworkProperty Metadata, поскольку существует перегрузка метода Dependency.Register(), которой он не требуется. Тем не менее, объект FrameworkPropertyMetadata вам понадобится, если вы хотите сконфигурировать одно из многочисленных возможностей свойства зависимостей. На заметку! Платформа Windows Forms предлагает ту же функцию посредством обычных атрибутов .NET из нескольких пространств имен. Несмотря на то что вы по-прежнему можете использовать некоторые атрибуты в элементах WPF (например, чтобы присоединить специальные преобразователи типов), некоторые важные опции в системе сгруппированы в классе FrameworkPropertyMetadata. Многие из этих возможностей конфигурируются посредством простых булевских флагов. (Значением по умолчанию для каждого булевского флага является false.) Некоторые из них являются обратными вызовами, указывающими на специальные методы, которые вы создаете для выполнения специфической задачи. Доступные свойства перечислены в табл. 6.1.
06_Pro-WPF2.indd 153
20.05.2008 16:16:00
154
Глава 6
Таблица 6.1. Свойства класса FrameworkPropertyMetadata Имя
Описание
AffectsArrange, AffectsMeasure, AffectsParentArrange, AffectsParentMeasure
Если значением является true, свойство зависимостей может повлиять на расположение соседних элементов (или родительского элемента) во время прохода измерения и прохода упорядочения при выполнении операции компоновки. Например, свойство зависимостей Margin присваивает свойству AffectsMeasure значение true, сигнализируя о том, что в случае изменения границы элемента контейнеру компоновки придется повторить этап измерения, чтобы определить новое размещение элементов.
AffectsRender
Если значением является true, свойство зависимостей может повлиять на способ вычерчивания элемента, требуя при этом его перерисовку.
BindsTwoWayByDefault
Если значением является true, свойство зависимостей будет использовать двустороннюю привязку данных вместо односторонней, которая применяется по умолчанию. Однако вы можете явным образом определить поведение привязки.
Inherits
Если значением является true, то значение свойства зависимостей продвигается по дереву элементов и может наследоваться вложенными элементами. Например, Font является наследуемым свойством зависимостей — если вы зададите его в элементе более высокого уровня, оно будет унаследовано вложенными элементами, если только они явным образом не заменят его собственными параметрами шрифта.
IsAnimationProhibited
Если значением является true, свойство зависимостей не может использоваться в анимации.
IsNotDataBindable
Если значением является true, свойство зависимостей не может быть задано в выражении привязки.
Journal
Если значением является true, данное свойство зависимостей будет сохраняться в журнале (хронология посещенных страниц) приложения со страничной организацией.
SubPropertiesDoNotAffectRender
Если значением является true, WPF не будет повторно визуализировать объект, если будет изменено хотя бы одно из его подсвойств (т.е. свойство свойства).
DefaultUpdateSourceTrigger
Задает значение свойству Binding. UpdateSourceTrigger, если используется в выражении привязки. Свойство Binding.UpdateSourceTrigger определяет, когда значение границы данных применит свои изменения. Во время создания привязки свойство UpdateSourceTrigger можно задавать вручную.
DefaultValue
Задает значение по умолчанию для свойства зависимостей.
CoerceValueCallback
Обеспечивает обратный вызов, при котором производится попытка “исправить” значение свойства перед его проверкой.
PropertyChangedCallback
Обеспечивает обратный вызов, который производится в случае изменения значения свойства.
Book_Pro_WPF-2.indb 154
19.05.2008 18:09:54
Свойства зависимостей и маршрутизируемые события
155
Приведение свойства Важно понять взаимоотношение между методом ValidateValueCallback (который можно задавать в качестве аргумента метода DependencyProperty.Register()) и методами PropertyChangedCallback и CoerceValueCallback (которые можно задавать в качестве аргументов конструкторов при создании объекта FrameworkPropertyMetadata). Вот как они работают.
• Сначала метод CoerceValueCallback может изменить заданное значение (обычно, чтобы оно было согласованным с остальными свойствами) или возвратить
DependencyProperty.UnsetValue, что приведет к отклонению изменения.
• Затем в игру вступает ValidateValueCallback. Этот метод возвращает true, чтобы принять значение как правильное, или false, чтобы отклонить его. В отличие от CoerceValueCallback, данный метод не имеет доступа к самому объекту, в котором задается это свойство; это означает, что вы не сможете проверять значения остальных свойств.
• И, наконец, если оба предыдущих этапа будут пройдены, начинает действовать метод PropertyChangedCallback. С этого момента вы можете поднять событие изменения, если вам нужно сгенерировать уведомление остальным классам. Метод CoerceValueCallback хорошо подходит для работы с взаимосвязанными свойствами. Например, ScrollBar имеет свойства Maximum, Minimum и Value, каждое из которых является наследником класса RangeBase. Если будет задано свойство Maximum, оно будет приведено так, чтобы его значение не было меньше значения свойства Minimum: private static object CoerceMaximum(DependencyObject d, object value) { RangeBase base1 = (RangeBase)d; if (((double) value) < base1.Minimum) { return base1.Minimum; } return value; }
Другими словами, если значение свойства Maximum меньше значения свойства Minimum, то используется значение последнего свойства. Обратите внимание на то, что метод CoerceValueCallback передает два параметра: заданное значение и объект, к которому оно применяется. Если задано свойство Value, то выполняется точно такое же приведение. Это делается для того, чтобы значение этого свойства не оказалось за пределами диапазона, определяемого с помощью Minimum и Maximum, с помощью следующего кода: internal static object ConstrainToRange(DependencyObject d, object value) { double newValue = (double)value; RangeBase base1 = (RangeBase)d; double minimum = base1.Minimum; if (newValue < minimum) { return minimum; } double maximum = base1.Maximum; if (newValue > maximum) { return maximum; } return newValue; }
Book_Pro_WPF-2.indb 155
19.05.2008 18:09:54
156
Глава 6
Свойство Minimum вообще не использует приведение. Наоборот, после изменения значения вызывается метод PropertyChangedCallback, который принудительно приводит свойства Minimum и Value, вручную запуская их приведение: private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { RangeBase base1 = (RangeBase)d; ... base1.CoerceValue(RangeBase.MaximumProperty); base1.CoerceValue(RangeBase.ValueProperty); }
Точно так же, после того как будет задано и приведено значение свойства Maximum, оно вручную приводит значение свойства Value: private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { RangeBase base1 = (RangeBase)d; ... base1.CoerceValue(RangeBase.ValueProperty); base1.OnMaximumChanged((double) e.OldValue, (double) e.NewValue); }
Конечный результат таков, что, если вы зададите конфликтные значения, приоритет будет отдан значению свойства Minimum, а Maximum будет задействовано после него (возможно, его значение будет приведено свойством Minimum); после этого применяется свойство Value (его значение может быть приведено свойствами Maximum и Minimum). Задача этой запутанной последовательности заключается в том, чтобы свойства ScrollBar могли быть заданы в разном порядке, не допуская при этом возникновения ошибки. Это может иметь большое значение для инициализации, когда, например, создается окно для документа XAML. Все элементы управления WPF гарантируют, что их свойства могут быть заданы в любом порядке, без изменения поведения. Однако при тщательном анализе данная задача требует доказательства. Например, рассмотрим следующий код: ScrollBar bar = new ScrollBar(); bar.Value = 100; bar.Minimum = 1; bar.Maximum = 200;
Когда элемент ScrollBar создается первый раз, свойство Value имеет значение 0, Minimum — 0, а Maximum — 1. После второй строки кода значение свойства Value приводится к 1 (так как изначально свойство Maximum по умолчанию имеет значение 1). Однако когда вы дойдете до четвертой строки кода, вы увидите нечто примечательное. Когда свойство Maximum изменяет свое значение, операция приведения значения выполняется над свойствами Minimum и Value. Эта операция выполняется над значениями, которые были определены вами изначально. Другими словами, локальное значение 100 по-прежнему сохраняется в системе свойств зависимостей WPF, и теперь, поскольку оно является применимым значением, его можно применить к свойству Value. Таким образом, после выполнения этой одной строки кода будут изменены два свойства. Ниже показано, как это происходит. ScrollBar bar = new ScrollBar(); bar.Value = 100; // (Сейчас bar.Value возвращает 1.)
Book_Pro_WPF-2.indb 156
19.05.2008 18:09:54
Свойства зависимостей и маршрутизируемые события
157
bar.Minimum = 1; // (bar.Value по-прежнему возвращает 1.) bar.Maximum = 200; // (А теперь bar.Value возвращает 100.)
Это поведение сохраняется независимо от того, когда вы зададите свойство Maximum. Например, если вы присвоите свойству Value значение 100 при загрузке окна, а затем, когда пользователь щелкнет на кнопке, присвоите значение свойству Maximum, то свойство Value по-прежнему будет иметь значение 100. (Единственное, что можно сделать, чтобы этого не происходило — задать другое значение или удалить локальное значение, которое вы установили с помощью метода ClearValue(), наследуемого всеми элементами из DependencyObject.) Это поведение объясняется действием системы разрешения свойств WPF, которая хранит точно то же локальное значение, которое вы задали, но оценивает, каким должно быть свойство (используя приведение и другие технологии), когда вы читаете его. Более подробно об этой системе мы поговорим в разделе “Как WPF использует свойства зависимостей” далее в этой главе. На заметку! Программисты, являющиеся “ветеранами” Windows Forms, должны помнить интерфейс ISupportInitialize, который использовался для решения подобных проблем в инициализации свойств путем упаковки серии изменений свойств в пакетный процесс. Несмотря на то что вы можете применять этот интерфейс в WPF (а синтаксический анализатор XAML приветствует его), с этой технологией способны работать лишь несколько элементов WPF. Такие проблемы лучше всего решать посредством приведения значения. Существует несколько причин, по которым предпочтительным является вариант приведения. Например, с помощью операции приведения можно решить другие проблемы, которые могут возникнуть, если неправильное значение будет задано в процессе привязки данных или анимации, в отличие от интерфейса ISupportInitialize.
Совместно используемые свойства зависимостей Некоторые классы совместно используют одно и то же свойство зависимостей, даже если они имеют отдельные иерархии классов. Например, TextBlock.FontFamily и Control.FontFamily указывают на одно и то же статическое свойство зависимостей, которое определено в классе TextElement и TextElement.FontFamilyProperty. Статический конструктор TextElement регистрирует свойство, а статические конструкторы TextBlock и Control просто повторно используют его посредством вызова метода DependencyProperty.AddOwner(): TextBlock.FontFamilyProperty = TextElement.FontFmamilyProperty.AddOwner(typeof(TextBlock));
Вы можете применять эту же технологию при создании собственных специальных классов (предполагая, что свойство еще не определено в классе, от которого вы наследуете, в результате чего вы получаете его “задаром”). Можно также использовать перегрузку метода AddOwner(), что позволит определить обратный вызов проверки и новый объект FrameworkPropertyMetadata, который будет применяться только к этому новому использованию свойства зависимостей. Повторное использование свойств зависимостей может привести к некоторым странным побочным эффектам в WPF, которые особенно проявляются в стилях. Например, если вы применяете стиль для автоматического задания свойства TextBlock.FontFamily, ваш стиль повлияет также и на свойство Control.FontFamily, поскольку “за кулисами” оба класса используют одно и то же свойство зависимостей. Этот феномен вы увидите в действии в главе 12.
Book_Pro_WPF-2.indb 157
19.05.2008 18:09:54
158
Глава 6
Прикрепляемые свойства зависимостей В главе 2 было рассказано о специальном типе свойства зависимостей, называемом прикрепляемым свойством. Прикрепляемое свойство (attached property) — это свойство зависимостей, которым управляет система свойств WPF. Его отличительной чертой является тот факт, что прикрепляемое свойство применяется к классу, отличному от того, в котором оно определено. Наиболее характерный пример прикрепляемых свойств можно найти в контейнерах компоновки, описанных в главе 4. Например, класс Grid определяет прикрепляемые свойства Row и Column, которые вы задаете во внедренных элементах, чтобы показать, где они должны быть расположены. Точно так же DockPanel определяет прикрепляемое свойство Dock, а Canvas — прикрепляемые свойства Left, Right, Top и Bottom. Для определения прикрепляемого свойства нужно воспользоваться методом RegisterAttached(), а не Register(). Ниже показан пример регистрации свойства 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 для прикрепляемых свойств требуется пара статических методов, которые могут быть вызваны для установки и получения значения свойства. Эти методы используют знакомые вам методы SetValue() и GetValue() (они являются наследниками класса DependencyObject). Статические методы должны иметь имена наподобие SetИмяСвойства() и GetИмяСвойства(). Ниже показаны статические методы, реализующие прикрепляемое свойство 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 с помощью такого кода: Grid.SetRow(txtElement, 0);
Book_Pro_WPF-2.indb 158
19.05.2008 18:09:54
Свойства зависимостей и маршрутизируемые события
159
Как вариант, вы можете напрямую вызвать метод SetValue() или GetValue() и пропустить статические методы: txtElement.SetValue(Grid.RowProperty, 0);
Метод SetValue() имеет одну странную особенность. Несмотря на то что XAML не позволяет применять его, вы можете использовать перегруженную версию метода SetValue() в коде, чтобы присоединить значение любому свойству зависимостей, даже если это свойство не определено как прикрепляемое. Например, вполне допустимым является следующий код: ComboBox comboBox = new ComboBox(); ... comboBox.SetValue(PasswordBox.PasswordCharProperty, "*");
Здесь значение свойства PasswordBox.PasswordChar задается для объекта ComboBox, несмотря даже на то, что PasswordBox.PasswordCharProperty зарегистрировано как обычное свойство зависимостей, а не как прикрепляемое свойство. Это действие не изменит способ работы ComboBox — в конце концов, код внутри ComboBox не будет искать значение свойства, о существовании которого ничего не известно, — однако вы можете в своем коде работать со значением PasswordChar. Несмотря на редкое применение, этот трюк позволяет глубже понять принцип работы системы свойств WPF и демонстрирует отличные возможности в плане расширяемости. Он показывает также, что несмотря на то, что прикрепляемые свойства регистрируются с помощью другого метода, а не как обычные свойства зависимостей, WPF не проводит между ними различий. Единственным отличием является то, что может разрешить анализатор синтаксиса XAML. Если вы не зарегистрируете свойство как прикрепляемое, вы не сможете задавать его в остальных элементах разметки.
Как WPF использует свойства зависимостей На страницах этой книги вы увидите, что свойства зависимостей необходимы самым разным средствам WPF. Тем не менее, все эти средства имеют два ключевых поведения, поддерживаемых каждым свойством зависимостей — это уведомление об изменении и динамическое разрешение значений. Как вы уже видели, при изменении значения свойства зависимостей осуществляется обратный вызов. Этот обратный вызов является частью низкоуровневой системы WPF; он отвечает за обновление привязок данных и запуск триггеров. Вопреки предположениям, свойства зависимостей не генерируют автоматически события, чтобы вы могли знать, когда изменяется значение свойства. Вместо этого они запускают защищенный метод OnPropertyChangedCallback(). Он передает информацию двум службам WPF (привязка данных и триггеры) и вызывает метод PropertyChangedCallback, если он будет определен. Другими словами, если вы хотите выполнить действие в случае изменения свойства, у вас есть два варианта: вы можете создать привязку, которая будет использовать значение свойства (см. главу 15), или написать триггер, который будет автоматически изменять другое свойство или запускать анимацию (см. главу 12). Однако свойства зависимостей не могут дать вам обобщенный способ запуска некоторого кода в ответ на изменение свойства. На заметку! Если вы работаете с созданным вами же элементом управления, вы можете воспользоваться механизмом обратного вызова свойства, чтобы реагировать на изменения свойства и даже генерировать событие. Многие обычные элементы управления используют этот прием для свойств, которые соответствуют информации, заданной пользователем. Например, элемент управления TextBox имеет событие TextChanged , а ScrollBar — событие ValueChanged. Элемент управления может реализовывать подобную функцию с помощью объекта PropertyChangedCallback, однако свойства зависимостей не имеют этой функции, что объясняется вопросами производительности.
Book_Pro_WPF-2.indb 159
19.05.2008 18:09:54
160
Глава 6
Второй особенностью, которая определяет характер работы свойств зависимостей, является динамическое разрешение значения. Это означает, что когда вы извлекаете значение из свойства зависимостей, WPF будет учитывать несколько факторов. Вы уже могли видеть это в действии на примере элемента управления ScrollBack, в котором свойство Value зависело от значения, заданного в коде локальным образом, а также от метода CoerceValueCallback. Такое поведение и объясняет название этих свойств — по сути, свойство зависимостей зависит от множества поставщиков свойств, каждый из которых имеет свой уровень приоритета. Когда вы извлекаете значение из свойства, система свойств WPF выполняет набор действий, направленных на достижение конечного значения. Сначала она определяет базовое значение свойства, учитывая следующие факторы, перечисленные в порядке возрастания приоритета. 1. Значение по умолчанию (задается объектом FrameworkPropertyMetadata). 2. Унаследованное значение (если задан флаг FrameworkPropertyMetadata. Inherits и значение было передано элементу где-то выше в иерархии). 3. Значение из стиля темы (см. главу 15). 4. Значение из стиля проекта (см. главу 12). 5. Локальное значение (другими словами, значение, заданное вами непосредственно в этом объекте с помощью кода или XAML). Как показывает этот список, вы переопределяете всю иерархию, применяя значение напрямую. Если же нет, значение передается вверх по списку следующему элементу, который может его принять. На заметку! Одно из преимуществ этой системы состоит в том, что она является очень экономичной. Если значение свойства не было задано локально, WPF извлечет его значение из стиля, другого элемента, либо выберет значение, заданное по умолчанию. При этом не требуется выделять память для хранения значения. Оценить “экономический эффект” можно, если добавить к форме несколько кнопок. Каждая кнопка имеет десятки свойств, которые вообще не будут расходовать память, если они будут заданы посредством одного из этих механизмов. WPF придерживается предыдущего списка, чтобы определить базовое значение свойства зависимостей. Однако базовое значение не обязательно является конечным значением, которое вы извлечете из свойства. Это связано с тем, что WPF рассматривает несколько других поставщиков, которые могут изменить значение свойства. Ниже описан процесс определения значения свойства, выполняемый WPF. 1. Определяется базовое значение (как было сказано выше). 2. Если свойство задается с помощью выражения, производится оценка этого выражения. На данный момент WPF поддерживает два типа выражений: ресурсы (см. главу 11) и привязка данных (см. главу 15). 3. Если это предназначено для анимации, применяется эта анимация. 4. Запускается метод CoerceValueCallback для “корректировки” значения. 5. Запускается метод PropertyChangedCallback для запрета неправильных данных. По сути, свойства зависимостей жестко связаны с небольшим набором служб WPF. Если бы в данной инфраструктуре этого не было, они могли бы породить излишнюю сложность и добавить значительные накладные расходы.
Book_Pro_WPF-2.indb 160
19.05.2008 18:09:55
Свойства зависимостей и маршрутизируемые события
161
Совет. В будущих версиях WPF к свойствам зависимостей будут добавлены дополнительные службы. Когда вы будете разрабатывать специальные элементы (об этом речь пойдет в главе 24), вы, возможно, будете использовать свойства зависимостей для большинства (если не всех) их общедоступных свойств.
Маршрутизированные события Каждый разработчик, использующий .NET, знаком с понятием события — это сообщение, которое посылается объектом (например, элементом WPF), чтобы уведомить код о том, что произошло что-то важное. WPF улучшает модель событий .NET благодаря новой концепции маршрутизации событий. Маршрутизация позволяет событию возникать в одном элементе, а генерироваться — в другом. Например, маршрутизация событий позволяет щелчку, начавшемуся в кнопке панели инструментов, генерироваться в панели инструментов, а затем во вмещающем панель окне, и только потом обрабатываться вашим кодом. Маршрутизация событий предлагает большую гибкость для написания лаконичного кода, который сможет обрабатывать события в более удобном для этого месте. Она необходима также для работы с моделью содержимого 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); } } ... }
Book_Pro_WPF-2.indb 161
19.05.2008 18:09:55
162
Глава 6
В то время как свойства зависимостей регистрируются посредством метода
DependencyProperty.Register(), маршрутизируемые события регистрируются с помощью метода EventManager.RegisterRoutedEvent(). При регистрации события нужно указать имя события, тип маршрута (об этом — чуть позже), делегат, определяющий синтаксис обработчика события (в данном примере это RoutedEventHandler), и класс, к которому принадлежит событие (в данном примере это ButtonBase). Как правило, маршрутизируемые события упаковываются в обычные события .NET, что делает их доступными для всех языков .NET. Упаковщик события добавляет и удаляет зарегистрированные вызывающие объекты с помощью методов AddHandler() и RemoveHandler(), каждый из которых определен в базовом классе FrameworkElement и наследуется каждым элементом WPF. Естественно, как и любое событие, определяющий класс должен где-то сгенерировать его. Это осуществляется в разделе реализации. Тем не менее, важно отметить, что ваше событие не генерируется через традиционный упаковщик событий .NET. Вместо этого используется метод RaiseEvent(), наследуемый каждым элементом от класса UIElement. Ниже представлен соответствующий код класса ButtonBase: RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this); base.RaiseEvent(e);
Метод RaiseEvent() отвечает за генерацию события для каждого вызывающего объекта, который был зарегистрирован с помощью метода AddHandler(). Поскольку этот метод является общедоступным, вызывающим объектам предоставляется выбор — они могут зарегистрироваться самостоятельно, вызывая метод AddHandler(), или могут воспользоваться упаковщиком события. (В следующем разделе продемонстрированы оба подхода.) В любом случае, они будут уведомлены о вызове метода RaiseEvent(). Как и свойства зависимостей, определение маршрутизируемого события может совместно использоваться разными классами. Например, два базовых класса используют событие MouseUp: UIElement (является отправной точкой для обычных элементов WPF) и ContentElement (является отправной точкой элементов содержимого, представляющего собой отдельные части содержимого, которое можно помещать в поток документа). Событие MouseUp определяется классом System.Windows.Input.Mouse. Классы UIElement и ContentElement просто повторно используют его с помощью метода RoutedEvent.AddOwner(): UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));
Все события WPF придерживаются знакомого вам условия о сигнатурах событий, существующего в .NET. Первый параметр каждого обработчика события содержит ссылку на объект, который сгенерировал событие (отправитель). Второй параметр представляет объект EventArgs, объединяющий любые дополнительные детали, которые могут быть важными. Например, событие MouseUp предлагает объект MouseEventArgs, который показывает, какая кнопка мыши была нажата при возникновении события: private void img_MouseUp(object sender, MouseButtonEventArgs e) { }
В приложениях Windows Forms для многих событий обычно применялся базовый класс EventArg, если им не требовалось передавать дополнительную информацию. В приложениях WPF ситуация иная, поскольку в них поддерживается модель маршрутизируемых событий. В WPF, если событие не должна сопровождать какая-либо дополнительная информация, оно использует класс RoutedEventArgs, который включает некоторые подробности относительно маршрутизации события. Если событие должно передать дополнительную информацию, оно будет использовать более специализированный объект, являющийся
Book_Pro_WPF-2.indb 162
19.05.2008 18:09:55
Свойства зависимостей и маршрутизируемые события
163
наследником RoutedEventArgs (как MouseButtonEventArgs в предыдущем примере). Поскольку каждый класс аргумента события WPF происходит от RoutedEventArgs, каждый обработчик события WPF имеет доступ к информации о маршрутизации события.
Присоединение обработчика событий Как было сказано в главе 2, присоединить обработчик события можно несколькими способами. Чаще всего для этой цели добавляется атрибут события в разметку XAML. Данный атрибут события получает имя события, которое вы хотите обрабатывать, а его значение получает имя метода обработчика события. Ниже показан пример, в котором этот синтаксис применяется для соединения события MouseUp элемента Image с обработчиком события img_MouseUp:
Хотя это и не обязательно, обычно имя метода обработчика события задается в виде
ИмяЭлемента_ИмяСобытия. Если элемент не имеет определенного имени (возможно, по причине того, что вам не нужно взаимодействовать с ним в любом другом месте в вашем коде), попробуйте использовать имя, которое он мог бы иметь: OK
Совет. У вас может возникнуть желание присоединить событие к высокоуровневому методу, выполняющему задачу, однако вы получите большую гибкость, если у вас будет дополнительный уровень кода обработки событий. Например, когда вы щелкнете на кнопке cmdUpdate, она не вызовет метод UpdateDatabase() напрямую. Вместо этого она должна вызвать обработчик события, например, cmdUpdate_Click(), вызывающий метод UpdateDatabase(), который и сделает всю работу. Этот шаблон позволяет изменить местонахождение кода вашей базы данных, заменить кнопку обновления другим элементом управления, привязать несколько элементов управления к одному и тому же процессу — и все это при полной возможности изменять в последующем пользовательский интерфейс. Если вам необходим простой способ работы с действиями, которые могут запускаться из нескольких разных мест в пользовательском интерфейсе (кнопки панели инструментов, команды меню и т.д.), вам нужно будет добавить средство команд WPF, описанное в главе 10. Вы можете также соединить событие с кодом. Ниже приведен эквивалент кода разметки XAML, показанной выше: img.MouseUp += new MouseButtonEventHandler(img_MouseUp);
Этот код создает объект делегата, имеющий правильную сигнатуру для события (в данном случае это экземпляр делегата MouseButtonEventHandler) и указывающий метод img_MouseUp(). Затем он добавляет делегата в список зарегистрированных обработчиков событий для события img.MouseUp. Язык C# разрешает применять более рациональный синтаксис, явным образом создающий подходящий объект делегата: img.MouseUp += img_MouseUp;
Подход с использованием кода будет полезным, если нужно динамически создавать элемент управления и присоединять обработчик события в некоторой точке в течение времени существования окна. Для сравнения скажем, что события, которые вы включаете в XAML, всегда присоединяются при первом создании экземпляра объекта окна. Этот подход позволяет также упростить и рационализировать код XAML, что будет исключительно полезным, если вы планируете совместно использовать его не с программистами, а, скажем, с художниками-дизайнерами. Недостатком является увеличение количества строк кода, который “раздует” ваши файлы с кодом.
Book_Pro_WPF-2.indb 163
19.05.2008 18:09:55
164
Глава 6
Подход, продемонстрированный в предыдущем коде, основан на упаковщике события, который вызывает метод UIElement.AddHandler(), показанный в предыдущем разделе. Вы можете также связать событие напрямую, самостоятельно вызвав метод UIElement.AddHandler(). Ниже показан пример: img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
Если вы будете использовать этот подход, вам всегда придется создавать подходящий тип делегата (например, MouseButtonEventHandler). Вы не можете создать объект делегата неявно, как это делается при подключении события через упаковщик свойства. Это объясняется тем, что метод UIElement.AddHandler() поддерживает все события WPF и не знает о том, какой тип делегата вы хотите использовать. Некоторые разработчики предпочитают использовать имя класса, в котором определено событие, а не имя класса, сгенерировавшего событие. Ниже показан эквивалентный синтаксис, наглядно демонстрирующий, как событие MouseUpEvent определено в UIElement. img.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
На заметку! Выбор подхода зависит от ваших предпочтений. Недостаток второго подхода — он не дает ясного представления о том, что класс Image предлагает событие MouseUpEvent. Этот код можно неправильно понять и предположить, что он присоединяет обработчик события, предназначенный для обработки события MouseUpEvent во вложенном элементе. Об этой технологии мы поговорим в разделе “Прикрепляемые события” далее в этой главе. Если вы хотите отсоединить обработчик события, то единственным решением в этом случае является написание соответствующего кода. Вы можете воспользоваться операцией -=, как показано ниже: img.MouseUp -= img_MouseUp;
Как вариант, можно вызвать метод UIElement.RemoveHandler(): img.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
Технически возможно соединить один и тот же обработчик события с одним и тем же событием более одного раза. Однако обычно это приводит к возникновению ошибки в коде. (В этом случае обработчик события будет запускаться множество раз.) Если вы попытаетесь удалить обработчик события, который был подключен дважды, событие вызовет обработчик события, но только один раз.
Маршрутизация событий Как упоминалось в предыдущей главе, многие элементы управления в WPF являются элементами управления содержимым, которые могут иметь разный тип и разный объем вложенного содержимого. Например, вы можете создать графическую кнопку без формы, создать метку, которая будет совмещать текст и рисунки, или поместить содержимое в специальный контейнер, чтобы его можно было прокручивать или разворачивать окно во весь экран. Вы можете даже повторять процесс “вкладывания” столько раз, сколько вы хотите получить уровней. При этом возникает интересный вопрос. Например, предположим, что имеется метка, в которой содержится панель StackPanel, и вместе они формируют два блока текста и изображения:
Book_Pro_WPF-2.indb 164
19.05.2008 18:09:55
Свойства зависимостей и маршрутизируемые события
165
Image and text label Courtesy of the StackPanel
Как вам уже известно, каждый ингредиент, который вы помещаете в окно WPF, является наследником класса UIElement, включая Label, StackPanel, TextBlock и Image. Класс UIElement определяет несколько ключевых событий. Например, каждый класс, являющийся потомком UIElement, предлагает события MouseUp и MouseDown. А теперь давайте подумаем, что произойдет, если вы щелкнете на изображении в данной метке. Понятно, что при этом возникнут события Image.MouseDown и Image. MouseUp. А если вам нужно обрабатывать все щелчки на метке одинаковым образом? В этом случае не будет разницы в том, где щелкнул пользователь — на изображении, на тексте или на пустом месте в области метки. В любом из этих случаев вы должны будете реагировать с помощью одного и того же кода. Понятно, что вы могли бы привязать один и тот же обработчик к событиям MouseDown и MouseUp каждого элемента, однако это может запутать код и усложнить сопровождение разметки. WPF предлагает более удобное решение за счет модели маршрутизируемых событий. Маршрутизируемые события бывают трех видов.
• Прямые события (direct event) подобны обычным событиям .NET. Они возникают в одном элементе, и не передаются в другой. Например, MouseEnter (возникает, когда указатель мыши наводится на элемент) является простым событием.
• Поднимающиеся (“пузырьковые”) события (bubbling event) “путешествуют” вверх по иерархии. Например, MouseDown является поднимающимся событием. Оно возникает в элементе, на котором был произведен щелчок. Затем оно передается от этого элемента к родителю, затем к родителю этого родителя, и так далее. Этот процесс продолжается до тех пор, пока WPF не достигнет вершины дерева элементов.
• Туннельные события (tunneling event) перемещаются вниз по иерархии. Они дают вам возможность предварительно просматривать (и, возможно, останавливать) событие до того, как оно дойдет до подходящего элемента управления. Например, PreviewKeyDown позволяет прервать нажатие клавиши, сначала на уровне окна, а затем в более специфических контейнерах, до тех пор, пока вы не дойдете до элемента, который имел фокус в момент нажатия клавиши. Когда вы регистрируете маршрутизируемое событие с помощью метода EventManager. RegisterEvent(), вы передаете значение из перечисления RoutingStrategy, отражающего необходимое поведение для события. Поскольку события MouseUp и MouseDown являются поднимающимися событиями, теперь вы можете определить, что произойдет в примере с меткой. При щелчке на лице с улыбкой событие MouseDown возникнет в следующем порядке: 1. Image.MouseDown 2. StackPanel.MouseDown 3. Label.MouseDown После того как событие MouseDown возникнет в метке, оно пройдет до следующего элемента управления (которым в этом случае является сетка Grid, разбивающая вмещающее окно), а затем до его родителя (окно). Окно находится на самом верху иерархии и в самом конце в последовательности поднятия события. Здесь у вас есть последний
Book_Pro_WPF-2.indb 165
19.05.2008 18:09:55
166
Глава 6
шанс обработать поднимающееся событие, такое как MouseDown. Если пользователь отпускает кнопку мыши, событие MouseUp возникает в такой же последовательности. На заметку! В главе 9 вы узнаете о том, как создаются приложения со страничной организацией в WPF. В этой ситуации контейнером самого верхнего уровня является не окно, а экземпляр класса Page. Обрабатывать поднимающиеся события можно не только в одном месте. В действительности, события MouseDown и MouseUp можно обрабатывать на любом уровне. Однако, как правило, для этой задачи выбирается наиболее подходящий на данный момент уровень.
Класс RoutedEventArgs Когда вы обрабатываете поднимающееся событие, параметр отправителя содержит ссылку на последнее звено в цепочке. Например, если событие поднимается вверх от изображения до метки, прежде чем вы обработаете его, параметр отправителя будет ссылаться на объект метки. В некоторых случаях вам нужно будет определить, где первоначально произошло событие. Эту информацию, а также другие подробности, можно получить в свойствах класса RoutedEventArgs (которые перечислены в табл. 6.2). Поскольку все классы аргументов событий WPF являются наследниками RoutedEventArgs, эти свойства доступны в любом обработчике события.
Таблица 6.2. Свойства класса RoutedEventArgs Имя
Описание
Source
Показывает, какой объект сгенерировал событие. Если речь идет о событии клавиатуры, то этим объектом будет элемент управления, находившийся в фокусе в момент возникновения события (например, когда была нажата клавиша). Если это событие мыши, то этим объектом будет самый верхний элемент под указателем мыши в момент возникновения события (например, когда был произведен щелчок кнопкой мыши).
OriginalSource
Показывает, какой объект сгенерировал событие. Как правило, OriginalSource является тем же, что и источник. Однако в некоторых случаях OriginalSource спускается глубже по дереву объектов, чтобы дойти до “закулисного” элемента, являющегося частью элемента более высокого уровня. Например, если вы щелкнете кнопкой мыши, чтобы закрыть рамку окна, то получите объект Window в качестве источника события и Border в качестве исходного источника. Это объясняется тем, что Window состоит из отдельных маленьких элементов. Чтобы разобраться с этой моделью более детально (и узнать, как ее можно изменить), обратитесь к главе 15, в которой рассказывается о шаблонах элементов управления.
RoutedEvent
Предлагает объект RoutedEvent для события, сгенерированного вашим обработчиком события (например, статический объект UIElement. MouseUpEvent). Эта информация будет полезной, если вы обрабатываете разные события с помощью одного и того же обработчика.
Handled
Позволяет остановить процесс поднятия или опускания события. Если свойство Handled элемента управления имеет значение true, событие не будет продолжать продвижение, и не будет возникать в любых других элементах. (Как будет показано в разделе “Обработка заблокированного события” далее в главе, существует один способ обхода этого ограничения.)
Book_Pro_WPF-2.indb 166
19.05.2008 18:09:55
Свойства зависимостей и маршрутизируемые события
167
Поднимающиеся события На рис. 6.1 показано простое окно, в котором видно, как поднимается событие. Если вы щелкнете на какой-либо части метки, события будут возникать в порядке, перечисленном в окне списка. На рис. 6.1 показан вид этого окна сразу после того, как пользователь щелкнул на изображении внутри метки. Событие MouseUp проходит пять уровней, останавливаясь на специальной форме BubbledLabelClick. Чтобы получить эту форму, нужно связать изображение и каждый элемент, стоящий над ним в иерархи элементов, с одним и тем же обработчиком события — методом SomethingClicked(). Ниже показано, как это делается в XAML. Рис. 6.1. Щелчок на изображении Image and text label Courtesy of the StackPanel Handle first event Clear List
Метод SomethingClicked() просто проверяет свойства объекта RoutedEventArgs и добавляет сообщение в окно списка:
06_Pro-WPF2.indd 167
20.05.2008 16:19:56
168
Глава 6
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.OriginalSource; lstMessages.Items.Add(message); e.Handled = (bool)chkHandle.IsChecked; }
На заметку! С технической точки зрения событие MouseUp снабжает объект MouseButtonEventArgs дополнительной информацией о состоянии мыши на момент возникновения события. Тем не менее, объект MouseButtonEventArgs является наследником MouseEventArgs, который в свою очередь является наследником RoutedventArgs. В итоге мы можем использовать его при объявлении обработчика события (как показано здесь), если нам не нужна дополнительная информация о мыши. В этом примере есть еще одна деталь. Если вы отметите флажок chkHandle , метод SomethingClicked() присвоит свойству RoutedEventArgs.Handled значение true, в результате чего будет остановлена последовательность поднятия события в момент его возникновения. Поэтому вы увидите в списке только первое событие, как показано на рис. 6.2. На заметку! Здесь нужна еще одна операция приведения, поскольку свойство CheckBox.IsChecked является булевским значением, которое может принимать значение null (bool? вместо bool). Значение null представляет промежуточное состояние флажка, которое означает, что он и не имеет метки, и не является отмеченным. Эта особенность не используется в данном примере, поэтому проблему решит простое приведение. Поскольку метод SomethingClicked() обрабатывает событие MouseUp, которое возникает в объекте Window, у вас будет возможность перехватывать щелчки в окне списка и на пустой поверхности окна. Однако событие MouseUp не Рис. 6.2. Отмечаем событие как обработанное возникает, когда вы щелкаете на кнопке Clear (она удаляет все записи в окне списка). Это связано с тем, что кнопка включает интересный фрагмент кода, который блокирует событие MouseUp и генерирует событие более высокого уровня — Click. Между тем, флагу Handled присваивается значение true, вследствие чего запрещается дальнейшее продвижение события MouseUp. Совет. В отличие от элементов управления Windows Forms, большая часть элементов WPF не имеет события Click. Вместо него они включают более простые события MouseDown и MouseUp. Событие Click зарезервировано для кнопочных элементов управления.
Book_Pro_WPF-2.indb 168
19.05.2008 18:09:56
Свойства зависимостей и маршрутизируемые события
169
Обработка заблокированного события Интересным является тот факт, что вы можете получать события, которые отмечены как обработанные. Вместо того чтобы присоединять обработчик события посредством XAML, вы должны использовать для этой цели рассмотренный ранее метод AddHandler(). Этот метод предлагает перегрузку, которая принимает булевское значение в третьем параметре. Если этим значением будет true, вы получите событие, даже если для него был установлен флаг Handled: cmdClear.AddHander(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp), true);
Это очень хороший вариант решения. Кнопка предназначена для блокирования события MouseUp по очень простой причине: чтобы избежать возникновения конфликта. В конце концов, в Windows принято, что нажать кнопку с помощью клавиатуры можно несколькими способами. Если вы допустите ошибку, обработав событие MouseUp в элементе Button вместо события Click, ваш код отреагирует только на щелчки мышью, а не на эквивалентные действия со стороны клавиатуры.
Прикрепляемые события Пример декоративной метки является довольно простым примером поднятия события, поскольку все элементы поддерживают событие MouseUp. Тем не менее, многие элементы управления обладают собственными специальными событиями. Кнопка является одним из таких примеров — она добавляет событие Click, которое не определено ни в одном базовом классе. Отсюда возникает интересная дилемма. Предположим, что вы помещаете набор кнопок в элемент StackPanel. Вам необходимо обработать все щелчки на кнопке в одном обработчике события. Грубый подход предусматривает присоединение события Click к каждой кнопке в одном и том же обработчике события. Однако событие Click поддерживает поднятие событий, что позволит решить задачу более изящным способом. Вы можете обработать все щелчки на кнопке, обрабатывая событие Click на более высоком уровне (например, на уровне элемента StackPanel). К сожалению, следующий код, который сразу же приходит на ум, работать не будет: Command 1 Command 2 Command 3 ...
Дело в том, что StackPanel не включает событие Click, поэтому этот код вызовет ошибку во время синтаксического анализа XAML. Для решения этой задачи нужно использовать другой синтаксис с применением присоединенных событий в виде ИмяКласса.ИмяСобытия. Ниже показан подходящий вариант: Command 1 Command 2 Command 3 ...
Теперь ваш обработчик события получит щелчок для всех кнопок, содержащихся в элементе StackPanel.
Book_Pro_WPF-2.indb 169
19.05.2008 18:09:56
170
Глава 6
На заметку! Событие Click определено в классе ButtonBase и наследуется от класса Button. Если вы присоединяете обработчик события к ButtonBase.Click, этот обработчик события будет использоваться при щелчке на любом элементе управления, который является потомком ButtonBase (включая классы Button, RadioButton и CheckBox). Если вы присоединяете обработчик события к Button.Click, то он будет использоваться только для объектов Button. Вы можете подключить прикрепляемое событие в коде, однако вместо операции += вам придется использовать метод UIElement.AddHandler(). Ниже показан такой пример (здесь предполагается, что элемент StackPanel имеет имя pnlButtons): pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));
В обработчике события DoSomething() вы можете несколькими способами определить, какая кнопка сгенерировала событие. Вы можете сравнить ее текст (этот способ может привести к проблемам с локализацией) или ее имя (этот способ тоже неудобный, так как вы не сможете сравнивать ошибочно введенные имена). Лучше всего проверить, было ли задано свойство Name каждой кнопки с помощью XAML, чтобы вы могли иметь доступ к соответствующему объекту посредством поля в вашем классе окна и сравнить эту ссылку с отправителем события. Ниже показан пример: private void DoSomething(object sender, RoutedEventArgs e) { if (sender == cmd1) { ... } else if (sender == cmd2) { ... } else if (sender == cmd3) { ... } }
Существует еще один вариант — вместе с кнопкой отправить порцию информации, которую можно использовать в коде. Например, вы можете задать свойство Tag каждой кнопки, как показано ниже: Command 1 Command 2 Command 3 ...
После этого вы сможете обращаться к свойству Tag в вашем коде: private void DoSomething(object sender, RoutedEventArgs e) { object tag = ((FrameworkElement)sender).Tag; MessageBox.Show((string)tag); }
Туннельные события Туннельные события работают точно так же, как и поднимающиеся события, но в обратном направлении. Например, если бы событие MouseUp было туннельным (на самом деле оно таковым не является), то при щелчке на метке событие MouseUp возникло бы сначала в окне, затем в элементе Grid, затем в StackPanel и так далее до тех пор, пока не будет достигнут источник, которым является изображение в метке. Туннельные события легко распознаются, поскольку они имеют приставку Preview. Более того, WPF обычно определяет поднимающиеся и туннельные события парами. Это означает, что если вы найдете поднимающееся событие MouseUp, то, скорее всего,
Book_Pro_WPF-2.indb 170
19.05.2008 18:09:56
Свойства зависимостей и маршрутизируемые события
171
вы найдете также туннельное событие PreviewMouseUp. Туннельное событие всегда возникает перед поднимающимся событием, как показано на рис. 6.3.
Здесь возникло первым
Корневой элемент (окно — Window)
Сюда поднято последним
Туннельное событие PreviewMouseUp Промежуточный элемент
Промежуточный элемент
Поднимающееся событие MouseUp Источник события
Рис. 6.3. Туннельные и поднимающиеся события Для разнообразия скажем, что если вы пометите туннельное событие как обработанное, событие поднятия не возникнет. Это связано с тем, что два события совместно используют один и тот же экземпляр класса RoutedEventArgs. Туннельные события будут полезны, если вам понадобится выполнить некоторую предварительную обработку, связанную с некоторыми нажатиями клавиш, или отфильтровать некоторые события мыши. На рис. 6.4 показаны результаты проверки туннелирования на примере события PreviewKeyDown. Когда вы нажимаете клавишу, находясь в текстовом поле, событие возникает сначала в этом поле, а затем спускается вниз по иерархии. А если на каком-то этапе вы пометите событие PreviewKeyDown как обработанное, то поднимающееся событие KeyDown не возникнет.
Определение стратегии маршрутизации события Понятно, что разные стратегии маршрутизации оказывают влияние на то, как вы будете работать с событием. А как определить, какой тип маршрутизации будет использовать данное событие? Туннельные события являются простыми. В соответствии с соглашениями, принятыми в .NET, туннельное событие всегда начинается со слова Preview (как, например, PreviewKeyDown). Однако похожего механизма различения поднимающихся событий от прямых событий не существует. Разработчикам, применяющим WPF, лучше всего искать событие в ссылке на библиотеки классов в справочной системе .NET Framework SDK (в узле .NET Development.NET Framework SDK. NET Framework 3.0 DevelopmentClass Library). Вы увидите информацию Routed Event Information, указывающую на статическое поле события, тип маршрутизации и сигнатуру события. Эту же информацию можно получить программным способом, проверяя статическое поле для события. Например, свойство ButtonBase.ClickEvent.RoutingStrategy содержит перечислимое значение, которое показывает, какой тип маршрутизации использует событие Click.
Book_Pro_WPF-2.indb 171
19.05.2008 18:09:56
172
Глава 6
Рис. 6.4. Туннелирование нажатия клавиши
Совет. Будьте внимательны, помечая туннельное событие как обработанное. В зависимости от способа написания элемента управления, это может привести к тому, что элемент управления не сможет обрабатывать свое собственное событие (связанное с ним поднимающееся событие), чтобы решить некоторую задачу или обновить свое состояние.
События WPF Теперь, когда вы знаете о том, как работают события WPF, пора приступить к рассмотрению самых разнообразных событий, на которые вы можете реагировать в своем коде. Несмотря на то что каждый элемент имеет широчайший спектр событий, наиболее важные события обычно делятся на четыре описанных ниже категории.
• События времени существования. Эти события возникают, когда элемент инициализируется, загружается или выгружается.
• События мыши. Эти события являются результатом действий мыши. • События клавиатуры. Эти события являются результатом действий клавиатуры (например, нажатие клавиши).
• События пера. Эти события являются результатом использования пера (stylus), которое заменяет мышь в наладонных ПК. Вместе события мыши, клавиатуры и пера известны как события ввода.
Book_Pro_WPF-2.indb 172
19.05.2008 18:09:56
Свойства зависимостей и маршрутизируемые события
173
События времени существования Все элементы генерируют события, когда они впервые создаются и когда освобождаются. Вы можете использовать эти события для инициализации окна. События времени существования перечислены в табл. 6.3; все они определены в классе FrameworkElement.
Таблица 6.3. События времени существования всех элементов Имя
Описание
Initialized
Возникает после создания экземпляра элемента и определения его свойств в соответствии с правилами разметки XAML. В этот момент элемент инициализируется, хотя остальные части окна могут еще быть не инициализированными. Также на этом этапе еще не применены стили и привязка данных. В этот момент свойство IsInitialized имеет значение true. Данное событие является обычным событием .NET, а не маршрутизируемым.
Loaded
Возникает после того, как все окно было инициализировано и были применены стили и привязка данных. Это последний этап, за которым происходит визуализация элемента. В этот момент свойство IsLoaded имеет значение true.
Unloaded
Возникает, когда элемент был освобожден. Это может быть вызвано или закрытием вмещающего окна, или удалением из окна специфического элемента.
Чтобы вы могли понять, как связаны между собой события Initialized и Loaded, давайте рассмотрим процесс визуализации. FrameworkElement реализует интерфейс ISupportInitialize, который предлагает два метода управления процессом инициализации. Первый из них, метод BeginInit(), вызывается сразу после создания экземпляра элемента. После того как будет вызван метод BeginInit(), анализатор синтаксиса XAML установит все свойства элемента (и добавит любое содержимое). Второй метод, EndInit(), вызывается после завершения процесса инициализации, когда возникает событие Iniitialized. На заметку! Это небольшое упрощение. Анализатор синтаксиса XAML самостоятельно заботится о вызове методов BeginInit() и EndInit(), как и должно быть. Однако если вы создадите элемент вручную и добавите его в окно, то вы вряд ли будете использовать этот интерфейс. В этом случае элемент сгенерирует событие Initialized сразу после того, как вы добавите его в окно, и как раз перед возникновением события Loaded. Когда вы создаете окно, каждая ветвь элементов инициализируется снизу вверх. Это означает, что глубоко вложенные элементы инициализируются до того, как будут инициализированы их контейнеры. Когда возникает событие Initialized, это означает, что дерево элементов, от текущего элемента и до его основания, является полностью инициализированным. Однако элемент, который содержит ваш элемент, возможно, не инициализирован, и вы не можете предположить, что любая другая часть окна является инициализированной. После того как каждый элемент будет инициализирован, он будет помещен в свой контейнер, получит стиль и при необходимости будет привязан к источнику данных. После того как возникнет событие Initialized окна, наступает следующий этап. Как только процесс инициализации будет завершен, возникает событие Loaded. Оно следует в порядке, обратном порядку события Initialized — другими словами, сначала событие Loaded генерирует вмещающее окно, после чего его генерируют остальные вложенные элементы. Когда событие Loaded возникнет во всех элементах, окно станет видимым и будут визуализированы элементы.
Book_Pro_WPF-2.indb 173
19.05.2008 18:09:56
174
Глава 6
События времени существования, перечисленные в табл. 6.3, — еще не вся история. Вмещающее окно имеет собственные события времени существования. Они перечислены в табл. 6.4.
Таблица 6.4. События времени существования класса Window Имя
Описание
SourceInitialized
Возникает при запросе свойства HwndSource (но перед тем, как окно станет видимым). HwndSource — это дескриптор окна, который может понадобиться для вызова устаревших функций интерфейса Win32 API.
ContentRendered
Возникает сразу после первой визуализации окна. Этот момент не подходит для выполнения каких-либо изменений, которые могут повлиять на визуальное отображение окна, иначе вам придется выполнять вторую операцию по визуализации. (Лучше использовать событие Loaded.) Однако событие ContentRendered не показывает, что ваше окно является полностью видимым и готово к вводу.
Activated
Возникает, когда пользователь переключается на это окно (например, из другого окна в вашем приложении или вообще из другого приложения). Это событие возникает также во время первой загрузки окна. В концептуальном плане событие Activated является “оконным” эквивалентом события GotFocus элемента управления.
Deactivated
Возникает, когда пользователь уходит (т.е. переключается) из этого окна (например, переходит в другое окно в вашем приложении или вообще в другое приложение). Это событие возникает также, когда пользователь закрывает окно, после события Closing, но перед событием Closed. В концептуальном плане, событие Deactivated является “оконным” эквивалентом события LostFocus элемента управления.
Closing
Возникает при закрытии окна, что может быть вызвано или действием пользователя, или программой с помощью методов Window. Close() или Application.Shutdown(). Событие Closing дает вам возможность отменить операцию и оставить окно открытым, если вы присвоите свойству CancelEventArgs.Cancel значение true. Однако вы не получите событие Closing, если ваше приложение завершит работу вследствие того, что пользователь выключил компьютер или вышел из системы — для этого нужно обрабатывать событие Application.SessionEnding, которое описано в главе 3.
Closed
Возникает после закрытия окна. Это тот момент, когда объекты элемента по-прежнему остаются доступными, а событие Unloaded еще не возникло. В этот момент вы можете произвести очистку, записать параметры в место постоянного хранения (например, в конфигурационный файл или в системный реестр Windows) и т.д.
Если вам нужно выполнить первичную инициализацию ваших элементов управления, то наилучшим моментом для этого является событие Loaded. Как правило, вы можете выполнить все действия, связанные с инициализацией, в одном месте, которым обычно является обработчик события Window.Loaded. Совет. Для выполнения инициализации можно использовать также конструктор окна (просто добавьте необходимый код сразу после вызова метода IniitializeComponent()). Однако лучше всего использовать событие Loaded. Это связано с тем, что если в конструкторе Window возникнет исключение, оно не будет принято до тех пор, пока анализатор синтаксиса XAML не проверит страницу. Как результат, ваше исключение будет упаковано в бесполезный объект XamlParseException (с исходным исключением в свойстве InnerException).
Book_Pro_WPF-2.indb 174
19.05.2008 18:09:57
175
Свойства зависимостей и маршрутизируемые события
События ввода Все события ввода — события, которые возникают вследствие действий мыши, клавиатуры или пера — передают дополнительную информацию в специальном классе аргументов событий. По сути, все эти классы совместно используют одного и того же предка — класс InputEventArgs. На рис. 6.5 показана иерархия наследования.
EventArgs
RoutedEventArgs
InputEventArgs
KeyboardEventArgs
MouseEventArgs
StylusEventArgs
KeyboardFocusChangedEventArgs
MouseButtonEventArgs
StylusButtonEventArgs
KeyEventArgs
MouseWheelEventArgs
StylusDownEventArgs
QueryCursorEventArgs
StylusSystemGestureEventArgs
Рис. 6.5. Классы EventArgs для событий ввода Класс InputEventArgs добавляет только два свойства: Timestamp и Device. Свойство Timestamp может иметь целочисленное значение, показывающее в миллисекундах, когда возникло событие. (Действительное время, представленное этим значением, особой роли не играет, хотя вы можете сравнить разные временные метки, чтобы узнать, какое событие возникло первым. Большие временные метки свидетельствуют о позднем событии.) Свойство Device возвращает объект, который предлагает более подробную информацию об устройстве, сгенерировавшем событие, которым может быть мышь, клавиатура или перо. Каждый из этих трех вариантов представлен отдельным классом, и все они являются наследниками абстрактного класса System.Windows.Input.InputDevice. В оставшейся части этой главы мы подробно рассмотрим вопросы обработки событий мыши и клавиатуры в WPF-приложении.
Ввод с использованием клавиатуры Когда пользователь нажимает клавишу, возникает целая серия событий. В табл. 6.5 перечислены события в порядке их возникновения.
Book_Pro_WPF-2.indb 175
19.05.2008 18:09:57
176
Глава 6
Табл. 6.5. События клавиатуры для всех элементов (по порядку) Имя
Тип маршрутизации
Описание
PreviewKeyDown
Туннелирование
Возникает при нажатии клавиши.
KeyDown
Поднятие
Возникает при нажатии клавиши.
PreviewTextInput
Туннелирование
Возникает, когда нажатие клавиши завершено, и элемент получает текстовый ввод. Это событие не возникает для тех клавиш, которые в результате не приводят к вводу текста (например, оно не возникает при нажатии клавиш , , , клавиш управления курсором, функциональных клавиш и т.д.)
TextInput
Поднятие
Возникает, когда нажатие клавиши завершено, и элемент получает текстовый ввод. Это событие не возникает для тех клавиш, которые в результате не приводят к вводу текста.
PreviewKeyUp
Туннелирование
Возникает при отпускании клавиши.
KeyUp
Поднятие
Возникает при отпускании клавиши.
Обработка событий, поступающих с клавиатуры, никогда не была столь легкой, как это может показаться. Некоторые элементы управления могут блокировать часть этих событий, поэтому они могут выполнять свою собственную обработку клавиатуры. Наиболее ярким примером является элемент TextBox, который блокирует событие TextInput. Элемент TextBox блокирует также событие KeyDown для некоторых нажатий клавиш, таких как клавиши управления курсором. В случаях, подобных этим, вы по-прежнему можете использовать туннельные события (PreviewTextInput и PreviewKeyDown). Элемент управления TextBox добавляет одно новое событие — TextChanged. Это событие возникает сразу после того, как нажатие клавиши приводит к изменению текста в текстовом поле. В этот момент новый текст уже является видимым в текстовом поле, потому отменить нежелательное нажатие клавиши уже будет поздно.
Обработка нажатия клавиши
Рис. 6.6. Наблюдение за клавиатурой
Book_Pro_WPF-2.indb 176
Чтобы понять, как работают и используются события клавиатуры, лучше всего рассмотреть пример. На рис. 6.6 показана программа, которая следит за всеми возможными нажатиями клавиш, когда в фокусе находится текстовое поле, и реагирует на них, если они возникают. На рис. 6.6 показан результат ввода заглавной буквы S в текстовом поле. Этот пример демонстрирует один важный момент. События PreviewKeyDown и KeyDown возникают всякий раз, когда происходит нажатие клавиши. Однако событие TextInput возникает только тогда, когда в элементе был “введен” символ. Это действие на самом деле может включать нажатие многих клавиш.
19.05.2008 18:09:57
Свойства зависимостей и маршрутизируемые события
177
В примере, показанном на рис. 6.5, нужно нажать две клавиши, чтобы получить заглавную букву S. Сначала необходимо нажать клавишу , а затем клавишу . Как результат, вы увидите по два события KeyDown и KeyUp, и только одно событие TextInput. Каждое из событий PreviewKeyDown, KeyDown, PreviewKey и KeyUp дает одну и ту же информацию в объекте KeyEventArgs. Самой важной деталью является свойство Key, которое возвращает значение из перечисления System.Windows.Input.Key, показывающее клавишу, которая была нажата или отпущена. Ниже представлен код обработчика события, который работает с событиями клавиш из примера, показанного на рис. 6.6. private void KeyEvent(object sender, KeyEventArgs e) { string message = "Event: " + e.RoutedEvent + " " + " Key: " + e.Key; lstMessages.Items.Add(message); }
Значение Key не учитывает состояние любой другой клавиши. Например, нет разницы в том, была ли нажата клавиша в тот момент, когда вы нажимали ; в любом случае, вы получите одно и то же значение Key (Key.S). Здесь присутствует одна сложность. В зависимости от того, какие параметры заданы для вашей клавиатуры Windows, удержание клавиши в нажатом состоянии приводит к тому, что нажатие как действие повторяется через короткий промежуток времени. Например, удержание нажатой клавиши приведет к вводу в текстовом поле целой серии символов S. Точно так же, удержание нажатой клавиши приводит к повтору действия нажатия и к возникновению серии событий KeyDown. В реальном примере, в котором вы нажимаете комбинацию , ваше текстовое поле сгенерирует серию событий KeyDown для клавиши , за ними — событие KeyDown для клавиши , событие TextInput (или событие TextChanged в случае текстового поля), а затем событие KeyUp для клавиш и . Если вы хотите проигнорировать повторы нажатия клавиши , вы можете проверить, является ли нажатие результатом удерживания клавиши в нажатом состоянии, с помощью свойства KeyEventArgs.IsRepeat, как показано ниже: if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;
Совет. События PreviewKeyDown, KeyDown, PreviewKey и KeyUp лучше всего подходят для написания низкоуровневого кода обработки клавиатуры (что вам вряд ли понадобится за пределами специального элемента управления) и обработки нажатий клавиш (например, функциональных клавиш). За событием KeyDown следует событие PreviewTextInput. (Событие TextInput не возникает, поскольку элемент TextBox блокирует его.) В этот момент текст еще не отображается в элементе управления. Событие TextInput предлагает код объекта TextCompositionEventArgs. Этот объект включает свойство Text, которое дает вам обработанный текст, подготовленный к получению элементом управления. Ниже представлен код, добавляющий текст в список, показанный на рис. 6.6. private void TextInput(object sender, TextCompositionEventArgs e) { string message = "Event: " + e.RoutedEvent + " " + " Text: " + e.Text; lstMessages.Items.Add(message); }
Book_Pro_WPF-2.indb 177
19.05.2008 18:09:57
178
Глава 6
В идеале вы могли бы использовать PreviewTextInput для выполнения проверки в элементе управления, таком как TextBox. Например, если вы создаете текстовое поле для ввода только чисел, вы можете проверить, не была ли введена при текущем нажатии клавиши буква; если буква не была введена, вы устанавливаете флаг Handled. К сожалению, событие PreviewTextInput не генерируется для некоторых клавиш, которые вам, может быть, придется обрабатывать. Например, если вы нажимаете клавишу пробела в текстовом поле, вы вообще пропускаете событие PreviewTextInput. Это означает, что вам нужно будет обработать также событие PreviewKeyDown. К сожалению, трудно реализовать надежную логику проверки данных в обработчике события PreviewKeyDown, поскольку все, чем вы можете оперировать — это значение Key, которое является слишком малой порцией информации. Например, перечисление Key проводит различие между цифровой клавиатурой (блок клавиш на клавиатуре, предназначенный для ввода только цифр) и обычной клавиатурой. Это означает, что в зависимости от того, как вы нажмете клавишу с цифрой 9, вы можете получить или значение Key.D9, или значение Key.NumPad9. Проверка всех дозволенных значений клавиши будет очень утомительной, если не сказать больше. Но выход есть — нужно использовать KeyConverter, чтобы преобразовать значение Key в более полезную строку. Например, использование функции KeyConverter. ConvertStringToString() в обоих значениях Key.D9 и Key.NumPad9 вернет результат "9" в форме строки. Если вы просто используете преобразование Key.ToString(), вы получите менее полезное имя перечисления (либо "D9", либо "NumPad9"): KeyConverter converter = new KeyConverter(); string key = converter.ConvertToString(e.Key);
Однако использовать KeyConverter тоже не очень неудобно, поскольку вы получите более объемный текст (например, "Backspace") для тех нажатий клавиш, которые не приводят к вводу текста. Наиболее подходящим вариантом является обработка события PreviewTextInput (при нем происходит большая часть проверки) и использование события PreviewKeyDown для тех нажатий клавиш, которые не генерируют событие PreviewTextInput в текстовом поле (например, клавиша пробела). Ниже показано простое решение. private void pnl_PreviewTextInput(object sender, TextCompositionEventArgs e) { short val; if (!Int16.TryParse(e.Text, out val)) { // Запрет нажатий нечисловых клавиш. e.Handled = true; } } private void pnl_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Space) { // Запрет клавиши пробела, которая не генерирует событие PreviewTextInput. e.Handled = true; } }
Вы можете присоединить эти обработчики событий к одному текстовому полю или подключить их к контейнеру (например, к StackPanel, который содержит несколько текстовых полей для ввода чисел) для получения большей эффективности.
Book_Pro_WPF-2.indb 178
19.05.2008 18:09:57
Свойства зависимостей и маршрутизируемые события
179
На заметку! Такое поведение при обработке может показаться чрезвычайно неудобным (что и есть на самом деле). Одной из причин, по которой TextBox не может обеспечить лучшую обработку клавиатуры, является то, что WPF фокусируется на привязке данных — возможности, которая позволяет подключать элементы управления, такие как TextBox, к специальным объектам. Когда вы используете этот подход, проверка обычно выполняется ограничивающим объектом, ошибки подаются исключением, а в случае неправильных данных генерируется сообщение об ошибке, которое появляется где-то в пользовательском интерфейсе. К сожалению, пока еще нет способа комбинировать высокоуровневую возможность привязки данных с низкоуровневой обработкой клавиатуры, что может оказаться необходимым для того, чтобы вообще избавить пользователя от случайного ввода неверных данных.
Фокус В мире Windows пользователь может работать одновременно с одним элементом управления. Элемент управления, который в данный момент времени получает нажатия клавиши пользователем, находится в фокусе. Иногда такой элемент прорисовывается несколько иначе. Например, кнопка WPF приобретает синий оттенок, чтобы свидетельствует о том, что она находится в фокусе. Чтобы элемент управления мог получать фокус, его свойству Focusable нужно присвоить значение true. По умолчанию оно является таковым для всех элементов управления. Довольно интересно то, что свойство Focusable определяется как часть класса UIElement, а это означает, что остальные элементы, не являющиеся элементами управления, тоже могут получать фокус. Обычно в классах, не определяющих элементы управления, свойство Focusable по умолчанию имеет значение false. Тем не менее, вы можете присвоить ему true. Попробуйте сделать это на примере контейнера StackPanel — когда он будет получать фокус, по краям панели будет появляться пунктирная рамка. Чтобы переместить фокус с одного элемента на другой, пользователь может щелкнуть кнопкой мыши или воспользоваться клавишей и клавишами управления курсором. В предыдущих средах разработки программисты прилагали много усилий для того, чтобы клавиша передавала фокус “по законам логики” (обычно слева направо, а затем вниз окна), и чтобы при первом отображении окна фокус передавался необходимому элементу управления. В WPF такая дополнительная работа требуется очень редко, поскольку WPF использует иерархическую компоновку элементов для реализации последовательности перехода с помощью клавиши табуляции. По сути, когда вы нажимаете клавишу , вы переходите к первому потомку в текущем элементе или, если текущий элемент не имеет потомка, к следующему потомку, находящемуся на том же уровне. Например, если вы осуществляете переход с помощью клавиши табуляции в окне, в котором имеются два контейнера StackPanel, вы пройдете через все элементы управления в первом контейнере StackPanel, а затем через все элементы управления во втором контейнере. Если вы хотите управлять последовательностью перехода с помощью клавиши табуляции, вы можете задать свойство TabIndex каждого элемента управления, чтобы определить его место в числовом порядке. Элемент управления, свойство TabIndex которого имеет значение 0, получает фокус первым, затем фокус получает элемент с большим значением этого свойства (например, 1, 2, 3 и т.д.). Если несколько элементов имеют одинаковые значения свойства TabIndex, WPF выполняет автоматическую последовательность передачи фокуса, в соответствии с которой фокус получает ближайший элемент в последовательности.
Book_Pro_WPF-2.indb 179
19.05.2008 18:09:57
180
Глава 6
Совет. По умолчанию свойство TabIndex во всех элементах управления имеет значение 1. Это означает, что вы можете назначить определенный элемент в качестве стартовой точки в окне, присвоив его свойству TabIndex значение 0, и применяя автоматическую навигацию, чтобы пользователь мог переходить к остальным элементам в окне от этой стартовой точки, в соответствии с тем порядком, который вы выберете для своих элементов. Свойство TabIndex определяется в классе Control, вместе со свойством IsTabStop. Свойству IsTabStop можно присвоить значение false, чтобы исключить элемент управления из последовательности перехода с помощью клавиши табуляции. Различие между IsTabStop и Focusable заключается в том, что элемент управления, свойство IsTabStop которого имеет значение false, может получить фокус другим путем — либо программно (если в вашем коде производится вызов метода Focus()), либо по щелчку кнопкой мыши. Элементы управления, являющиеся невидимыми или заблокированными (изображены серым цветом) обычно не включаются в последовательность перехода с помощью клавиши табуляции и не активизируются, независимо от настройки свойств TabIndex, IsTabStop и Focusable. Чтобы скрыть или заблокировать элемент управления, используются свойства Visibility и IsEnabled, соответственно.
Получение состояния клавиши Когда происходит нажатие клавиши, вам необходимо знать больше, чем просто то, какая именно клавиша была нажата. Кроме этого, нужно выяснить, какие еще клавиши были нажаты в это же время. Это означает, что вам придется изучить состояние остальных клавиш, особенно модификаторов вроде , и . События клавиш (PreviewKeyDown, KeyDown, PreviewKeyUp и KeyUp) способствуют получению этой информации. Во-первых, объект KeyEventArgs включает свойство KeyState, которое отражает свойство клавиши, сгенерировавшей событие. Еще есть свойство KeyboardDevice, которое предлагает ту же информацию для любого ключа на клавиатуре. Неудивительно, что свойство KeyboardDevice предлагает экземпляр класса KeyboardDevice. Его свойства включают информацию о том, какой элемент в данный момент имеет фокус (FocusedElement) и какие клавиши-модификаторы были нажаты в момент возникновения события (Modifiers). К клавишам-модификаторам относятся , и ; вы можете проверить их состояние с помощью следующего кода: if ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { lblInfo.Text = "You held the Control key."; }
KeyboardDevice тоже предлагает несколько удобных методов, которые перечислены в табл. 6.6. Каждому из них вы передаете значение из перечисления Key. Когда вы используете свойство KeyEventArgs.KeyboardDevice, ваш код получает состояние виртуальной клавиши. Это означает, что он получает состояние клавиатуры в момент возникновения события. Это может означать то же, что и текущее состояние клавиатуры. Например, представим, что произойдет, если пользователь вводит данные быстрее, нежели работает код. Каждый раз, когда будет возникать ваше событие KeyPress, вы будете иметь доступ к нажатию клавиши, сгенерировавшей событие, а не к символам, соответствующим нажатию. Как правило, именно такое поведение и оказывается необходимым.
Book_Pro_WPF-2.indb 180
19.05.2008 18:09:58
Свойства зависимостей и маршрутизируемые события
181
Таблица 6.6. Методы KeyboardDevice Имя
Описание
IsKeyDown()
Сообщает о том, была ли нажата данная клавиша в момент возникновения события.
IsKeyUp()
Сообщает о том, была ли отпущена данная клавиша в момент возникновения события.
IsKeyToggled()
Сообщает о том, находилась ли данная клавиша во “включенном” состоянии в момент возникновения события. Это относится лишь к тем клавишам, которые могут включаться и выключаться: , и .
GetKeyStates()
Возвращает одно или несколько значений из перечисления KeyStates, которое сообщает о том, является ли данная клавиша нажатой, отпущенной, включена или выключена. По сути, это то же самое, что и вызов методов IsKeyDown() и IsKeyUp() с передачей им той же клавиши.
Однако вы не ограничены получением информации о клавише в событиях клавиатуры. Вы можете также получать состояние клавиатуры в любой момент времени. Для этой цели необходимо воспользоваться классом Keyboard, который очень похож на класс KeyboardDevice, за исключением того, что он создан из статических членов. Ниже показан пример, в котором класс Keyboard используется для проверки текущего состояния левой клавиши : if (Keyboard.IsKeyDown(Key.LeftShift)) { lblInfo.Text = "The left Shift is held down."; }
На заметку! Класс Keyboard имеет методы, которые позволят присоединять обработчики событий клавиатуры, действующие в рамках всего приложения: AddKeyDownHandler() и AddKeyUpHandler(). Однако применять эти методы не рекомендуется. Гораздо лучше реализовать систему команд WPF, о которой будет рассказано в главе 10.
Ввод с использованием мыши События мыши решают несколько связанных задач. Самые главные события мыши позволяют определять действия в ответ на перемещение указателя мыши над элементом. Этими событиями являются MouseEnter (возникает, когда указатель мыши перемещается над элементом) и MouseLeave (происходит, когда указатель мыши покидает пределы элемента). Оба эти события являются прямыми событиями (direct events), а это означает, что они не используют туннелирование или поднятие. Вместо этого они генерируются в одном элементе и продолжают свое существование только в нем. Такое поведение является оправданным и объясняется способами вложения элементов управления в окно WPF. Например, если имеется панель StackPanel, в которой содержится кнопка, и вы наводите указатель мыши на эту кнопку, событие MouseEnter возникнет первым в элементе StackPanel (как только вы войдете в пределы панели), а затем в кнопке (как только вы наведете на нее указатель). Когда указатель мыши покинет пределы StackPanel, возникнет событие MouseLeave сначала в кнопке, а затем в StackPanel. Вы можете также реагировать на два события, которые возникают при перемещении указателя мыши: PreviewMouseMove (туннельное событие) и MouseMove (событие
Book_Pro_WPF-2.indb 181
19.05.2008 18:09:58
182
Глава 6
поднятия). Эти события предлагают вам код объекта MouseEventArgs. Этот объект включает свойства, которые могут сообщать о состоянии кнопок мыши в момент возникновения события, и метод GetPosition(), который сообщает координаты указателя мыши относительно выбираемого вами элемента. Ниже представлен пример, который отображает местонахождение указателя мыши относительно формы в независимых от устройства единицах. private void MouseMoved(object sender, MouseEventArgs e) { Point pt = e.GetPosition(this); lblInfo.Text = String.Format("You are at ({0},{1}) in window coordinates", pt.X, pt.Y); }
Рис. 6.7. Наблюдение за мышью
В данном случае координаты определяются, начиная с левого верхнего угла клиентской области (под строкой заголовка). На рис. 6.7 виден результат выполнения этого кода. Вы увидите, что координаты мыши в этом примере не представлены целыми числами. Это объясняется тем, что данный снимок экрана был произведен в системе с разрешением 120 dpi, а не со стандартным разрешением в 96 dpi. Как было сказано в главе 1, WPF автоматически масштабирует свои единицы для компенсации, используя большее количество пикселей. Поскольку размер экранного пикселя больше не совпадает с размером в системе единиц WPF, физическое положение указателя мыши можно преобразовать в дробное число единиц WPF, что и было здесь продемонстрировано.
Совет. Класс UIElement содержит два полезных свойства, которые могут помочь в определении местоположения указателя мыши. С помощью свойства IsMouseOver можно определить, находится ли указатель мыши над элементом или одним из его потомков, а благодаря свойству IsMouseDirectlyOver можно выяснить, располагается ли указатель мыши только над элементом, а не над его потомком. Как правило, в своем коде вы не будете считывать значения этих свойств, и не будете оперировать ними, а будете использовать их для создания триггеров стилей, которые автоматически изменяют элементы по мере перемещения указателя мыши над ними. Эта технология будет рассматриваться в главе 12.
Щелчки кнопками мыши Щелчки кнопками мыши подобны нажатиям клавиш на клавиатуре. Разница лишь в том, что события различаются для левой и правой кнопок. В табл. 6.7 перечислены события в порядке их возникновения. Помимо перечисленных, есть еще два события, которые реагируют на вращение колесика мыши: PreviewMouseWheel и MouseWheel. Все события кнопок мыши имеют дело с объектом MouseButtonEventArgs. Класс MouseButtonEventArgs происходит от класса MouseEventArgs (а это означает, что он включает ту же информацию о координатах и состоянии кнопки) и добавляет несколько новых членов.
Book_Pro_WPF-2.indb 182
19.05.2008 18:09:58
183
Свойства зависимостей и маршрутизируемые события Таблица 6.7. События щелчков кнопками мыши для всех элементов (в порядке их возникновения) Имя
Тип маршрутизации
Описание
PreviewMouseLeftButtonDown и PreviewMouseRightButtonDown MouseLeftButtonDown
Туннелирование
Возникает при нажатии кнопки мыши.
Поднятие
Возникает при нажатии кнопки мыши.
PreviewMouseLeftButtonUp и PreviewMouseRightButtonUp
Туннелирование
Возникает при отпускании кнопки мыши.
MouseLeftButtonUp и MouseRightButtonUp
Поднятие
Возникает при отпускании кнопки мыши.
Менее важными свойствами являются MouseButton (сообщает о том, какая кнопка сгенерировала событие) и ButtonState (сообщает о том, в каком состоянии находилась кнопка в момент возникновения события: была нажата или отпущена). Более интересным свойством является ClickCount, которое сообщает о том, сколько раз был произведен щелчок кнопкой, что позволит различать одиночные щелчки (когда ClickCount будет иметь значение 1) и двойные щелчки (когда ClickCount будет иметь значение 2). Совет. Как правило, Windows-приложения реагируют, когда кнопка мыши отпускается после щелчка (событие “up”, а не “down”). Некоторые элементы добавляют высокоуровневые события мыши. Например, класс
Control добавляет события PreviewMouseDoubleClick и MouseDoubleClick, которые замещают событие MouseLeftButtonUp. Точно так же, класс Button вызывает событие Click, которое могут сгенерировать клавиатура или мышь. На заметку! Как и события, возникающие при нажатии клавиши, события мыши предлагают информацию о том, в каком месте находился указатель мыши, и какой кнопкой был произведен щелчок в момент возникновения события. Для получения информации о текущей позиции указателя мыши и состоянии ее кнопок вы можете воспользоваться статическими членами класса Mouse, которые ничем не отличаются от статических членов класса MouseButtonEventArgs.
Захват мыши Обычно каждый раз, когда элемент получает событие “down” кнопки мыши, через короткий промежуток времени он получает соответствующее событие “up” кнопки мыши. Однако так бывает не всегда. Например, если вы щелкаете на элементе, удерживаете нажатой кнопку мыши, а затем перемещаете указатель мыши за пределы элемента, то элемент не получит событие отпускания кнопки мыши. В некоторых ситуациях вам может понадобиться уведомление о событиях отпускания кнопки мыши, даже если они возникают после того, как указатель мыши покинул пределы элемента. Чтобы получать уведомления, вам нужно захватить мышь, вызывая для этого метод Mouse.Capture() и передавая ему соответствующий элемент. С этого момента вы будете получать события о нажатии и отпускании кнопок мыши до тех пор, пока снова не вызовете метод Mouse.Capture() и не передадите пустую (null) ссылку. Остальные элементы не получат события мыши до тех пор, пока мышь будет оставаться захваченной. Это означает, что пользователь не сможет щелкать кнопками мыши гделибо в окне, щелкать внутри текстовых полей и т.д. Захват мыши иногда используется для реализации функций перетаскивания и изменения размеров элементов. В главе 8 будет приведен пример специального окна, допускающего изменение размеров.
Book_Pro_WPF-2.indb 183
19.05.2008 18:09:58
184
Глава 6
Совет. При вызове метода Mouse.Capture() вы можете передавать необязательное значение в качестве второго параметра. Обычно при вызове метода Mouse.Capture() используется CaptureMode.Element, а это означает, что ваш элемент будет всегда получать события мыши. Однако вы можете применить CaptureMode.SubTree для того, чтобы события мыши могли доходить до элемента, на котором был произведен щелчок кнопкой мыши, если этот элемент является потомком элемента, выполняющего захват. В этом есть смысл, если вы уже используете поднятие или туннелирование события для наблюдения за событиями мыши в дочерних элементах. В некоторых случаях вы можете утратить захват мыши не по своей воле. Например, Windows может освободить мышь, если ей потребуется отобразить системное диалоговое окно. Это может случиться также в ситуации, если вы не освободите мышь после того, как возникнет событие, а пользователь переместит указатель, чтобы щелкнуть в окне в другом приложении. В любом случае вы сможете реагировать на потерю захвата мыши, обрабатывая событие LostMouseCapture для данного элемента. Пока мышь будет захвачена элементом, вы не сможете взаимодействовать с другими элементами. (Например, вы не сможете щелкнуть на другом элементе в вашем окне.) Захват мыши обычно используется в краткосрочных операциях, таких как перетаскивание.
Перетаскивание Операции перетаскивания (способ изъятия информации из одного места в окне и переноса ее в другое место) сегодня не являются столь распространенными, как раньше. Программисты перешли на другие методы копирования информации, которые не требуют удержания нажатой кнопки мыши (технология, овладеть которой многим пользователям удается с трудом). Программы, которые поддерживают операцию перетаскивания, часто предлагают ее как быструю комбинацию для опытных пользователей, а не как стандартный способ работы. Операции перетаскивания в WPF не претерпели существенных изменений. Если вы использовали их в приложениях Windows Forms, то поймете, что программный интерфейс в WPF остался практически неизмененным. Ключевым отличием является то, что методы и события, используемые в операциях перетаскивания, сосредоточены в классе System.Windows.DragDrop, и через него доступны другим классам (например, UIElement). В действительности, операция перетаскивания выполняется в три этапа, которые описаны ниже. 1. Пользователь щелкает на элементе (или выделяет некоторую область внутри него) и удерживает нажатой кнопку мыши. В этот момент начинается выполнение операции перетаскивания и сохраняется некоторая информация. 2. Пользователь наводит указатель мыши на другой элемент. Если этот элемент может принимать тип перетаскиваемого содержимого (например, порцию текста), указатель мыши принимает вид значка перетаскивания. В противном случае указатель мыши принимает вид перечеркнутого кружка. 3. Когда пользователь отпускает кнопку мыши, элемент получает информацию и принимает решение о дальнейшей ее судьбе. Эту операцию можно отменить, нажав клавишу (не отпуская кнопки мыши). Вы можете потренироваться с операцией перетаскивания, добавив два объекта
TextBox в окно, которое имеет код для поддержки операции перетаскивания. Если вы выберете некоторый текст внутри текстового поля, то сможете перетащить его в другое текстовое поле. Когда вы отпустите кнопку мыши, текст будет перемещен. Те же принципы распространяются и на приложения — например, вы можете перетащить кусок текста из документа Word в объект WPF TextBox, или наоборот.
Book_Pro_WPF-2.indb 184
19.05.2008 18:09:58
Свойства зависимостей и маршрутизируемые события
185
На заметку! Не следует путать операцию перетаскивания с возможностью “перемещения” элемента в пределах окна. Эта особенность является технологией, которую используют инструменты рисования и черчения диаграмм для того, чтобы вы могли перемещать содержимое. Об этом речь пойдет в главе 14. Иногда бывает необходимо выполнить перетаскивание между элементами, не обладающими такой возможностью. Например, вам нужно будет сделать так, чтобы пользователь мог перетаскивать содержимое из текстового окна на метку. Или создать пример, показанный на рис. 6.8, который дает пользователю возможность перетаскивать текст из объекта Label или TextBox в другую метку. В этой ситуации придется обрабатывать события перетаскивания. Существуют две стороны операции переРис. 6.8. Перетаскивание содержимого из таскивания: источник и цель. Чтобы создать одного элемента в другой источник перетаскивания, нужно вызвать метод DragDrop.DoDragDrop() в некоторой точке, чтобы начать операцию перетаскивания. В этот момент вы идентифицируете источник для операции перетаскивания, задаете содержимое, которое хотите передавать, и показываете, какие эффекты будут разрешены при перетаскивании (копирование, перемещение и т.д.). Обычно метод DoDragDrop() вызывается в ответ на событие MouseDown или PreviewMouseDown. Ниже показан пример, который начинает операцию перетаскивания при щелчке на метке. Для операции перетаскивания используется текстовое содержимое метки: private void lblSource_MouseDown(object sender, MouseButtonEventArgs e) { Label lbl = (Label)sender; DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy); }
Элемент, который принимает данные, должен иметь в своем свойстве AllowDrop значение true. Кроме того, ему нужно обработать событие Drop, чтобы иметь возможность оперировать данными: To Here
Когда вы присваиваете свойству AllowDrop значение true, вы конфигурируете элемент, чтобы разрешить любой тип информации. Если нужны большие возможности, можете обработать событие DragEnter. В этот момент вы можете проверить тип перетаскиваемых данных, а затем определить тип разрешенной операции. Следующий пример разрешает работать только с текстовым содержимым — если вы попытаетесь перетащить что-то, что не может быть преобразовано в текст, операция перетаскивания не будет разрешена, а указатель мыши примет вид перечеркнутого кружка: private void lblTarget_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.Text)) e.Effects = DragDropEffects.Copy; else e.Effects = DragDropEffects.None; }
Book_Pro_WPF-2.indb 185
19.05.2008 18:09:58
186
Глава 6
Наконец, когда операция будет завершена, вы сможете извлечь данные и работать с ними. Следующий код принимает перемещенный текст и вставляет его в метку: private void lblTarget_Drop(object sender, DragEventArgs e) { ((Label)sender).Content = e.Data.GetData(DataFormats.Text); }
Во время операции перетаскивания вы можете меняться объектами любых типов. Однако, несмотря на то, что этот простой способ прекрасно подходит для ваших приложений, его применять не рекомендуется, если вам нужно связываться с другими приложениями. Если вы хотите перетащить информацию в другое приложение, вам следует использовать базовый тип данных (например, строки, целые числа и т.п.) или объект, который мог бы реализовывать интерфейсы ISerializable или IDataObject (что позволит .NET передавать ваш объект в поток байтов и заново создавать объект в другом домене приложения). Интересным приемом является преобразование элемента WPF в XAML с последующей его реконструкцией в каком-то другом месте. Все, что вам нужно — это объекты XamlWriter и XamlReader, рассмотренные в главе 2. На заметку! Если вы хотите передавать данные между приложениями, используйте класс System. Windows.Clipboard, который предлагает статические методы помещения данных в буфер обмена Windows и извлечения их в самых разных форматах.
Резюме В этой главе мы детально рассмотрели свойства зависимостей WPF и маршрутизируемые события. Сначала вы могли увидеть, как определяются и регистрируются свойства зависимостей, и как они подключаются к остальным службам WPF. Затем мы рассмотрели маршрутизируемые события и узнали, как он позволяют работать с событиями на разных уровнях — либо непосредственно в источнике, либо во вмещающем элементе. В конце главы вы могли увидеть, как эти стратегии маршрутизации реализуются в элементах WPF, чтобы вы могли обрабатывать ввод данных с клавиатуры и с помощью мыши. Может быть, вам захочется приступить к написанию обработчиков событий, которые будут реагировать на обычные события вроде перемещения мыши, чтобы реализовать простые графические эффекты или обновить пользовательский интерфейс. Однако пока что этого делать не следует. Как будет показано в главе 12, вы можете автоматизировать многие простые программные операции с помощью декларативной разметки с использованием стилей и триггеров WPF. Однако прежде чем вы займетесь изучением данного вопроса, в следующей главе мы коротко поговорим о принципах работы в WPF самых главных графических фрагментов (например, кнопки, метки и текстовые окна). Совет. Один из самых лучших способов изучить возможности WPF заключается в анализе кода для базовых элементов WPF, таких как Button, UIElement и FrameworkElement. Одним из наиболее удобных инструментов для этого является Reflector, созданный Лутцем Редерем (Lutz Roeder); этот инструмент доступен по адресу http://www.aisto.com/roeder/dotnet. С его помощью вы сможете увидеть определения свойств зависимостей и маршрутизируемых событий, просмотреть код статического конструктора, который инициализирует их, и даже узнать, как свойства и события используются в коде класса.
Book_Pro_WPF-2.indb 186
19.05.2008 18:09:58
ГЛАВА
7
Классические элементы управления С
ейчас, когда мы разобрались с основными вопросами по компоновке, содержимому и обработке событий в WPF, мы можем приступить к подробному изучению элементов, которые включает WPF. В этой главе мы поговорим о самых основных элементах управления WPF, к которым относятся метки, кнопки и текстовые окна. Несмотря на то что Windows-разработчики пользуются этими элементами уже много лет, в настоящей главе будет рассказано о некоторых важных деталях относительно их реализации в WPF. Наряду с элементами, вы познакомитесь с классом System.Windows. Control и узнаете, как элементы управления WPF используют кисти и шрифты.
Класс Control Как уже было сказано в главе 5, окна WPF содержат элементы, однако только некоторые из этих элементов являются элементами управления. К элементам управления относятся элементы, поддерживающие интерактивную связь с пользователем — они могут принимать фокус и получать входные данные от клавиатуры или мыши. Все элементы управления происходят от класса System.Windows.Control, который наделяет их базовыми характеристиками:
• они позволяют определять расположение содержимого внутри элемента управления;
• они позволяют определять порядок перехода с использованием клавиши табуляции;
• они поддерживают рисование фона, переднего плана и рамки; • они поддерживают форматирование размера и шрифта текстового содержимого. С первыми двумя пунктами должно быть все ясно — мы их уже рассматривали. (В главе 5 рассматривались вопросы содержимого и выравнивания, а в главе 6 — фокус и порядок обхода по клавише табуляции.) В следующих двух разделах речь пойдет о кистях и шрифтах.
Кисти фона и переднего плана Все элементы управления имеют фон и передний план. Как правило, фоном является поверхность элемента управления (белая или серая область внутри рамки кнопки), а передним планом — текст. Цвет этих двух областей (но не содержимого) в WPF определяется с помощью свойств Background и Foreground соответственно.
Book_Pro_WPF-2.indb 187
19.05.2008 18:09:59
188
Глава 7
Логично предположить, что свойства Background и Foreground могут использовать объекты цвета, как в приложении, созданном на основе Windows Forms. Однако на самом деле эти свойства используют более универсальный объект — Brush. Благодаря этому вы сможете заливать содержимое фона и переднего плана сплошным цветом (с помощью кисти SolidColorBrush) или чем-то экзотическим (например, используя кисти LinearGradientBrush или TileBrush). В этой главе мы рассмотрим только простую кисть SolidColorBrush, а в главе 13 мы на практических примерах познакомимся с другими ее вариантами. На заметку! Все классы Brush определены в пространстве имен System.Windows.Media.
Установка цветов в коде Предположим, что вы хотите установить поверхность голубого цвета внутри кнопки
cmd. Ниже показан код, с помощью которого можно это сделать: cmd.Background = new SolidColorBrush(Colors.AliceBlue);
Этот код создает новый объект SolidColorBrush с помощью готового цвета посредством статического свойства класса Colors. (Имена основаны на названиях цветов, которые поддерживаются большинством браузеров.) Затем определяется кисть в качестве фоновой кисти кнопки, в результате чего фон кнопки становится светло-голубым. На заметку! Данный метод придания стиля кнопке не является в полной мере удовлетворительным. Если вы попытаетесь применить его, вы увидите, что он конфигурирует фоновый цвет кнопки в ее обычном состоянии (не нажата), и не изменяет цвет, который появляется при щелчке на кнопке (серый цвет). Чтобы научиться настраивать каждый аспект вида кнопки, вам следует ознакомиться с шаблонами, которые рассматриваются в главе 15. Вы можете также пользоваться системными цветами (они могут выбираться исходя из предпочтений пользователя) из перечисления System.Windows.SystemColors. Ниже показан пример: cmd.Background = new SolidColorBrush(SystemColors.ControlColor);
Поскольку системные кисти используются часто, класс SystemColors предлагает готовые свойства, возвращающие объект SolidColorBrush. Ниже показано, как их применять. cmd.Background = SystemColors.ControlBrush;
В каждом из этих примеров присутствует одна незначительная проблема. Если системный цвет будет изменен после того, как вы запустите этот код, ваша кнопка не будет обновлена, поэтому новый цвет применяться не будет. По сути, этот код делает мгновенный снимок текущего цвета или кисти. Чтобы ваша программа умела реагировать на изменения в конфигурации, вы должны применять динамические ресурсы, о которых пойдет речь в главе 11. Классы Colors и SystemColors позволяют просто и быстро задать цвет, однако это можно делать не только с их помощью. Вы можете, например, создать объект Color, определяя значения R, G и B (они соответствуют красной, зеленой и синей составляющим цвета). Каждое из этих значений является числом из диапазона 0–255: int red = 0; int green = 255, int blue = 0; cmd.Foreground = new SolidColorBrush(Color.FromRgb(red, green, blue));
Book_Pro_WPF-2.indb 188
19.05.2008 18:09:59
Классические элементы управления
189
Вы можете также сделать цвет частично прозрачным, используя значение альфаканала и вызывая метод Color.FromArgb(). Значение альфа-канала, равное 255, соответствует полностью непрозрачном цвету, а значение 0 — полностью прозрачному.
RGB и scRGB Стандарт RGB полезен, поскольку применяется во многих других программах. Так, например, вы можете получить RGB-значение цвета в программе для рисования и использовать этот же цвет в WPF-приложении. Однако не исключено, что другие устройства (например, принтеры) могут поддерживать более широкий диапазон цветов. По этой причине был создан альтернативный стандарт scRGB, в котором каждый компонент цвета (альфа-канал, красный, зеленый и синий) представлен с помощью 64-битных значений. Структура цветов WPF поддерживает оба подхода. Она включает как набор стандартных свойств RGB (A, R, G и B), так и набор свойств scRGB (ScA, ScR, ScG и ScB). Эти свойства связаны между собой, поэтому если вы зададите свойство R, то соответственным образом изменится и свойство ScR. Взаимосвязь между значениями RGB и значениями scRGB не является линейной. Значение 0 в системе RGB соответствует значению 0 в scRGB, 255 в RGB соответствует 1 в scRGB, а все значения в диапазоне 0–255 в RGB представлены как десятичные значения в диапазоне 0–1 в scRGB.
Установка цветов в XAML Когда вы задаете цвет фона или переднего плана в XAML, вы можете воспользоваться сокращением. Вместо того чтобы определять объект Brush, вы можете задать наименование или значение цвета. Синтаксический анализатор WPF автоматически создаст объект SolidColorBrush с использованием выбранного вами цвета, и будет применять этот объект для фона или переднего плана. Ниже показан пример, в котором используется имя цвета. A Button
Он эквивалентен следующему синтаксису: A Button
Если вы решите создать другой тип кисти (например, LinearGradientBrush), вам нужно будет применять более длинную форму и использовать ее для рисования фона. Если необходим код цвета, придется пользоваться менее удобным синтаксисом, в котором значения R, G и B представляются в шестнадцатеричном формате. Доступны два формата: #rrggbb или #aarrggbb (они отличаются тем, что последний формат содержит значение альфа-канала). Чтобы задать значения A, R, G и B, вам понадобятся только две цифры, поскольку все они представляются в шестнадцатеричной форме. Ниже показан пример, который создает тот же цвет, что и в предыдущем фрагменте кода, с помощью записи #aarrggbb: A Button
Здесь значением альфа-канала является FF (255), значением красной составляющей — FF (255), а значениями зеленой и синей — 0.
Book_Pro_WPF-2.indb 189
19.05.2008 18:09:59
190
Глава 7
На заметку! Кисти поддерживают автоматическое уведомление об изменениях. То есть, если вы присоединяете кисть к элементу управления и изменяете ее, элемент управления обновляет себя соответствующим образом. Этот механизм работает благодаря тому, что кисти являются потомками класса System.Windows.Freezable. Название этого класса объясняется тем, что все “замораживаемые” (freezable) объекты имеют два состояния — состояние, при котором они доступны для чтения, и состояние, при котором они доступны только для чтения (“замороженное” состояние). Свойства Background и Foreground не являются единственными свойствами, которые вы можете определять для кисти. Вы можете также нарисовать рамку вокруг элементов управления (и некоторые другие элементы вроде Border) с помощью свойств BorderBrush и BorderThickness. Свойство BorderBrush принимает выбранную вами кисть, а свойство BorderThickness принимает ширину рамки в единицах, не зависящих от устройства. Прежде чем вы сможете увидеть рамку, вы должны будете установить оба свойства. На заметку! Некоторые элементы управления не поддерживают использование свойств BorderBrush и BorderThickness. Объект Button игнорирует их полностью, поскольку определяет свой фон и рамку с помощью декоратора ButtonChrome. Тем не менее, вы можете придать кнопке новый облик (с выбранной вами рамкой) с помощью шаблонов, о которых речь пойдет в главе 15.
Прозрачность В отличие от Windows Forms, в WPF поддерживается истинная прозрачность. Это означает, что если вы расположите несколько элементов один поверх другого и зададите для каждого из них различную степень прозрачности, то увидите в точности то, что и можно ожидать. Если же говорить проще, то эта возможность дает возможность создать графический фон, который будет “просматриваться сквозь” элементы, расположенные на нем. При наличии необходимых навыков эта особенность позволит создавать многослойные анимационные объекты и другие эффекты, создание которых в других средах может оказаться чрезвычайно проблематичным. Сделать элемент прозрачным можно двумя способами.
• С помощью свойства Opacity. Свойство Opacity (непрозрачность) — это дробное значение в диапазоне 0..1, где 1 соответствует полностью непрозрачному цвету, а 0 — полностью прозрачному. Свойство Opacity определяется в классе UIElement (а также в базовом классе Brush), поэтому его можно применять ко всем элементам.
• С помощью полупрозрачного цвета. Любой цвет, значение альфа-канала которого составляет менее 255, является полупрозрачным. По мере возможности старайтесь использовать прозрачные цвета вместо свойства Opacity, так как они работают лучше. А поскольку вы можете применять разные цвета к разным частям элемента управления, вы можете применять прозрачные цвета для создания элемента управления, который будет частично прозрачным — например, чтобы создать полупрозрачный фон с совершенно непрозрачным текстом. На рис. 7.1 показан пример, в котором имеется несколько полупрозрачных слоев.
• Окно имеет непрозрачный белый фон. • Панель верхнего уровня StackPanel, которая содержит все элементы, имеет свойство ImageBrush, которое определяет изображение. Свойство Opacity этой кисти уменьшено для того, чтобы подсветить ее; это дает возможность видеть сквозь нее белый фон.
Book_Pro_WPF-2.indb 190
19.05.2008 18:09:59
Классические элементы управления
191
• В первой кнопке используется полупрозрачный красный цвет фона. Изображение просматривается сквозь фон кнопки, а текст является непрозрачным.
• Метка (под первой кнопкой) используется “как есть”. По умолчанию все метки имеют полностью непрозрачный цвет фона.
• Текстовое окно использует непрозрачный текст и непрозрачную рамку, а также полупрозрачный цвет фона.
• Еще одна панель StackPanel под текстовым окном использует TileBrush, чтобы создать шаблон с улыбаю- Рис. 7.1. Окно с несколькими полупрозрачныщимся лицом. Кисть TileBrush име- ми слоями ет уменьшенное значение Opacity, поэтому фон просматривается сквозь нее. Например, вы можете видеть солнце в правой нижней части формы.
• Вторая панель StackPanel имеет элемент TextBlock с полностью прозрачным фоном и полупрозрачным белым текстом. Если присмотреться, то можно увидеть, что оба фона просматриваются сквозь некоторые буквы. Ниже показано содержимое окна в XAML. Имейте в виду, что этот пример включает одну деталь, с которой вы еще не знакомы: специализированную кисть ImageBrush для рисования содержимого изображения. (Класс ImageBrush будет рассмотрен в главе 13.) A Semi-Transparent Button Some Label Text A semi-transparent text box Semi-Transparent Layers
Прозрачность является популярной возможностью WPF — в действительности, она настолько проста и работает так хорошо, что является уже неотъемлемой частью пользовательского интерфейса WPF. Поэтому следите за тем, чтобы не перестараться, используя ее.
Book_Pro_WPF-2.indb 191
19.05.2008 18:09:59
192
Глава 7
Шрифты Класс Control определяет небольшой набор свойств, связанных со шрифтами, которые задают, как отображается текст в элементе управления. Эти свойства перечислены в табл. 7.1. На заметку! Класс Control не определяет свойства, использующие его шрифт. В то время как многие элементы управления включают свойство Text, оно не определяется как часть базового класса элементов управления. Очевидно, что свойства шрифтов ничего не означают, если они не используются производным классом.
Таблица 7.1. Свойства шрифтов класса Control Имя
Описание
FontFamily
Имя шрифта, который вы хотите использовать.
FontSize
Размер шрифта в единицах, не зависящих от устройства (каждая из них представляет собой 1/96 дюйма). Использование этого свойства несколько отличается от традиционного использования; оно предназначено для поддержки новой модели визуализации в WPF, которая не зависит от разрешения. Обычные Windows-приложения измеряют шрифты с помощью точек (point), которые равны 1/72 дюйма на стандартном мониторе для ПК. Если вам нужно преобразовать размер шрифта WPF в более знакомый размер шрифта, вы можете воспользоваться следующим приемом: просто умножьте его на 3/4. Например, традиционные 38 точек эквивалентны 48 единицам в WPF.
FontStyle
Наклонение текста, представленное объектом FontStyle. Необходимый вам предварительно заданный набор FontStyle вы получаете из статических свойств класса FontStyles, который включает написание символов Normal, Italic или Oblique. (Oblique — это устаревший способ создания курсивного текста на компьютере, в котором нет необходимого курсивного шрифта. Буквы берутся из обычного шрифта и пишутся под углом с помощью трансформации. Как правило, результат получается неважным.)
FontWeight
Вес текста, представленный объектом FontWeight. Необходимый вам предварительно заданный набор FontWeight вы получаете из статических свойств класса FontWeight. Полужирный (bold) является наиболее очевидным из них, хотя некоторые гарнитуры предлагают другие варианты, такие как Heavy, Light, ExtraBold и т.д.
FontStretch
Величина, на которую растягивается или сжимается текст, представленная объектом FontStretch. Необходимый вам предварительно заданный набор FontStretch вы получаете из статических свойств класса FontStretches. Например, UltraCondensed сжимает текст до 50% от обычной ширины, а UltraExpanded расширяет их до 200%. Растяжение шрифта является особенностью OpenType, которая не поддерживается многими гарнитурами. (Чтобы поэкспериментировать с этим свойством, попробуйте шрифт Rockwell, который поддерживает ее.)
Очевидно, что наиболее важным из этих свойств является FontFamily. Семейство шрифтов (font family) представляет собой коллекцию связанных между собой гарнитур — например, Arial Regular, Arial Bold, Arial Italic и Arial Bold Italic являются частью семейства шрифтов Arial. Несмотря на то что типографские правила и символы для каждой вариации определяются отдельно, операционная система подразумевает, что все они связаны между собой. Поэтому вы можете конфигурировать элемент для использо-
Book_Pro_WPF-2.indb 192
19.05.2008 18:09:59
Классические элементы управления
193
вания Arial Regular, присвоить свойству FontWeight значение Bold и быть уверенными в том, что WPF будет переключаться на гарнитуру Arial Bold. При выборе шрифта необходимо указывать полное имя семейства, как показано ниже: A Button
Почти так же делается и в коде: cmd.FontFamily = "Times New Roman"; cmd.FontSize = "18";
При идентификации FontFamily нельзя использовать укороченную строку. Это означает, что вы не можете указывать Times или Times New вместо полного имени Times New Roman. Чтобы получить курсив или полужирный шрифт, вы можете (необязательно) использовать полное имя гарнитуры, как показано ниже: A Button
Тем не менее, проще и полезнее использовать просто имя семейства и задавать другие свойства (такие как FontStyle и FontWeight), чтобы получить требуемый вариант. Например, следующая разметка присваивает семейству шрифт Times New Roman, а весу шрифта — FontWeights.Bold: A Button
Текстовые декорации и типография Некоторые элементы тоже поддерживают более сложную манипуляцию текстом с помощью свойств TextDecorations и Typography. Они позволяют украшать текст. Например, вы можете задать свойство TextDecorations с помощью статического свойства из класса TextDecorations. Оно предлагает только четыре декорации, каждая из которых позволяет добавить в текст некоторую разновидность линии. Они включают Baseline, OverLine, Strikethrough и Underline. Свойство Typography является более сложным — оно позволяет получать доступ к специализированным вариантам гарнитур, которые могут предоставить лишь некоторые шрифты. В качестве примера можно упомянуть различные выравнивания чисел, лигатуры (связи между соседними буквами) и капители. По большому счету, особенности TextDecorations и Typography находят свое применение только в содержимом потоковых документов. (О документах речь пойдет в главе 19.) Однако вычурности присутствуют и в классе TextBox. Кроме того, они поддерживаются элементом управления TextBlock, который является облегченной версией Label, прекрасно подходит для показа небольших объемов текстового содержимого и допускает перенос текста. Несмотря на то что вы вряд ли будете использовать TextDecorations в элементе управления TextBox или изменять его свойство Typography, вам может понадобиться использовать подчеркивание в TextBlock, как показано ниже: Underlined text
Если вы планируете поместить большой объем текстового содержимого в окно и хотите отформатировать отдельные части (например, подчеркнуть важные слова), вам следует прочитать главу 19, в которой вы узнаете об очень многих потоковых элементах. Несмотря на то что потоковые элементы предназначены для использования в документах, вы можете внедрять их внутри TextBlock.
Book_Pro_WPF-2.indb 193
19.05.2008 18:09:59
194
Глава 7
Наследование шрифтов Когда вы задаете одно из свойств шрифта, значение этого свойства проходит сквозь вложенные объекты. Например, если вы зададите свойство FontFamily для окна верхнего уровня, то каждый элемент управления в данном окне получит это же значение FontFamily (если только элемент управления явным образом не определит другой шрифт). Эта особенность похожа на концепцию свойств окружения (ambient property), существующую в Windows Forms, хотя сама ее основа является другой. Она работает благодаря тому, что свойства шрифтов являются свойствами зависимостей, а одной из возможностей, которой обладают свойства зависимостей, является наследование значений свойств — процесс передачи параметров шрифта всем вложенным элементам управления. Стоит отметить, что наследование значения свойства может осуществляться в элементах, которые даже не поддерживают это свойство. Например, предположим, что вы создаете окно, в котором имеется панель StackPanel, а внутри нее — три метки Label. Вы можете задать свойство FontSize окна, поскольку класс Window происходит от класса Control. Вы не можете задать свойство FontSize панели, поскольку она не является элементом управления. Тем не менее, если вы зададите свойство FontSize окна, значение свойства пройдет “сквозь” панель StackPanel, и его получат метки, которые, в конечном счете, изменят размер своего шрифта. Наряду с параметрами шрифтов, в некоторых других базовых свойствах используется наследование значений свойств. Так, наследование применяется свойством Foreground в классе Control. А свойство Background не использует наследования. Тем не менее, фон, заданный по умолчанию, представляет собой пустую ссылку, которая визуализируется большинством элементов управления в виде прозрачного фона. (Это означает, что родительский фон будет просматриваться, как было показано на рис. 7.1.) В классе UIElement наследование поддерживается свойствами AllowDrop, IsEnabled и IsVisible. В классе FrameworkElement наследование поддерживается свойствами CultureInfo и FlowDirection. На заметку! Свойство зависимостей поддерживает наследование только в том случае, если флаг FrameworkPropertyMetadata.Inherits будет иметь значение true, которое не является его значением по умолчанию. В главе 6 мы подробно говорили о классе FrameworkProperty Metadata и регистрации свойств.
Замена шрифтов При установке шрифтов вы должны основательно подходить к выбору шрифта и выяснить заранее, будет ли он поддерживаться на пользовательском компьютере. Однако WPF может прийти на помощь в этом вопросе благодаря системе обхода шрифтов. Вы можете указать в свойстве FontFamily список шрифтов, разделяя их запятыми. После этого WPF сама выберет шрифт из заданного списка. Ниже показан пример, в котором производится попытка использовать шрифт Technical Italic, а в случае невозможности его использования будут выбран шрифт Comic Sans MS или Arial: A Button
Если семейство шрифтов действительно будет содержать запятую в своем имени, вам нужно будет написать ее в строке дважды. Между прочим, вы можете получить список всех шрифтов, установленных на текущем компьютере, с помощью статической коллекции SystemFontFamilies класса
Book_Pro_WPF-2.indb 194
19.05.2008 18:10:00
Классические элементы управления
195
System.Windows.Media.Fonts. Ниже показан пример, в котором эта коллекция используется для добавления шрифтов в окно списка: foreach (FontFamily fontFamily in Fonts.SystemFontFamilies) { lstFonts.Items.Add(fontFamily.Source); }
Объект FontFamily позволяет проверить другие детали, такие как междустрочный интервал и связанные гарнитуры. На заметку! Одним из ингредиентов, которых нет в WPF, является диалоговое окно для выбора шрифта. Группа разработчиков WPF Text предложила два более привлекательных средства для выбора шрифта, включая версию без кода, использующую привязку данных (http:// blogs.msdn.com/text/archive/2006/06/20/592777.aspx), и более изощренную версию, которая поддерживает необязательные типографские особенности, которые можно встретить в шрифтах OpenType (http://blogs.msdn.com/text/archive/2006/11/01/ sample-font-chooser.aspx).
Встраивание шрифтов Другой опцией при работе с необычными шрифтами является их встраивание в приложение. Благодаря такой возможности, ваше приложение никогда не будет иметь проблем с нахождением требуемого шрифта. Процесс встраивания очень прост. Сначала вы добавляете файл шрифта (как правило, файл с расширением .ttf) в приложение и присваиваете параметру Build Action значение Resource. (Это можно сделать в Visual Studio, выбрав файл шрифта в Solution Explorer и изменив Build Action в окне Properties (Свойства).) Затем, при использовании шрифта, вам нужно будет добавить символьную последовательность ./# перед именем семейства, как показано ниже: This is an embedded font
Символы ./ WPF интерпретирует как текущую папку. Чтобы понять, что это означает, вы должны разобраться с системой упаковки XAML. Как было сказано в главе 1, вы можете запускать автономные (т.н. несвязанные) XAML-файлы прямо в вашем браузере, не компилируя их. Единственное условие состоит в том, что XAML-файл не может использовать файл с базовым кодом. В этом случае WPF ищет файлы шрифтов, которые находятся в том же каталоге, в котором находится XAML-файл, и делает их доступными для вашего приложения. Как правило, прежде чем запускать ваше WPF-приложение, вы будете компилировать его в сборку .NET. В этом случае текущая папка остается местом хранения XAMLдокумента, только на этот раз документ компилируется и встраивается в вашу сборку. WPF ссылается на скомпилированные ресурсы с помощью стандартизированного синтаксиса URI (Uniform Resource Identifier — унифицированный идентификатор ресурса), который будет рассматриваться в главе 11. Все URI-идентификаторы приложения начинаются с последовательности pack://application. Если вы создадите проект под именем ClassicControls и добавите окно EmbeddedFont.xaml, то окно будет иметь следующий URI-идентификатор: pack://application:,,,/ClassicControls/embeddedfont.xaml
Этот URI-идентификатор доступен в нескольких местах, включая вариант с использованием свойства FontFamily.BaseUri. WPF применяет этот URI-идентификатор в качестве привязки при поиске шрифтов. Таким образом, когда вы используете синтак-
Book_Pro_WPF-2.indb 195
19.05.2008 18:10:00
196
Глава 7
сис ./ в скомпилированном WPF-приложении, WPF будет искать те шрифты, которые встроены как ресурсы, существующие вместе с вашим скомпилированным XAML. После символьной последовательности ./ вы можете указать имя файла, однако обычно добавляется просто знак числа (#) и имя семейства шрифтов. В предыдущем примере встроенный шрифт получил имя Bayern. На заметку! Настройка встроенного шрифта может оказаться несколько сложной. Вам нужно убедиться в том, что вы получите верное имя семейства шрифтов, а также в том, что вы выбираете корректное действие сборки для файла шрифта. Более того, Visual Studio в настоящее время не обеспечивает поддержку разработки встроенных шрифтов (это означает, что ваш текст в элементе управления не будет отображаться в корректном шрифте до тех пор, пока вы не запустите приложение). Чтобы посмотреть пример правильной настройки, просмотрите пример кода в этой главе. Встраивание шрифтов поднимает очевидные вопросы, связанные с лицензированием. К сожалению, большинство поставщиков шрифтов разрешают встраивать свои шрифты в документы (например, в файлы формата PDF), но не в приложения (например, сборки WPF), даже если встроенный шрифт WPF не является доступным непосредственно для конечного пользователя. WPF не лицензирует шрифты, и все же перед тем как распространять шрифт, убедитесь в том, что вы не нарушаете условия лицензии. О разрешениях на использование шрифтов можно узнать с помощью бесплатной утилиты-расширения окна свойств шрифта Microsoft, которая доступна для загрузки по адресу http://www.microsoft.com/typography/TrueTypeProperty21.mspx. Как только вы инсталлируете эту утилиту, щелкните правой кнопкой мыши на любом файле шрифта и выберите в контекстном меню команду Properties (Свойства), чтобы посмотреть более детальную информацию о нем. В частности, проверьте на вкладке Embedding (Встраивание) информацию о том, разрешено ли встраивание этого шрифта. Шрифты, имеющие отметку Installed Embedding Allowed (Инсталлированное встраивание разрешено), пригодны для использования в WPF-приложениях, в то время как шрифты, имеющие отметку Editable Embedding Allowed (Редактируемое встраивание разрешено) могут оказаться не пригодными для этого. Информацию о лицензионном использовании можно узнать у поставщика шрифта.
Указатели мыши В любом приложении нужно делать так, чтобы указатель мыши показывал, что приложение занято, или отражал работу разных элементов управления. Вы можете задать указатель мыши для любого элемента, используя свойство Cursor, которое является наследником класса FrameworkElement. Каждый указатель представляется объектом System.Windows.Input.Cursor . Получить объект Cursor проще всего можно с помощью статических свойств класса Cursors (из пространства имен System.Windows.Input). Они включают все стандартные указатели Windows, такие как песочные часы, рука, стрелки изменения размеров и т.д. Ниже показан пример, в котором для текущего окна определяются песочные часы: this.Cursor = Cursors.Wait;
Теперь, когда вы будете перемещать указатель мыши в текущем окне, указатель примет вид песочных часов (в системе Windows XP) или водоворота (в системе Windows Vista). На заметку! Свойства класса Cursors рисуют указатели, определенные в компьютере. Если пользователь настроит набор стандартных указателей, созданное вами приложение будет использовать специальные указатели.
Book_Pro_WPF-2.indb 196
19.05.2008 18:10:00
Классические элементы управления
197
Если вы зададите указатель в XAML, вам не нужно будет использовать класс Cursors напрямую. Это объясняется тем, что TypeConverter для свойства Cursor может распознавать имена свойств и получать соответствующий объект Cursor из класса Cursors. Это означает, что вы можете написать разметку, подобную нижеследующей, чтобы отобразить курсор “справки” (комбинация стрелки и вопросительного знака), когда указатель мыши будет наведен на кнопку: Help
Параметры указателя можно менять. Например, вы можете задать разные указатели для кнопки и окна, в которой она находится. Указатель кнопки будет отображаться при наведении на кнопку, а указатель окна будет использоваться в любом другом участке окна. Тем не менее, существует одно исключение. Родитель может переопределить параметры указателя своих потомков с помощью свойства ForceCursor. Если этому свойству будет присвоено значение true, свойство потомка Cursor будет проигнорировано, в то время как родительское свойство Cursor будет применено повсеместно. Если вы хотите применить параметры указателя к каждому элементу в каждом окне приложения, свойство FrameworkElement.Cursor не поможет вам сделать это. Вместо него вам нужно будет использовать статическое свойство Mouse.OverrideCursor, которое переопределяет свойство Cursor каждого элемента: Mouse.OverrideCursor = Cursors.Wait;
Чтобы отменить это переопределение, действующее в рамках всего приложения, присвойте свойству Mouse.OverrideCursor значение null. И, наконец, WPF поддерживает использование специальных указателей. Вы можете применять как обычные файлы указателей .cur (по сути, это небольшие битовые образы), так и файлы анимационных указателей .ani. Чтобы использовать специальный указатель, нужно передать имя файла вашего указателя или поток вместе с данными указателя конструктору объекта Cursor: Cursor customCursor = new Cursor(Path.Combine(applicationDir, "stopwatch.ani"); this.Cursor = customCursor;
Объект Cursor не поддерживает напрямую синтаксис URI, который позволяет другим элементам WPF (таким как Image) работать с файлами, хранящимися в скомпилированной сборке. Однако ничего не мешает добавить файл указателя в приложение в качестве ресурса, а затем извлечь его как поток, который можно будет использовать для создания объекта Cursor. Для этой цели предназначен метод Application.GetResourceStream(): StreamResourceInfo sri = Application.GetResourceStream( new Uri("stopwatch.ani", UriKind.Relative)); Cursor customCursor = new Cursor(sri.Stream); this.Cursor = customCursor;
Этот код подразумевает, что вы добавили файл stopwatch.ani в ваш проект и присвоили параметру Build Action значение Resource. Эта технология будет подробно рассматриваться в главе 12.
Элементы управления содержимым Как уже было сказано в главе 5, многие основные элементы управления WPF связаны с управлением содержимым. К их числу относятся такие элементы управления, как Label, Button, CheckBox и RadioButton.
Book_Pro_WPF-2.indb 197
19.05.2008 18:10:00
198
Глава 7
Метки Простейшим элементом управления содержимым является Label — метка. Как и любой другой элемент управления содержимым, она принимает одиночную порцию содержимого, которую вы хотите поместить внутри нее. Отличительной чертой элемента Label является его поддержка мнемонических команд — клавиш быстрого доступа, которые передают фокус связанному элементу управления. Для обеспечения поддержки этой функции элемент управления Label предлагает свойство Target. Чтобы задать это свойство, вам необходимо воспользоваться выражением привязки, которое будет указывать на другой элемент управления. Ниже показан синтаксис, который нужно использовать для этой цели: Choose _A Choose _B
Символ подчеркивания в тексте метки указывает на клавишу быстрого доступа. (Если вы действительно хотите, чтобы в метке отображался символ подчеркивания, нужно добавить два таких символа.) Все мнемонические команды работают при одновременном нажатии клавиши и заданной вами клавиши быстрого доступа. Например, если в данном примере пользователь нажмет комбинацию , то первая метка передаст фокус связанному элементу управления, которым в данном случае является txtA. Точно так же нажатие комбинации приводит к передаче фокуса элементу управления txtB. На заметку! Если вам доводилось программировать с использованием Windows Forms, то вы, наверное, применяли символ амперсанда (&) для обозначения клавиши быстрого доступа. В XAML для этой цели служит символ подчеркивания, поскольку символ амперсанда нельзя ввести в XML напрямую — вместо него нужно использовать неуклюжую комбинацию &. Обычно буквы клавиш быстрого доступа скрыты до тех пор, пока пользователь не нажмет , после чего они отмечаются подчеркиванием (рис. 7.2). Однако это поведение зависит от параметров системы.
Рис. 7.2. Клавиши быстрого доступа в метке
Book_Pro_WPF-2.indb 198
19.05.2008 18:10:00
Классические элементы управления
199
Совет. Если вам нужно лишь отображать содержимое, не поддерживая мнемонические команды, применяйте более облегченный элемент TextBlock. В отличие от элемента управления Label, TextBlock поддерживает перенос текста с помощью свойства TextWrapping.
Кнопки WPF распознает три типа кнопок: Button, CheckBox и RadioButton. Все эти кнопки представляют собой элементы управления содержимым, являющимися наследниками класса ButtonBase. Класс ButtonBase включает всего лишь несколько членов. Он определяет событие Click и добавляет поддержку команд, которые позволят подключить кнопки для высокоуровневых задач приложений (об этом мы будем говорить в главе 10). Наконец, класс ButtonBase добавляет свойство ClickMode, которое определяет, когда кнопка генерирует событие Click в ответ на действия мыши. Значением, используемым по умолчанию, является ClickMode.Release, которое означает, что событие Click будет сгенерировано при нажатии и отпускании кнопки мыши. Однако вы можете также сделать так, чтобы событие Click возникало при первом нажатии кнопки мыши (ClickMode.Press) или всякий раз, когда указатель мыши будет наведен на кнопку и задержан над ней (ClickMode.Hover). На заметку! Все кнопки поддерживают клавиши доступа, которые работают подобно мнемоническим командам в элементе управления Label. Чтобы обозначить клавишу доступа, нужно добавить символ подчеркивания. Когда пользователь нажмет клавишу и клавишу доступа, возникнет событие Click кнопки.
Класс Button Класс Button представляет вездесущую кнопку Windows. Он добавляет всего два свойства, доступные для записи: IsCancel и IsDefault.
• Если свойство IsCancel имеет значение true, то эта кнопка будет работать как кнопка отмены окна. Если вы нажмете клавишу , когда текущее окно будет находиться в фокусе, то эта кнопка будет приведена в действие.
• Если свойство IsDefault имеет значение true, то эта кнопка считается кнопкой, используемой по умолчанию (она еще называется кнопкой принятия). Ее поведение зависит от того, где вы находитесь в данный момент в окне. Если вы навели указатель мыши на элемент управления, отличный от Button (например, TextBox, RadioButton, CheckBox и т.д.), то кнопка, используемая по умолчанию, будет затенена голубым цветом — почти так, как если бы она находилась в фокусе. Если вы нажмете клавишу , эта кнопка будет приведена в действие. Однако если вы наведете указатель мыши на другой элемент управления Button, то текущая кнопка будет затенена голубым цветом, и при нажатии будет приведена в действие именно эта кнопка, а не кнопка по умолчанию. Многие пользователи используют эти клавиши быстрого доступа (особенно клавишу для закрытия нежелательного диалогового окна), поэтому есть смысл потратить время на определение этих деталей в каждом создаваемом вами окне. Для кнопки по умолчанию и кнопки отмены вы можете написать код обработки события, так как WPF не поддерживает это поведение. В некоторых случаях имеет смысл сделать так, чтобы одна и та же кнопка в окне являлась и кнопкой отмены, и кнопкой по умолчанию. Таким примером может быть
Book_Pro_WPF-2.indb 199
19.05.2008 18:10:00
200
Глава 7
кнопка OK в окне О программе. Однако в окне должна быть только одна кнопка отмены и одна кнопка по умолчанию. Если вы назначите несколько кнопок отмены, то при нажатии клавиши будет просто передаваться фокус следующей кнопке по умолчанию, без ее активизации. Если у вас имеется несколько кнопок по умолчанию, нажатие клавиши приведет к непонятному поведению. Если в фокусе будет находиться элемент управления, отличный от Button, то при нажатии фокус будет передан следующей кнопке по умолчанию. Если же в фокусе находится элемент управления Button, нажатие клавиши активизирует ее.
Свойства ISDEFAULT и ISDEFAULTED Класс Button включает также свойство IsDefaulted, которое доступно только для чтения. IsDefaulted возвращает значение true для кнопки по умолчанию, если в фокусе находится другой элемент управления, не принимающий клавишу . В этой ситуации нажатие приведет к активизации кнопки. Например, элемент управления TextBox не принимает клавишу , если только вы не присвоите свойству TextBox.AcceptsReturn значение true. Если элемент управления TextBox, свойство TextBox.AcceptsReturn которого имеет значение true, находится в фокусе, то свойство IsDefaulted кнопки по умолчанию будет иметь значение false. Если элемент управления TextBox, свойство AcceptsReturn которого имеет значение false, получает фокус, то свойство IsDefaulted кнопки по умолчанию получает значение true. Свойство IsDefaulted возвращает значение false, когда кнопка находится в фокусе, даже если нажатие клавиши в этом месте приводит к активизации кнопки. Несмотря на то что вы вряд ли будете использовать свойство IsDefaulted, оно позволит написать некоторые типы триггеров стилей, о чем речь пойдет в главе 12. Если вам это не нужно, добавьте это свойство в список малопонятных особенностей WPF, чтобы разобраться с ним позже, консультируясь с вашими коллегами.
Классы ToggleButton и RepeatButton Помимо Button, еще три класса являются потомками класса ButtonBase.
• GridViewColumnHeader, который представляет заголовок столбца, активизируемый щелчком кнопкой мыши, если вы используете сеточный элемент ListView. Элемент управления ListView рассматривается в главе 18.
• RepeatButton, который будет непрерывно генерировать события Click, если пользователь нажмет, и будет удерживать нажатой кнопку. Обычные кнопки генерируют событие Click при однократном нажатии кнопки.
• ToggleButton, который представляет кнопку, имеющую два состояния (нажата и отпущена). Если вы щелкнете на кнопке ToggleButton, она будет оставаться нажатой до тех пор, пока вы не щелкнете на ней снова. Иногда такое поведение называют “клейким щелчком”. Классы RepeatButton и ToggleButton определены в пространстве имен System. Windows.Controls.Primitives, которое показывает, что сами по себе они применяются редко. Как правило, они используются для построения более сложных элементов управления, создавая или расширяя возможности путем наследования. Например, RepeatButton используется для создания высокоуровневого элемента управления ScrollBar (который является частью еще более высокоуровневого элемента ScrollViewer). RepeatButton придает кнопкам со стрелками в конце линейки прокрутки их отличительное поведение — прокрутка продолжается до тех пор, пока вы их
Book_Pro_WPF-2.indb 200
19.05.2008 18:10:00
Классические элементы управления
201
нажимаете. Точно так же ToggleButton применяется для порождения более полезных классов CheckBox и RadioButton, которые будут рассмотрены далее. Однако ни RepatButton, ни ToggleButton не являются абстрактными классами, поэтому в пользовательских интерфейсах вы можете работать с ними напрямую. ToggleButton очень удобно использовать внутри элемента ToolBar, который мы рассмотрим в главе 18.
Элемент управления CheckBox Кнопки CheckBox и RadioButton — это кнопки другого вида. Они являются потомками класса ToggleButton, а это означает, что пользователь может включать и выключать их (отсюда и наличие слова toggle). В случае CheckBox включение элемента управления означает отметку в нем флажка. Класс CheckBox не добавляет никаких членов, поэтому базовый интерфейс CheckBox определяется в классе ToggleButton. Более того, ToggleButton добавляет свойство IsChecked. Свойство IsChecked может принимать обнуляемое булевское значение — другими словами, оно может принимать значения true, false или null. Очевидно, что true представляет отмеченный флажок, а false — пустое место. Значение null используется для представления промежуточного состояния, которое отображается в виде затененного окошка. Промежуточное состояние обычно служит для того, чтобы представить значения, которые не были заданы, или области, в которых существует некоторые разногласия. Например, если у вас имеется флажок, который позволяет применять полужирный шрифт в текстовом приложении, а текущий выбор включает как полужирный, так и обычный текст, вы можете присвоить флажку значение null, чтобы отображать промежуточное состояние. Чтобы присвоить значение null в разметке WPF, нужно использовать расширение разметки Null, как показано ниже: A check box in indeterminate state
Наряду со свойством IsChecked класс ToggleButton добавляет свойство IsThreeState, которое определяет, может ли пользователь вводить флажок в промежуточное состояние. Если свойство IsThreeState будет иметь значение false (оно присваивается по умолчанию), то при щелчке флажок будет менять свое состояние между “отмечен” и “не отмечен”, а промежуточное состояние можно задать только с помощью кода. Если свойство ThreeState будет иметь значение true, то щелчки на флажке будут по очереди давать три возможных состояния. Класс ToggleButton определяет также три события, которые возникают, когда флажок принимает одно из определенных состояний: Checked, Unchecked и Intermediate. В большинстве случаев проще всего внедрить эту логику в один из обработчиков событий, обрабатывая событие Click, наследуемое от класса ButtonBase. Событие Click возникает всякий раз, когда кнопка меняет свое состояние.
Элемент управления RadioButton RadioButton тоже является наследником класса ToggleButton и использует то же свойство IsChecked и те же события Checked, Unchecked и Intermediate. Вместе с ними RadioButton добавляет свойство GroupName, которое позволяет управлять расположением переключателей в группах. Обычно переключатели группируются их контейнером. Это означает, что если вы поместите три элемента управления RadioButton в панели StackPanel, они сформируют группу, из которой вы сможете выбрать только один из них. С другой стороны, если вы поместите комбинацию переключателей в две разных панели StackPanel, вы получите две независимые группы.
Book_Pro_WPF-2.indb 201
19.05.2008 18:10:01
202
Глава 7
Свойство GroupName позволяет переопределить это поведение. Вы можете использовать его для того, чтобы создать несколько групп в одном и том же контейнере, или же чтобы создать одну группу, которая будет охватывать множество контейнеров. В любом случае, трюк очень прост — нужно просто присвоить всем переключателям, принадлежащим друг другу, имя одной и той же группы. Рассмотрим пример: Group 1 Group 1 Group 1 Group 2 Group 3 Group 3 Group 3 Group 2
Здесь мы видим два контейнера, вмещающих переключатели, и три группы. Последний переключатель внизу каждого группового окна является частью третьей группы. В этом примере мы нарочно придумали такую компоновку, однако в реальности могут существовать задачи, когда нужно будет безболезненно отделять определенный переключатель, чтобы он не утратил членства в группе. Совет. Для упаковки переключателей нет необходимости применять контейнер GroupBox, хотя, как правило, именно он и используется. Этот контейнер отображает рамку и надпись, которую можно применить к вашей группе кнопок.
Контекстные окна указателя WPF предлагает гибкую модель работы с контекстными окнами указателя (tooltip — желтые прямоугольники, которые появляются, когда вы наводите указатель мыши на что-то, интересующее вас). Поскольку контекстные окна указателя в WPF относятся к группе элементов управления содержимым, вы можете поместить в контекстное окно буквально все. Вы можете также настроить различные временные параметры, чтобы задать частоту появления и исчезновения контекстных окон указателя. Самый простой способ показать контекстное окно указателя состоит не в том, чтобы напрямую использовать класс ToolTip, а в определении свойства ToolTip вашего элемента. Свойство ToolTip определено в классе FramworkElement, поэтому он доступен везде, где вы поместите его в окне WPF. Например, ниже показана кнопка с базовым контекстным окном указателя: I have a tooltip
Когда вы наведете на нее указатель мыши, то в знакомом вам желтом окошке появится текст: “This is my tooltip”.
Book_Pro_WPF-2.indb 202
19.05.2008 18:10:01
Классические элементы управления
203
Если вы хотите создать более амбициозное содержимое контекстного окна указателя (например, комбинацию вложенных элементов), вам нужно разбить свойство ToolTip на отдельные элементы. Ниже показан пример, в котором задается свойство ToolTip с помощью более сложного вложенного содержимого: Image and text Image and text I have a fancy tooltip
Как и в предыдущем примере, WPF неявно создает объект ToolTip. Разница заключается в том, что в данном случае объект ToolTip содержит панель StackPanel, а не простую строку. На рис. 7.3 показан результат.
Рис. 7.3. Контекстное окно указателя с улыбающимся лицом Если несколько окон указателя будут перекрывать друг друга, то выиграет специальное контекстное окно указателя. Например, если вы добавите контекстное окно указателя в контейнер StackPanel в предыдущем примере, то это окно появится, когда вы наведете указатель мыши на пустое место панели или элемент управления, не имеющий собственного контекстного окна указателя. На заметку! Не помещайте в контекстное окно указателя элементы управления, поддерживающие интерактивную связь с пользователями, поскольку окно ToolTip не сможет получить фокус. Например, если вы поместите кнопку в элемент ToolTip, эта кнопка будет отображаться, но на ней нельзя будет щелкнуть. (Если вы попытаетесь щелкнуть на ней, ваш щелчок будет принят находящимся за ним окном.) Если вам нужно окно, похожее на контекстное окно указателя, которое могло бы содержать в себе другие элементы управления, попробуйте воспользоваться элементом Popup, который вкратце будет рассмотрен в этой главе.
Book_Pro_WPF-2.indb 203
19.05.2008 18:10:01
204
Глава 7
Настройка параметров контекстного окна указателя В предыдущем примере было показано, как можно настроить содержимое контекстного окна указателя. А что делать, если вам нужно сконфигурировать другие параметры, связанные с работой контекстного окна указателя? На этот случай есть два варианта. Первый — вы можете явно определить объект ToolTip. Это даст вам шанс напрямую задать разнообразные свойства ToolTip. ToolTip — это элемент управления содержимым, поэтому вы можете настроить стандартные свойства, такие как Background (чтобы сменить желтый фоновый цвет), Padding и Font. Вы можете также изменить свойства, определенные в классе ToolTip (они перечислены в табл. 7.2). Большинство из этих свойств предназначено для того, чтобы помочь поместить контекстное окно указателя в то место, которое вам необходимо.
Таблица 7.2. Свойства контекстного окна указателя Имя
Описание
HasDropShadow
Определяет, имеет ли контекстное окно указателя расплывчатую черную тень, которая выделяет его на фоне расположенного за ним окна.
Placement
Определяет, как будет позиционировано контекстное окно указателя, используя одно из значений из перечисления PlacementMode. Значением по умолчанию является Mouse, которое означает, что верхний левый угол контекстного окна указателя будет располагаться относительно текущей позиции указателя мыши. (Действительное положение контекстного окна указателя может быть смещено от начальной точки благодаря свойствам HorizontalOffset и VerticalOffset.) Кроме того, вы можете задавать месторасположение контекстного окна указателя с помощью абсолютных координат экрана или размещать его относительно некоторого элемента (который нужно указать с помощью свойства PlacementTarget).
HorizontalOffset и VerticalOffset
Позволяют задать контекстному окну указателя точное месторасположение. Можно использовать как положительные, так и отрицательные значения.
PlacementTarget
Позволяет поместить контекстное окно указателя относительно другого элемента. Чтобы использовать это свойство, свойство Placement должно иметь одно из следующих значений: Left, Right, Top или Bottom. (Это крайняя точка элемента, по отношению к которому будет производиться выравнивание контекстного окна указателя.)
PlacementRectangle
Позволяет поместить контекстное окно указателя со смещением. Работает точно так же, как и свойства HorizontalOffset и VerticalOffset. Это свойство не будет работать, если свойство Placement будет иметь значение Mouse.
CustomPopupPlacementCallback
Позволяет определять местоположение контекстного окна указателя динамически с помощью кода. Если свойство Placement будет иметь значение Custom, то это свойство будет определять метод, вызываемый элементом ToolTip для получения координат местоположения контекстного окна указателя. Ваш метод обратного вызова получает три порции информации: popupSize (размер ToolTip), targetSize (размер PlacementTarget, если он используется) и offset (точка, которая создается на основе свойств HorizontalOffset и VerticalOffset.). Метод возвращает объект CustomPopupPlacement, который сообщает WPF о том, в каком месте должно находиться контекстное окно указателя.
Book_Pro_WPF-2.indb 204
19.05.2008 18:10:01
Классические элементы управления
205
Окончание табл. 7.2 Имя
Описание
StaysOpen
Не имеет никакого практического эффекта. Это свойство предназначено для того, чтобы позволить создать контекстное окно указателя, остающееся открытым до тех пор, пока пользователь не щелкнет еще где-нибудь. Однако свойство ToolTipService. ShowDuration перекрывает свойство StaysOpen. Как результат, контекстные окна указателей исчезают по истечении предварительно заданного промежутка времени (обычно по истечении 5 секунд), или когда пользователь переместит указатель мыши в сторону. Если вы хотите создать окно, которое будет подобно контекстному окну указателя, и которое будет оставаться открытым в течение неопределенного промежутка времени, то самым простым подходом является использование элемента управления Popup.
С помощью свойств ToolTip следующая разметка создает контекстное окно указателя, которое не имеет тени, но использует прозрачный красный фон, который позволяет видеть находящееся за ним окно (и элементы управления, имеющиеся в нем): Image and text Image and text I have a fancy tooltip
В большинстве случаев вам будет достаточно использовать стандартное размещение контекстного окна указателя — в текущей позиции указателя мыши. Тем не менее, разнообразные свойства ToolTip предлагают множество других вариантов размещения. Ниже перечислены стратегии, согласно которым вы можете определить местоположение контекстного окна указателя.
• Привязка к текущей позиции указателя мыши. Это стандартный способ, который основан на том, что свойству Placement присваивается значение Mouse. Левый верхний угол контекстного окна указателя присоединяется к левому верхнему углу невидимого “ограничивающего прямоугольника” указателя мыши.
• Привязка к позиции элемента, на который наведен указатель мыши. Свойству Placement присваивается значение Left, Right, Top, Bottom или Center, в зависимости от того, где находится край элемента, который вы хотите использовать для привязки. Левый верхний угол контекстного окна указателя будет присоединен к этому краю.
• Привязка к позиции другого элемента (или окна). Свойство Placement задается точно так же, как если бы вы присоединяли контекстное окно указателя к текущему элементу. (Используются значения Left, Right, Top или Center.) Затем выбирается элемент путем задания свойства PlacementTarget. Не забывайте использовать синтаксис {Binding ElementName=Имя}, чтобы обозначить элемент, который вы хотите использовать.
Book_Pro_WPF-2.indb 205
19.05.2008 18:10:01
206
Глава 7
• Определение смещения. Используется одна из вышеперечисленных стратегий, а также определяются свойства HorizontalOffset и VerticalOffset, с помощью которых можно получить небольшое дополнительное пространство.
• Использование абсолютных координат. Свойству Placement присваивается значение Absolute, а с помощью свойств HorizontalOffset и VerticalOffset (или PlacementRectangle) задается пространство между контекстным окном указателя и левым верхним углом окна.
• Осуществление расчетов во время выполнения. Свойству Placement присваивается значение Custom. С помощью свойства CustomPopupPlacementCallback определяется созданный вами метод. На рис. 7.4 показаны разные способы расположения контекстного окна указателя. Обратите внимание, что при расположении контекстного окна указателя в одну линию с нижним или правым краем элемента образуется пустое пространство. Его появление объясняется способом измерения содержимого элементом ToolTip. Относительно указателя мыши
Относительно элемента, со смещением
Относительно стороны элемента Tooltip
Tooltip
Tooltip
Кнопка
Tooltip
Tooltip
Кнопка VerticalOffset HorizontalOffset
Tooltip
Рис. 7.4. Схемы расположения контекстного окна указателя (Tooltip)
Настройка свойств ToolTipService Существуют некоторые свойства контекстного окна указателя, которые нельзя сконфигурировать с помощью свойств класса ToolTip. Для этой цели нужно прибегнуть к услугам другого класса — ToolTipService. Он позволяет конфигурировать временные задержки, связанные с отображением контекстного окна указателя. Все свойства этого класса являются прикрепленными свойствами, поэтому вы можете задавать их прямо в дескрипторе элемента управления, как показано ниже: ...
Класс ToolTipService определяет много тех же свойств, что и класс ToolTip. А это значит, что при работе с контекстными окнами указателя, содержащими только текст, вы можете применять очень простой синтаксис. Вместо того чтобы добавлять вложенный элемент ToolTip, вы можете задать все, что вам необходимо, с помощью атрибутов: I have a tooltip
Свойства класса ToolTipService перечислены в табл. 7.3. В этом классе определены также два маршрутизируемых события: ToolTipOpening и ToolTipClosing. Вы можете реагировать на эти события, чтобы заполнить контекстное окно указателя оперативным содержимым, или же для того, чтобы переопределить способ работы контекстного окна указателя. Например, если в каждом из этих событий вы установите флаг handled, контекстные окна указателя больше не будут отображаться или скроются автоматически. А показать и скрыть их вручную можно с помощью свойства IsOpen.
Book_Pro_WPF-2.indb 206
19.05.2008 18:10:01
Классические элементы управления
207
Совет. Рекомендуется дублировать одни и те же параметры контекстного окна указателя для нескольких элементов управления. Если вы планируете настроить способ обработки контекстных окон указателя во всем приложении, используйте стили, чтобы настройки применялись автоматически, о чем будет рассказано в главе 12. К сожалению, значения свойства ToolTipService не наследуются, а это значит, что если вы зададите их на уровне окна или контейнера, они не будут распространяться на вложенные элементы.
Таблица 7.3. Свойства класса ToolTipService Имя
Описание
InitialShowDelay
Задает временную задержку (в миллисекундах), по истечении которой контекстное окно указателя будет отображено, если указатель будет наведен на элемент.
ShowDuration
Задает промежуток времени (в миллисекундах), в течение которого будет отображаться контекстное окно указателя, а затем исчезнет с экрана, если пользователь не будет перемещать указатель мыши.
BetweenShowDelay
Задает временное окно (в миллисекундах), в течение которого пользователь может переходить от одного контекстного окна указателя к другому без задержки, определяемой свойством InitialShowDelay. Например, если свойство BetweenShowDelay будет иметь значение 5000, то у пользователя будет пять секунд на то, чтобы навести указатель мыши на другой элемент управления, имеющий контекстное окно указателя. Если пользователь наведет указатель мыши на другой элемент управления в течение этих пяти секунд, новое контекстное окно указателя появится без замедления. Если же пользователь потратит больше пяти секунд, в действие вступит InitialShowDelay. В этом случае второе контекстное окно указателя отобразится по истечении периода времени, указанного в свойстве InitialShowDelay.
ToolTip
Задает содержимое контекстного окна указателя. Установка свойства ToolTipService.ToolTip эквивалентно заданию свойства FrameworkElement.ToolTip элемента.
HasDropShadow
Определяет, будет ли контекстное окно указателя иметь тень, выделяющую его на фоне находящегося за ним окна.
ShowOnDisabled
Определяет поведение контекстного окна указателя, если связанный с ним элемент отключен. Если это свойство имеет значение true, контекстное окно указателя будет отображаться для отключенных элементов (т.е. элементов, свойство IsEnabled которых имеет значение false). По умолчанию этому свойству присваивается значение false, в результате чего контекстное окно указателя отображается только в том случае, если связанный с ним элемент управления является активным.
Placement, PlacementTarget, PlacementRectangle, HorizontalOffset и VerticalOffset
Позволяют управлять местоположением контекстного окна указателя. Эти свойства работают точно так же, как и аналогичные им свойства класса ToolTip.
IsEnabled и IsOpen
Позволяют управлять контекстным окном указателя в коде. IsEnabled дает возможность временно отключить ToolTip, а IsOpen позволяет программным образом показать или скрыть контекстное окно указателя (или просто проверить, открыто ли оно).
Book_Pro_WPF-2.indb 207
19.05.2008 18:10:01
208
Глава 7
Элемент управления Popup Элемент управления Popup имеет много общего с элементом ToolTip, хотя ни один из них не является наследником другого. Как и ToolTip, элемент Popup может включать порцию содержимого, которое может нести в себе любой элемент WPF. (Это содержимое хранится в свойстве Popup.Child, а не в свойстве ToolTip.Content.) Как и в элементе управления ToolTip, содержимое Popup может распространяться за пределы окна. И, наконец, параметры местоположения элемента Popup можно задать с помощью тех же свойств, а показать и скрыть его можно с помощью того же свойства IsOpen. Различия между элементами Popup и ToolTip очень важные.
• Popup никогда не отображается автоматически. Чтобы этот элемент управления отобразился на экране, вы должны заранее об этом позаботиться.
• Свойство Popup.StaysOpen по умолчанию имеет значение true, поэтому элемент управления Popup не исчезнет с экрана до тех пор, пока вы явным образом не присвоите свойству Popup.StaysOpen значение false. Если вы присвоите свойству StaysOpen значение false, элемент управления Popup исчезнет с экрана, как только вы щелкнете где-нибудь на экране. На заметку! Всплывающее окно, остающееся открытым, может надоесть пользователю, поскольку оно ведет себя наподобие отдельного автономного окна. Если вы отведете указатель мыши в сторону от него, это окно останется зафиксированным в его исходной позиции. Такого поведения нет ни у элемента ToolTip, ни у Popup, у которых свойство StaysOpen имеет значение false. Как только вы щелкнете кнопкой мыши, контекстное окно указателя или всплывающее окно исчезнут с экрана.
• Элемент управления Popup имеет свойство PopupAnimation, которое позволяет управлять отображением упомянутого элемента управления, когда его свойство
IsOpen имеет значение true. Данное свойство может принимать значения None (присваивается по умолчанию), Fade (постепенное увеличение непрозрачности всплывающего окна), Scroll (непрозрачность плавно переходит с левого верхнего угла окна, пока позволяет пространство) и Slide (окно скользит на свое место, пока позволяет пространство). Чтобы любой из этих анимационных эффектов мог работать, необходимо присвоить свойству AllowsTransparency значение true.
• Элемент управления Popup может принимать фокус. Таким образом, вы можете помещать в него элементы управления, поддерживающие интерактивную связь с пользователем (например, Button). Эта возможность является одной из ключевых причин использования элемента Popup вместо ToolTip.
• Элемент управления Popup определен в пространстве имен System.Windows. Controls.Primitives, так как он чаще всего используется в качестве строительного блока для более сложных элементов управления. Вы обнаружите, что Popup не является таким изящным, как другие элементы управления. Например, вы должны задавать свойство Background, если хотите видеть содержимое, поскольку оно не наследуется от вашего окна, и вам самостоятельно придется добавлять рамку (элемент Border для этой цели подходит как нельзя лучше). Поскольку элемент управления Popup нужно отображать вручную, вы можете вообще создавать его полностью в коде. Однако его можно с той же легкостью определить и в разметке XAML — нужно лишь не забыть включить свойство Name, чтобы вы могли манипулировать ним в коде.
Book_Pro_WPF-2.indb 208
19.05.2008 18:10:02
Классические элементы управления На рис. 7.5 показан пример. Когда пользователь наводит указатель мыши на подчеркнутое слово, появляется всплывающее окно с дополнительной информацией и ссылка, которая открывает окно Web-браузера. Чтобы создать это окно, вам нужно включить элемент управления TextBlock с исходным текстом и элемент управления Popup с дополнительным содержимым, которое вы будете отображать, когда пользователь наведет указатель мыши в нужное место. С технической точки зрения нет разницы в том, где будет определен тег Popup, поскольку он не связан ни с одним определенным элементом управления. Вместо этого вы должны определить свойства месторасположения элемента управления Popup. В данном примере всплывающее окно появляется в текущей позиции указателя мыши:
209
Рис. 7.5. Всплывающее окно с гиперссылкой
You can use a Popup to provide a link for a specific term of interest. For more information, see Wikipedia
В этом примере представлены два элемента, которых вы могли раньше не видеть. Элемент Run позволяет применить форматирование к специфической части элемента управления TextBlock — это порция содержимого потока (об этом мы поговорим в главе 19, когда будем рассматривать документы). Hyperlink позволяет задать текст, который может реагировать на щелчок кнопкой мыши, произведенный на нем. Об этом речь пойдет в главе 9, когда будут рассматриваться приложения со страничной организацией. К оставшимся деталям относится относительно простой код, который показывает элемент управления Popup, когда мышь будет наведена на заданное слово, и код, который открывает Web-браузер при щелчке на ссылке: private void run_MouseEnter(object sender, MouseEventArgs e) { popLink.IsOpen = true; } private void lnk_Click(object sender, RoutedEventArgs e) { Process.Start(((Hyperlink)sender).NavigateUri.ToString()); }
На заметку! Вы можете показать и скрыть элемент управления Popup с помощью триггера — действия, которое происходит автоматически, когда определенное свойство получает определенное значение. Вам просто нужно создать триггер, который будет реагировать, когда свойство Popup.IsMouseOver получит значение true, и присвоить свойству Popup.IsOpen значение true. Более подробно об этом мы будем говорить в главе 12.
Book_Pro_WPF-2.indb 209
19.05.2008 18:10:02
210
Глава 7
Текстовые элементы управления WPF включает три текстовых элемента управления: TextBox , RichTextBox и PasswordBox. Элемент PasswordBox является прямым наследником класса Control. Элементы управления TextBox и RichTextBox являются наследниками класса TextBase. В отличие от рассмотренных нами элементов управления содержимым, текстовые окна могут заключать в себе только ограниченный тип содержимого. TextBox всегда хранит строку (она определяется в свойстве Text). PasswordBox тоже содержит строку текста (она определяется в свойстве Password), однако использует SecureString для защиты от некоторых типов атак. Только элемент RichTextBox может хранить содержимое более сложного порядка: FlowDocument, которое может содержать в себе сложную комбинацию элементов. В следующих разделах мы рассмотрим основные возможности TextBox. В завершение разговора мы рассмотрим средства безопасности PasswordBox. На заметку! RichTextBox является усовершенствованным элементом управления, предназначенным для отображения объектов FlowDocument. Вы узнаете, как он используется, в главе 19.
Множество строк текста Как правило, элемент управления TextBox хранит одну строку текста. (Вы можете ограничить допустимое количество символов с помощью свойства MaxLength.) Однако часто бывают случаи, когда приходится создавать многострочное текстовое окно для работы с большим объемом содержимого. Для этой цели нужно воспользоваться свойством TextWrapping, присвоив ему значение Wrap или WrapWithOverflow. При значении Wrap текст будет всегда обрываться на краю элемента управления, даже если при этом слишком большое слово будет разбито на два. WrapWithOverflow позволяет растянуть несколько строк за границы правого края, если алгоритм разрыва строки не может найти подходящее место (например, пробел или дефис) для разбиения строки. Чтобы на самом деле увидеть в текстовом окне множество строк, оно должно иметь подходящие размеры. Вместо того чтобы задавать жестко кодированную высоту (которая не подойдет для разных размеров шрифтов и может вызвать проблемы с компоновкой), вы можете использовать свойства MinLines и MaxLines. Свойство MinLines определяет минимальное количество строк, которые должны отображаться в текстовом окне. Например, если этому свойству присвоить значение 2, то текстовое окно примет высоту, равную высоте минимум двух строк текста. MaxLines задает максимальное количество отображаемых строк. Даже если текстовое окно будет расширено до таких размеров, чтобы уместиться в своем контейнере (например, строка Grid с пропорциональными размерами или последний элемент в DockPanel), оно не превысит заданный лимит. На заметку! Свойства MinLines и MaxLines не влияют на количество содержимого, которое вы можете поместить в текстовом окне. Они просто помогают придать подходящие размеры текстовому окну. В своем коде вы можете проверить свойство LineCount, чтобы узнать точно, сколько строк умещается в текстовом окне. Если ваше текстовое окно поддерживает укладку текста, то нужно позаботиться о том, чтобы пользователь мог вводить больше текста, чем может быть отображено в видимых строках. По этой причине обычно имеет смысл добавить постоянно отображаемую линейку прокрутки (или отображаемую по запросу), присвоив свойству Vertical ScrollBarVisibility значение Visible или Auto. (Вы можете также задать свойство
Book_Pro_WPF-2.indb 210
19.05.2008 18:10:02
Классические элементы управления
211
HorizontalScrollBarVisibility, чтобы отображать реже используемую горизонтальную полосу прокрутки.) Иногда приходится делать так, чтобы пользователь мог вводить жесткие возвраты в многострочном текстовом окне, нажимая клавишу . (Обычно при нажатии клавиши в текстовом окне активизируется кнопка, используемая по умолчанию.) Чтобы текстовое окно поддерживало клавишу , присвойте свойству AcceptsReturn значение true. Можно также задать свойство AcceptsTab, чтобы пользователь мог вставлять символы табуляции. В противном случае при нажатии клавиши будет передан фокус следующему элементу управления, заданному в последовательности перехода с помощью клавиши табуляции. Совет. Класс TextBox включает также набор методов, которые позволяют программно перемещаться по текстовому содержимому небольшими или крупными шагами. К этим методам относятся LineUp(), LineDown(), PageUp(), PageDown(), ScrollToHome(), ScrollToEnd() и ScrollToLine(). Иногда вы будете создавать текстовые окна исключительно для отображения текста. В этом случае свойству IsReadOnly потребуется присвоить значение true, чтобы исключить возможность редактирования текста в поле. Этот прием предпочтительнее блокирования текстового окна путем присваивания свойству IsEnabled значения false, поскольку заблокированное текстовое окно отображает текст, выделенный серым цветом (такой текст трудно читать), не поддерживает выделение текста (или копирование в буфер обмена) и его прокрутку.
Выделение текста Как известно, вы можете выделять текст в любом текстовом окне, щелкая кнопкой мыши и перемещая ее указатель, или, удерживая нажатой клавишу , выделять его с помощью клавиш управления курсором. Класс TextBox дает возможность определять или изменять выделенный в данный момент текст программным образом, используя свойства SelectionStart, SelectionLength и SelectedText. Свойство SelectionStart определяет позицию, начиная с нуля, в которой будет осуществляться выделение текста. Например, если этому свойству присвоить значение 10, то первым Рис. 7.6. Выделение текста выделенным символом будет одиннадцатый символ в текстовом окне. Свойство SelectionLength задает общее количество выделенных символов. (Значение, равное нулю, свидетельствует о том, что не было выделено ни одного символа.) И, наконец, свойство SelectedText позволяет быстро проверить или изменить выделенный текст в текстовом окне. Вы можете отреагировать на изменение выделения с помощью события SelectionChanged. На рис. 7.6 показан пример, который реагирует на это событие и отображает информацию о текущем выделении текста.
Book_Pro_WPF-2.indb 211
19.05.2008 18:10:02
212
Глава 7
Класс TextBox включает свойство AutoWordSelection, позволяющее управлять поведением выделения. Если ему присвоить значение true, то в текстовом окне будет выделяться по одному слову одновременно.
Другие возможности элемента управления TextBox Элемент управления TextBox обладает еще несколькими специализированными возможностями. Наиболее интересной является проверка орфографии, при которой нераспознанные слова подчеркиваются волнистой линией красного цвета. Пользователь может щелкнуть правой кнопкой мыши на нераспознанном слове и выбрать из списка правильный вариант, как показано на рис. 7.7.
Рис. 7.7. Проверка орфографии текста в текстовом окне Чтобы добавить функцию проверки орфографии в элемент управления TextBox, вам нужно задать свойство зависимостей SpellCheck.IsEnabled, как показано ниже: ...
Функция проверки орфографии является специфической для WPF и не зависит от любого другого программного обеспечения (например, Office). Функция проверки орфографии определяет необходимый словарь на основании выбранного пользователем языка ввода на клавиатуре. Выбрать словарь можно с помощью свойства Language элемента управления TextBox, которое происходит от класса FrameworkElement, или посредством задания атрибута xml:lang в элементе . К сожалению, функцию проверки орфографии никак нельзя настроить. Она содержит только одно дополнительное свойство (SpellingReform), которое определяет, будут ли применяться изменения в правилах орфографии французского и немецкого языков, датируемые 1990 г. Другой полезной функцией является Undo, которая позволяет отменить последние изменения. Функцию Undo можно реализовать программно (посредством метода Undo()), с помощью комбинации клавиш , а также при условии, что свойство CanUndo не будет иметь значение False. Совет. Программно манипулируя текстом в текстовом окне, вы можете использовать методы BeginChange() и EndChange(), чтобы сгруппировать серию действий, которые TextBox обработает как один “блок” изменений. Впоследствии эти действия можно будет отменить за один раз.
Book_Pro_WPF-2.indb 212
19.05.2008 18:10:02
Классические элементы управления
213
Элемент управления PasswordBox Элемент управления PasswordBox выглядит подобно элементу управления TextBox, однако он отображает строку, содержащую символы-кружочки, скрывающие собой настоящие символы. (С помощью свойства PasswordChar можно выбрать другую маску символов.) Кроме того, PasswordBox не поддерживает работу с буфером обмена, поэтому вы не сможете скопировать текст, который содержится в этом элементе управления. По сравнению с TextBox , класс PasswordBox имеет более простой интерфейс. Как и класс TextBox, он предлагает свойство MaxLength, методы Clear(), Paste() и SelectAll(), а также событие PasswordChanged, которое возникает в случае изменения текста. Главное отличие этого элемента управления от TextBox кроется внутри. Несмотря на то что вы можете задать текст и прочитать его как обычную строку с помощью свойства Password, внутренне элемент управления PasswordBox использует исключительно объект System.Security.SecureString. SecureString — это исключительно текстовый объект, подобный обычной строке. Разница заключается в способе его хранения в памяти. SecureString хранится в памяти в зашифрованном виде. Ключ, который используется для расшифровки строки, генерируется псевдослучайным образом и хранится в порции памяти, которая никогда не записывается на диск. Поэтому даже если произойдет поломка вашего компьютера, злоумышленник не сможет проверить страничный файл, чтобы извлечь данные пароля. В самом лучшем случае он найдет всего лишь зашифрованную форму. Класс SecuteString также имеет средство освобождения по запросу. Когда вы вызываете метод SecureString.Dispose(), данные пароля, находящиеся в памяти, перезаписываются. Это дает гарантию, что вся информация о пароле будет стерта из памяти, и никто не сможет ею воспользоваться. Как вы могли предположить, PasswordBox вызывает метод Dispose() для хранимой внутри строки SecureString при уничтожении элемента управления.
Элементы управления списками WPF включает многие элементы управления, которые могут работать с коллекцией элементов, начиная с простых элементов управления ListBox и ComboBox, которые будут рассмотрены здесь, и заканчивая более специализированными элементами управления, такими как ListView, TreeView и ToolBar, которые будут рассмотрены в последующих главах. Все эти элементы управления являются потомками класса ItemsControl (он, в свою очередь, является потомком класса Control). Класс ItemsControl является базовым для всех элементов управления, которые имеют дело со списками. Он предлагает два способа заполнения списка элементов. Наиболее простым способом является добавление элемента прямо в коллекцию Items, при котором используется код или XAML. Однако в WPF чаще всего применяется метод привязки данных. В этом случае вы присваиваете свойству ItemsSource объект, имеющий коллекцию элементов данных, которые вы хотите отобразить. (О процессе привязки данных речь пойдет в главе 16.) Иерархия, которая начинается с ItemsControls, является немного запутанной. Так, важная ветвь отведена селекторам, к которым относятся ListBox, ComboBox и TabControl. Эти элементы управления являются потомками класса Selector и имеют свойства, позволяющие отслеживать выделенный в данный момент времени элемент (SelectedItem) или его позицию (SelectedIndex). Отдельно от них определены элементы управления, связанные с работой со списками элементов, но не поддерживающие выделения. К ним относятся классы для меню, панелей инструментов и деревьев — все они представляют собой наследники ItemsControls, но не являются селекторами.
Book_Pro_WPF-2.indb 213
19.05.2008 18:10:02
214
Глава 7
Чтобы разблокировать большинство возможностей любого элемента ItemsControl, необходимо использовать привязку данных. Это нужно делать даже тогда, когда вы не производите выборку данных из базы или даже из внешнего источника данных. Привязка данных в WPF обычно без проблем справляется с данными в различных формах, включая специальные объекты данных и коллекции. Однако пока что мы не будем рассматривать подробности привязки данных. Сейчас мы лишь поверхностно рассмотрим элементы управления ListBox и ComboBox.
Элемент управления ListBox Классы ListBox и ComboBox представляют два общих средства в среде Windows — списки переменной длины, которые дают пользователю возможность выбирать элемент. На заметку! Класс ListBox тоже допускает множественный выбор, если его свойству SelectionMode присвоить значение Multiple или Extended. В режиме Extended вам необходимо удерживать нажатой клавишу , чтобы выбрать дополнительные элементы, или клавишу , чтобы выбрать диапазон элементов. В любом типе списка с множественным выбором для получения всех выделенных элементов используется коллекция SelectedItems вместо свойства SelectedItem. Чтобы добавить элементы в ListBox, можно вложить элементы ListBoxItem в элемент управления ListBox. Например, ниже показан элемент управления ListBox, который содержит список цветов: Green Blue Yellow Red
Если помните, то в главе 2 мы говорили, что разные элементы управления обрабатывают вложенное содержимое по-разному. ListBox хранит каждый вложенный объект в своей коллекции Items. ListBox является достаточно гибким элементом управления. Он может хранить не только объекты ListBoxItem, но и любой произвольный элемент. Это возможно благодаря тому, что класс ListBoxItem является наследником класса ContentControl, который позволяет хранить отдельную порцию вложенного содержимого. Если эта порция содержимого является классом, происходящим от UIElement, то она будет визуализирована в элементе управления ListBox. Если же она представляет собой другой тип объекта, ListBox вызовет метод ToString() и отобразит результирующий текст. Например, если вам нужно создать список с изображениями, используйте следующую разметку:
ListBox сам способен создавать необходимые ему объекты ListBoxItem. Это означает, что вы можете поместить ваши объекты прямо внутрь элемента ListBox. Ниже представлен пример, в котором вложенные объекты StackPanel используются для комбинирования текста и изображений:
Book_Pro_WPF-2.indb 214
19.05.2008 18:10:03
Классические элементы управления
215
A happy face A warning sign A happy face
В этом примере StackPanel становится элементом, встроенным в элемент управления ListBoxItem. Эта разметка создает расширенный список, как показано на рис. 7.8.
Рис. 7.8. Список изображений
На заметку! Следует отметить, что в данном примере цвет текста при выделении элемента не изменяется. В этом нет ничего хорошего, поскольку черный текст на синем фоне прочитать очень трудно. Чтобы решить эту проблему, вам нужно использовать шаблон данных (см. главу 17). Способность вкладывать произвольные элементы внутрь текстового окна дает возможность создавать разнообразные элементы управления списками, не используя при этом другие классы. Например, инструментальное средство Windows Forms включает класс CheckedListBox, отображаемый как список, в котором напротив каждого элемента ставится флажок. В WPF не нужен никакой специальный класс, поскольку вы можете быстро создать его с помощью стандартного элемента управления ListBox: Option 1 Option 2
При использовании списка, вмещающего в себе разные элементы, следует иметь в виду, что когда вы считываете значение SelectedItem (а также коллекции SelectedItems и Items), вы не увидите объекты ListBoxItem — вместо них вы увидите любые объекты, помещенные вами в список. В примере с элементом управления CheckedListBox это означает, что SelectedItem представляет объект CheckBox. Например, ниже показан код, который реагирует на возникновение события SelectionChanged. Затем он получает выделенный в данный момент CheckBox и показывает, был ли этот элемент отмечен: private void lst_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (lst.SelectedItem == null) return; txtSelection.Text = String.Format( "You chose item at position {0}.\r\nChecked state is {1}.", lst.SelectedIndex, ((CheckBox)lst.SelectedItem).IsChecked); }
Book_Pro_WPF-2.indb 215
19.05.2008 18:10:03
216
Глава 7
Совет. Если вы хотите найти выделенный в данный момент элемент, вы можете прочитать его в свойстве SelectedItem или SelectedItems, как показано здесь. Если вы хотите определить, какой элемент (если такой вообще существует) не был выделен, вы можете применять свойство RemovedItems объекта SelectionChangedEventArgs. Аналогично, свойство AddItems сообщает о том, какие элементы были добавлены в выбор. В режиме выбора одного элемента всегда будет добавляться один элемент, и удаляться один элемент при каждом изменении выбора. В режиме множественного выбора или в расширенном режиме так будет не всегда. В следующем фрагменте кода происходит циклический перебор коллекции элементов, что поможет узнать, какие из них были отмечены. (Вы можете написать похожий код, который будет перебирать коллекцию выделенных элементов в списке множественного выбора с флажками.) private void cmd_ExamineAllItems(object sender, RoutedEventArgs e) { StringBuilder sb = new StringBuilder(); foreach (CheckBox item in lst.Items) { if (item.IsChecked == true) { sb.Append(item.Content); sb.Append(" is checked."); sb.Append("\r\n"); } } txtSelection.Text = sb.ToString(); }
На рис. 7.9 показано окно списка, в котором используется этот код. Помещая вручную элементы в список, вы должны сами решить, будут ли элементы помещены как есть, или же их нужно упаковать в объект ListBoxItem. Второй подход часто является понятным, хотя и трудоемким. Самое важное в этом деле — быть последовательным. Например, если вы поместите объекты StackPanel в список, то объектом ListBox.SelectedItem будет StackPanel. Если вы поместите объекты StackPanel, упакованные объектами ListBoxItem, то объектом ListBox. SelectedItem будет ListBoxItem, отсюда и соответствующий код. ListBoxItem обладает еще одной особенностью: Рис. 7.9. Список флажков в нем определено свойство IsSelected, значение которого можно считывать (или устанавливать), и события Selected и Unselected, которые информируют о том, когда был выделен данный элемент. Однако эти же возможности вы можете реализовать с помощью членов класса ListBox, таких как свойство SelectedItem (или SelectedItems) и событие SelectionChanged. Интересно, что существует технология извлечения упаковщика ListBoxItem для специфического объекта, когда вы применяете подход с вложенными объектами. Хитрость заключается в вызове метода ContainerFromElement(). Ниже показан код, который с помощью этой технологии проверяет, был ли выделен первый элемент в списке: ListBoxItem item = (ListBoxItem)lst.ContainerFromElement( (DependencyObject)lst.SelectedItems[0]); MessageBox.Show("IsSelected: " + item.IsSelected.ToString());
Book_Pro_WPF-2.indb 216
19.05.2008 18:10:03
Классические элементы управления
217
Элемент управления ComboBox Элемент управления ComboBox подобен элементу управления ListBox. Он хранит коллекцию объектов ComboBoxItem, которые создаются явным или неявным образом. Как и ListBoxItem, ComboBoxItem является элементом управления содержимым, который может хранить любой вложенный элемент. Ключевым отличием классов ComboBox и ListBox является способ их визуализации в окне. Элемент управления ComboBox использует раскрывающийся список, а это значит, что пользователь может выбрать только один элемент за один раз. Если вы хотите сделать так, чтобы пользователь мог вводить текст в комбинированном окне для выбора элемента, вы должны присвоить свойству IsEditable значение true. Кроме того, вы должны убедиться, что храните обычные, только текстовые объекты ComboBoxItem, или объект, обеспечивающий значащую репрезентацию ToString(). Например, если вы заполните редактируемое комбинированное окно объектами Image, то текст, который появится в верхней части, будет представлять полностью определенное имя класса Image. Элемент управления ComboBox имеет одно ограничение в виде способа подгонки размеров, когда вы используете автоматический выбор размеров. ComboBox выбирает для себя такую ширину, которая позволит уместить его содержимое; это означает, что когда вы будете переходить от одного элемента к другому, ComboBox будет выбирать подходящий размер. К сожалению, не существует способа заставить ComboBox принять размер наибольшего элемента. Вместо этого вам нужно задать жестко закодированное значение свойства Width, что, понятное дело, не является идеальным решением.
Элементы управления, основанные на диапазонах значений WPF включает три элемента управления, использующих концепцию диапазонов (range). Эти элементы принимают числовое значение, которое находится в диапазоне между заданными минимальным и максимальным значениями. Эти элементы управления — ScrollBar, ProgressBar и Slider — являются наследниками класса RangeBase (который, в свою очередь, является наследником класса Control). Однако, несмотря на то, что они вместе используют одну абстракцию (диапазон), работают они по-разному. Класс RangeBase определяет свойства, перечисленные в табл. 7.4. Как правило, элемент управления ScrollBar не используется напрямую. Чаще всего применяется элемент управления более высокого уровня ScrollViewer, который обладает свойствами двух элементов управления ScrollBar . (Элемент управления ScrollViewer рассматривался в главе 5.) Однако по своей сути более ценными являются Slider и ProgressBar.
Элемент управления Slider Элемент управления Slider является более специализированным элементом управления, который временами оказывается полезным. Например, им можно воспользоваться для задания числовых значений в тех ситуациях, когда само число не является особенно важным. Например, громкость в проигрывателе лучше всего устанавливать, перемещая в разные стороны бегунок на линейке прокрутки. Общая позиция бегунка показывает относительную громкость (нормально, тихо, громко), а лежащее в его основе число не представляет особого смысла для пользователя. Ключевые свойства элемента управления Slider определены в классе RangeBase. Кроме них, вы можете использовать все свойства, перечисленные в табл. 7.5.
Book_Pro_WPF-2.indb 217
19.05.2008 18:10:03
218
Глава 7
Таблица 7.4. Свойства класса RangeBase Имя
Описание
Value
Это текущее значение элемента управления (которое должно находиться в диапазоне между минимумом и максимумом). По умолчанию оно начинается с 0. Вопреки тому, что вы могли предположить, значение Value не является целочисленным — оно имеет тип double, что дает ему возможность принимать дробные значения. Вы можете реагировать на событие ValueChanged, если хотите получать уведомления об изменении значения.
Maximum
Верхний лимит (максимальное допустимое значение).
Minimum
Нижний лимит (минимальное допустимое значение).
SmallChange
Величина, на которую уменьшается или увеличивается значение свойства Value при “малом изменении”. Назначение малого изменения зависит от элемента управления (и может вообще не использоваться). Для элементов управления ScrollBar и Slider это величина, на которую изменяется значение, когда вы используете клавиши управления курсором. Для элемента ScrollBar вы можете применять кнопки управления курсором в любом конце линейки.
LargeChange
Величина, на которую уменьшается или увеличивается значение свойства Value в “крупном изменении”. Назначение крупного изменения зависит от элемента управления (и может вообще не использоваться). Для элементов управления ScrollBar и Slider это та величина, на которую изменяется значение, когда вы используете кнопки и , или когда вы щелкнете на линейке в любой стороне ползунка (который отмечает текущую позицию).
Таблица 7.5. Дополнительные свойства в классе Slider Имя
Описание
Orientation
Переключает между вертикальным и горизонтальным ползунком.
Delay и Interval
Управляет скоростью перемещения ползунка вдоль дорожки, когда пользователь щелкает и удерживает нажатой клавишу мыши на любой стороне ползунка. Оба значения задаются в миллисекундах. Delay — это время, по истечении которого ползунок переместится на одну единицу (малое изменение) после вашего щелчка, а Interval — это время, которое должно истечь, прежде чем он продолжит перемещение, если вы продолжите удерживать нажатой кнопку мыши.
TickPlacement
Определяет место появления отметок возле линейки, которые помогают визуализировать шкалу. По умолчанию свойство TickPlacement имеет значение None, вследствие чего ни одна отметка не отображается. Если вы работаете с горизонтальным ползунком, вы можете поместить метки над дорожкой (TopLeft) или под ней (BottomRight). Если вы работаете с вертикальным ползунком, вы можете поместить их слева (TopLeft) или справа (BottomRight). (Имена свойства TickPlacement являются немного запутанными, поскольку два значения покрывают четыре возможных варианта, в зависимости от того, как ориентирован ползунок.)
TickFrequency
Задает интервал между отметками, который определяет, сколько отметок будет появляться. Например, вы можете помещать их через каждые 5 числовых единиц, каждые 10 и т.д.
Ticks
Если вы хотите поместить отметки в определенных, нерегулярных позициях, вы можете использовать коллекцию Ticks. Просто добавьте в эту коллекцию по одному числу (типа double) для каждой отметки. Например, вы можете поместить отметки в позиции 1, 1.5, 2 и 10 на шкале, добавив эти числа.
Book_Pro_WPF-2.indb 218
19.05.2008 18:10:03
Классические элементы управления
219
Окончание табл. 7.5 Имя
Описание
IsSnapToTickEnabled
Если это свойство будет иметь значение true, то при перемещении ползунка он автоматически будет переведен в место, прыгая к ближайшей отметке. По умолчанию это свойство имеет значение false.
IsSelectionRangeEnabled
Если этому свойству будет присвоено значение true, вы сможете использовать диапазон для затенения участка линейки прокрутки. Диапазон выбора позиции задается с помощью свойств SelectionStart и SelectionEnd. Диапазон выбора не имеет значения, тем не менее, вы можете использовать его для любых целей. Например, в проигрывателях иногда используется затененная фоновая линейка, чтобы показать процесс загрузки файла.
На рис. 7.10 сравниваются элементы управления Slider с разными параметрами отметок.
Рис. 7.10. Добавление отметок к ползунку
Элемент управления ProgressBar Элемент управления ProgressBar показывает ход выполнения длительной задачи. В отличие от ползунка, ProgressBar не является интерактивным элементом управления. Наоборот, за изменение значения свойства Value отвечает исключительно ваш код. (Строго говоря, правила WPF предполагают, что ProgressBar не должен быть элементом управления, поскольку он не реагирует на действия мыши или ввод на клавиатуре.) Вы уже видели один пример использования элемента управления ProgressBar в главе 3 — там мы рассматривали окно, в котором задача выполнялась в фоновом потоке. ProgressBar не имеет предварительно заданной высоты, равной двум или трем единицам, зависящим от устройства. Вы должны самостоятельно установить свойство Height (или поместить его в подходящий контейнер с фиксированными размерами), если хотите видеть большую, более традиционную линейку. Элемент управления ProgressBar может служить для отображения “долгоиграющего” индикатора состояния, даже если вы не знаете, насколько долго будет длиться задача. Интересно (и странно), что это делается путем присваивания свойству IsIndeterminate значения true:
Book_Pro_WPF-2.indb 219
19.05.2008 18:10:03
220
Глава 7
После того как вы зададите свойство IsIndeterminate, свойства Minimum, Maximum и Value вам больше не будут нужны. Дело в том, что ProgressBar будет показывать периодический зеленый импульс, перемещающийся слева направо, что является универсальным условием Windows отображения хода выполнения какой-либо задачи. Эта разновидность индикатора имеет определенный смысл в панели состояния приложения. Например, вы можете применять его для того, чтобы показать, что вы соединяетесь с удаленным сервером для передачи информации.
Резюме В этой главе были рассмотрены базовые элементы управления WPF, разделенные на перечисленные ниже категории.
• Элементы управления содержимым, которые могут содержать вложенные элементы, такие как Label, Button и ToolTip.
• Текстовые элементы управления, которые могут хранить обычный текст (TextBox) или пароль (PasswordBox).
• Элементы управления списками, которые содержат коллекцию элементов, такие как ListBox и ComboBox.
• Элементы управления, основанные на диапазонах, которые принимают числовое значение из диапазона, такие как Slider и ProgressBar. В последующих главах мы продолжим исследование элементов управления. В частности, в следующих двух главах вы ознакомитесь с наиболее важными элементами управления верхнего уровня в WPF: Window и Page.
Book_Pro_WPF-2.indb 220
19.05.2008 18:10:03
ГЛАВА
8
Окна О
кна (window) являются основными элементами в любом настольном приложении — настолько “основными”, что в их честь даже была названа операционная система Windows. И хотя в WPF имеется модель для создания навигационных приложений, распределяющих задачи по отдельным страницам, окна все равно остаются преобладающей технологией для создания приложений. В этой главе речь пойдет о классе Window. Тем, кому доводилось ранее программировать с применением набора инструментальных средств Windows Forms, большая часть предлагаемого материала покажется знакомой, потому что класс Window, по сути, является более свободной моделью класса Form. Такие читатели могут бегло просмотреть материал этой главы, обратив внимание только на те детали, которые являются совершенно новыми, вроде непрямоугольных окон и диалоговых окон задач в стиле Vista, и затем сразу же перейти к следующей главе, которая посвящена рассмотрению другого высокоуровневого контейнера (а именно — класса Page) и другого подхода к структуризации приложений (подхода, подразумевающего использование навигации в стиле Web).
Класс Window Как уже рассказывалось в главе 5, класс Window унаследован от класса ContentControl. Это означает, что он может содержать только одного потомка (каковым обычно является контейнер макета наподобие элемента управления Grid) и что его фон можно закрашивать с помощью кисти путем установки свойства Background. Можно еще также использовать и свойства BorderBrush и BorderThickness для добавления вокруг окна границы, но эта граница добавляется внутри оконной рамки (то есть по краю клиентской области). Оконную рамку можно вообще удалять путем установки для свойства WindowStyle значения None, что позволяет создавать полностью настраиваемое (т.е. имеющее специальную форму) окно, о чем более подробно будет рассказываться далее в этой главе, в разделе “Непрямоугольные окна”. На заметку! Клиентская область — это область внутри окна. Именно в ней размещается содержимое. К не клиентской области относится граница и строка заголовка в верхней части окна. За управление этой областью отвечает операционная система. Помимо этого, класс Window имеет небольшой набор членов, которые будут знакомы любому программисту Windows. Наиболее очевидными из них являются свойства, которые касаются внешнего вида и позволяют изменять способ отображения не клиентской части окна. Основные члены класса Window перечислены в табл. 8.1.
Book_Pro_WPF-2.indb 221
19.05.2008 18:10:04
222
Глава 8
Таблица 8.1. Основные свойства класса Window Имя
Описание
AllowsTransparency
Если для свойства AllowsTransparency устанавливается значение true, класс Window позволяет другим окнам “проглядывать” через данное при условии, что для фона установлен прозрачный цвет. Если для него устанавливается значение false (что является поведением по умолчанию), находящееся позади данного окна содержимое не “проглядывается”, и прозрачный цвет фона визуализируется как черный. Это свойство в случае использования вместе с имеющим значение None свойством WindowsStyle позволяет создавать окна, имеющие необычную форму, о чем более подробно будет рассказываться далее в этой главе, в разделе “Непрямоугольные окна”.
Icon
Это свойство представляет объект ImageSource, указывающий на пиктограмму, которая должна использоваться для данного окна. Пиктограммы отображаются в левом верхнем углу окна (если в нем применяется один из стандартных стилей границ), в панели задач (если для свойства ShowInTaskBar установлено значение true) и в окне выбора, которое появляется, когда пользователь нажимает комбинацию клавиш для перехода из одного работающего приложения в другое. Поскольку эти пиктограммы имеют разный размер, в используемом для них файле .ico должны содержаться изображения размером как минимум 16×16 и 32×32 пикселя. На самом деле стандартными для пиктограмм в Vista считаются изображения размером 48×48 и 256×256 (см. страницу по адресу http:// www.axialis.com/tutorials/tutorial-vistaicons.html), размер которых можно изменять в соответствии с существующими требованиями. Если в свойстве Icon содержится ссылка null, окно получает ту же пиктограмму, что и приложение (установить пиктограмму для которого можно в Visual Studio, дважды щелкнув на узле Properties (Свойства) в окне проводника решений Solution Explorer и затем отобразив вкладку Application (Приложение)). Если это свойство вообще пропущено, WPF использует стандартную, но непримечательную пиктограмму с изображением окна.
Top и Left
Эти свойства определяют расстояние между левым верхним углом окна и левыми верхними краями экрана (в аппаратно независимых пикселях). В случае изменения любого из них вызывается событие LocationChanged. Если для свойства WindowStartupPosition задается значение Manual, эти свойства могут устанавливаться до появления окна для определения его позиции. Их также еще можно использовать для изменения позиции окна после того, как оно уже появилось, каким бы ни было значение WindowStartupPosition.
ResizeMode
Это свойство берет значение из перечисления ResizeMode, которое определяет, может ли пользователь изменять размер окна. Этот параметр также еще влияет на видимость кнопок разворачивания и сворачивания окна. Чтобы полностью заблокировать окно, используйте значение NoResize, чтобы разрешить только сворачивать окно — значение CanMinimize, чтобы разрешить все касающиеся изменения размера окна действия — значение CanResize, а чтобы добавить визуальную подсказку, появляющуюся в правом нижнем углу окна и показывающую, что размер окна можно изменять — значение CanResizeWithGrip.
Book_Pro_WPF-2.indb 222
19.05.2008 18:10:04
223
Окна
Окончание табл. 8.1 Имя
Описание
RestoreBounds
Это свойство извлекает информацию о границах окна. Однако если окно в текущий момент находится в развернутом или свернутом состоянии, в этом свойстве отображаются те границы, которые использовались последними перед тем, как окно было развернуто или свернуто. Это свойство является чрезвычайно полезным, когда требуется, чтобы сохранялась информация о позиции и размерах окна, о чем еще будет рассказываться далее в этой главе.
ShowInTaskbar
Если для этого свойства устанавливается значение true, окно отображается в панели задач и списке, появляющемся после нажатия комбинации клавиш . Обычно значение true для этого свойства устанавливается только для главного окна приложения.
SizeToContent
Это свойство позволяет создавать окно, способное автоматически увеличиваться в соответствии с размером содержимого. Значение оно берет из перечисления SizeToContent. Чтобы отключить автоматическое изменение размеров окна, используйте значение Manual, а чтобы разрешить окну увеличиваться в различных направлениях в соответствии с размерами динамического содержимого — значение Height, Width или WidthAndHeight.
Title
В этом свойстве указывается заголовок, который должен отображаться в строке заголовка окна (и в панели задач).
Topmost
Когда для этого свойства устанавливается значение true, данное окно всегда отображается поверх всех остальных окон в приложении (если только у них тоже для этого свойства не установлено значение true). Это очень удобный параметр для палитр, которые обычно должны “плавать” поверх других окон.
WindowStartupLocation
Это свойство берет значение из перечисления WindowStartupLocation. Для размещения окна именно в той позиции, которая указывается в свойствах Left и Top , следует использовать значение Manual, для размещения окна по центру экрана — значение CenterScreen, а для размещения окна с учетом позиции того окна, которое его запустило — значение CenterOwner. В случае отображения немодального окна с помощью CenterOwner нужно обязательно удостовериться в том, что свойство Owner нового окна устанавливается перед отображением данного.
WindowState
Это свойство берет значение из перечисления WindowState. Оно информирует о том, в каком состоянии сейчас находится окно: в развернутом, свернутом или обычном, а также позволяет изменять это состояние. В случае изменения этого свойства вызывается событие StateChanged.
WindowStyle
Это свойство берет значение из перечисления WindowStyle, которое определяет границу окна. Допустимыми значениями являются: SingleBorderWindow (которое устанавливается по умолчанию), ThreeDBorderWindow (которое визуализирует одинаковые границы в Windows Vista и почти одинаковые в Windows XP), ToolWindow (которое визуализирует тонкую границу, удобную для “плавающих” окон с инструментами без кнопок для сворачивания и разворачивания) и None (которое визуализирует очень тонкую приподнятую границу без области для строки заголовка). Отличия между ними проиллюстрированы на рис. 8.1.
Book_Pro_WPF-2.indb 223
19.05.2008 18:10:04
224
Глава 8
а)
б) Рис. 8.1. Различные значения для свойства WindowStyle: а) — в Windows Vista и б) — в Windows XP О событиях жизненного цикла, которые вызываются при создании, активизации или выгрузке окна, уже рассказывалось (см. главу 6). Помимо них класс Windows также включает события LocationChanged и WindowStateChanged, которые вызываются при изменении, соответственно, позиции и состояния (WindowState) окна.
Отображение окна Чтобы отобразить окно, необходимо создать экземпляр класса Window и вызвать метод Show() или ShowDialog(). Метод ShowDialog() отображает модальное окно. Модальные окна не позволяют пользователю получать доступ к родительскому окну, блокируя возможность использования в нем мыши и возможность ввода в нем каких-либо данных до тех пор, пока модальное окно не будет закрыто. Вдобавок метод ShowDialog() еще и не осуществляет возврат до тех пор, пока модальное окно не будет закрыто, так что выполнение любого находящего после него кода на время откладывается. (Это, однако, не означает, что в данное время не может выполняться и никакой другой код — например, при наличии запущенного таймера обработчик его событий все равно будет работать.) Наиболее часто применяемая в коде схема выглядит так: отображение модального окна, ожидание его закрытия и последующее выполнение над его данными какой-нибудь операции.
Book_Pro_WPF-2.indb 224
19.05.2008 18:10:04
Окна
225
Ниже показан пример использования метода ShowDialog(): TaskWindow winTask = new TaskWindow(); winTask.ShowDialog(); // Выполнение достигает этой точки после закрытия winTask.
Метод Show()отображает немодальное окно, которое не блокирует доступ пользователя ни к каким другим окнам. Более того, метод Show() осуществляет возврат сразу же после отображения окна, так что следующие после него в коде операторы выполняются незамедлительно. Можно создавать и показывать сразу несколько немодальных окон, и пользователь может взаимодействовать со всеми ними одновременно. В случае применения немодальных окон иногда требуется код синхронизации, гарантирующий обновление информации во втором окне при внесении каких-то изменений в первом и тем самым исключающий вероятность работы пользователя с недействительными данными. Ниже показан пример использования метода Show(): MainWindow winMain = new MainWindow(); winMain.Show(); // Выполнение достигает этой точки сразу же после отображения winMain.
Модальные окна идеально подходят для предоставления пользователю приглашения сделать выбор, прежде чем выполнение операции сможет быть продолжено. Например, возьмем приложение Microsoft Word. Это приложение всегда отображает окна Options (Параметры) и Print (Печать) в модальном режиме, вынуждая пользователя принимать решение перед продолжением. С другой стороны, окна, предназначенные для поиска по тексту или проверки наличия в документе орфографических ошибок, Microsoft Word отображает в немодальном режиме, позволяя пользователю редактировать текст в основном окне документа, пока идет выполнение задачи. Закрывается окно точно так же просто, с помощью метода Close(). Альтернативным вариантом является сокрытие окна из вида путем использования метода Hide() или установки для свойства Visibility значения Hidden. И в том и в другом случае окно остается открытым и доступным для кода. Как правило, скрывать имеет смысл только немодальные окна. Дело в том, что при сокрытии модального окна код остается в “замороженном” состоянии до тех пор, пока окно не будет закрыто, а закрыть невидимое окно пользователь никак не сможет.
Позиционирование окна Обычно размещать окно в каком-нибудь точно определенном месте на экране не требуется. В таких случаях можно просто использовать для свойства WindowState значение CenterOwner и ни о чем не беспокоится. В других случаях, которые хоть и бывают реже, но все-таки бывают, требуется указывать точную позицию окна, что подразумевает использование значения Manual для свойства WindowState и указание точных координат в свойствах Left и Right. Иногда выбору подходящего месторасположения и размера для окна нужно уделять немного больше внимания. Для примера давайте рассмотрим следующую ситуацию: вы случайно создали окно размером, который является слишком большим для отображения на дисплее с низким разрешением. Если речь идет о приложении с одним единственным окном, тогда наилучшим решением будет создать окно с возможностью изменения размеров. Если же речь идет о приложении с несколькими плавающими окнами, то дело усложняется. Вы можете попробовать просто ограничить позиции окна теми, которые поддерживаются даже на самых маленьких мониторах, но это, скорее всего, будет раздражать
Book_Pro_WPF-2.indb 225
19.05.2008 18:10:04
226
Глава 8
пользователей мониторов более новых моделей (которые приобрели мониторы с большей разрешающей способностью специально для того, чтобы иметь возможность умещать на экране одновременно больше информации). Поэтому вероятнее всего вам придется принимать решение о наилучшем размещении окна во время выполнения. А для этого вам потребуется извлечь кое-какую базовую информацию о доступном экранном оборудовании с помощью класса System.Windows.SystemParameters. Класс SystemParameters состоит из огромного списка статических свойств, которые возвращают информацию о различных параметрах системы. Например, его можно использовать для определения, включил ли пользователь помимо всего прочего функцию “горячего” отслеживания (hot tracking) и возможность перетаскивания целых окон. В случае окон класс SystemParameters является особенно полезным, потому что предоставляет два свойства, которые возвращают информацию о размерах текущего экрана: свойство FullPrimaryScreenHeight и свойство FullPrimaryScreenWidth. Оба они довольно просты, что иллюстрирует показанный ниже код (центрирующий окно во время выполнения): double screeHeight = SystemParameters.FullPrimaryScreenHeight; double screeWidth = SystemParameters.FullPrimaryScreenWidth; this.Top = (screenHeight - this.Height) / 2; this.Left = (screenWidth - this.Width) / 2;
Хотя этот код и эквивалентен применению свойства WindowState со значением CenterScreen, он предоставляет гибкость, позволяя реализовать различную логику позиционирования и выполнять ее в подходящее время. Даже еще более лучший вариант — воспользоваться прямоугольником SystemParameters. WorkArea для размещения окна в доступной области экрана. При вычислении рабочей области область, где пристыковывается панель задач (и любые другие “полосы”, стыкованные с рабочим столом), не учитывается. double workHeight = SystemParameters.WorkArea.Height; double workWidth = SystemParameters.WorkArea.Width; this.Top = (workHeight - this.Height) / 2; this.Left = (workWidth - this.Width) / 2;
На заметку! Оба примера кода характеризуются одним небольшим недостатком. Когда свойство Top устанавливается для окна, которое уже является видимым, это окно незамедлительно перемещается и обновляется. То же происходит и при установке свойства Left в следующей строке кода. В результате пользователям с хорошим зрением может быть заметно, что окно перемещается дважды. К сожалению, класс Window не предоставляет метода, который бы позволял устанавливать оба этих свойства одновременно. Поэтому единственным решением является позиционирование окна после его создания, но перед его отображением с помощью метода Show() или ShowDialog().
Сохранение и восстановление информации о местоположении окна К числу типичных требований для окна относится и запоминание его последнего месторасположения. Эта информация может храниться как в конфигурационном файле пользователя, так и в системном реестре Windows. При желании сделать так, чтобы информация о расположении какого-то важного окна хранилась в конфигурационном файле конкретного пользователя, сначала нужно дважды щелкнуть на узле Properties (Свойства) в окне проводника решений Solution Explorer и выбрать раздел Settings (Параметры), после чего добавить действующий толь-
Book_Pro_WPF-2.indb 226
19.05.2008 18:10:04
Окна
227
ко на уровне данного пользователя параметр с типом данных System.Windows.Rect, как показано на рис. 8.2.
Рис. 8.2. Свойство для хранения информации о расположении и размерах окна При наличии такого параметра далее можно очень легко создать код, который будет автоматически сохранять информацию о размерах и расположении окна, например такой: Properties.Settings.Default.WindowPosition = win.RestoreBounds; Properties.Settings.Default.Save();
Обратите внимание, что в приведенном коде используется свойство RestoreBound, которое предоставляет правильные размеры (т.е. последний размер окна в обычном — не свернутом и не развернутом — состоянии), даже если в текущий момент окно развернуто или свернуто. (Эта удобная функция не была напрямую доступна в Windows Forms и требовала вызова неуправляемой API-функции GetWindowPlacement()). Извлечь эту информацию, когда она необходима, тоже легко: try { Rect bounds = Properties.Settings.Default.WindowPosition; win.Top = bounds.Top; win.Left = bounds.Left; // Восстановить размер, только если он устанавливался для окна вручную. if (win.SizeToContent == SizeToContent.Manual) { win.Width = bounds.Width; win.Height = bounds.Height; } } сatch { MessageBox.Show("No settings stored."); // Нет сохраненных параметров. }
Book_Pro_WPF-2.indb 227
19.05.2008 18:10:04
228
Глава 8
Единственным ограничением при таком подходе является необходимость создавать отдельное свойство для каждого окна, у которого должна сохраняться информация о расположении и размерах. Если требуется, чтобы информация о расположении сохранялась у множества различных окон, тогда, возможно, лучше будет разработать более гибкую систему. Например, ниже показан вспомогательный класс, который сохраняет информацию о расположении для любого передаваемого ему окна с помощью ключа реестра, хранящего имя этого окна. (Вы можете использовать и дополнительную идентификационную информацию, если хотите сохранить параметры для нескольких окон, которые будут иметь одинаковые имена.) public class WindowPositionHelper { public static string RegPath = @"Software\MyApp\WindowBounds\"; public static void SaveSize(Window win) { // Создать или извлечь ссылку на ключ, где будут храниться параметры. RegistryKey key; key = Registry.CurrentUser.CreateSubKey(RegPath + win.Name); key.SetValue("Bounds", win.RestoreBounds.ToString()); } public static void SetSize(Window win) { RegistryKey key; key = Registry.CurrentUser.OpenSubKey(RegPath + win.Name); if (key != null) { Rect bounds = Rect.Parse(key.GetValue("Bounds").ToString()); win.Top = bounds.Top; win.Left = bounds.Left; // Восстановить размер, только если он устанавливался для окна вручную. if (win.SizeToContent == SizeToContent.Manual) { win.Width = bounds.Width; win.Height = bounds.Height; } } } }
Чтобы использовать этот класс в окне, нужно вызвать метод SaveSize() при закрытии окна и метод SetSize() при его первом открытии. В каждом случае также обязательно следует передать ссылку на окно, которое вспомогательный класс должен инспектировать. Обратите внимание, что в данном примере у каждого окна должно быть свое значение для свойства Name.
Взаимодействие окон В главе 3 приводилась модель приложения WPF, и было впервые показано, как окна могут взаимодействовать между собой. Там было видно, что класс Application предоставляет два инструмента для получения доступа к другим окнам: свойство MainWindow и свойство Window. При желании отслеживать окна более специализированным образом — например, путем отслеживания экземпляров определенного класса Window, которые могут представлять документы — разработчик может добавлять в класс Application свои собственные статические свойства.
Book_Pro_WPF-2.indb 228
19.05.2008 18:10:05
Окна
229
Конечно, получение ссылки на другое окно — это только полдела. Также необходимо определиться со способом взаимодействия. В принципе необходимость во взаимодействии окон следует сводить к минимуму, потому что это излишне усложняет код. Однако если действительно требуется, чтобы значение элемента управления в одном окне изменялось на основании действия, выполняемого пользователем в другом окне, тогда, конечно, лучше создать в целевом окне специальный метод. Это гарантирует правильную идентификацию зависимости и добавит еще один уровень косвенности, упрощающий подгонку изменений в интерфейсе окна. Совет. Если два окна должны взаимодействовать между собой каким-то сложным образом, разрабатываются или развертываются отдельно или подвержены изменениям, можно пойти на один шаг дальше и формализовать их взаимодействие, создав интерфейс с общедоступными методами и реализовав его в классе своего окна. На рис. 8.3 и 8.4 представлены два примера реализации такой схемы. На рис. 8.3 показано окно, которое вынуждает второе окно обновлять свои данные в ответ на щелчок на кнопке. Это окно не пытается напрямую изменить пользовательский интерфейс второго окна; вместо этого оно полагается на специальный промежуточный метод по имени DoUpdate(). Получающее окно
Действующее окно
Обработчик события кнопки
Класс Window
Обновляет
Передает событие
Обновить
ВЫЗЫВАЕТ
Специальный метод DoUpdate()
Класс Window
Рис. 8.3. Взаимодействие с одним окном Второй пример, показанный на рис. 8.4, иллюстрирует ситуацию, когда требуется обновление более одного окна. В этом случае действующее окно полагается на более высокоуровневый метод приложения, который вызывает методы, требуемые для обновления других окон (возможно, даже путем прохода по коллекции окон). Этот подход лучше, потому что он работает на более высоком уровне. В подходе, показанном на рис. 8.3, действующему окну не нужно знать ничего конкретного об элементах управления в получающем окне. В подходе, приведенном на рис. 8.4, производится еще один шаг вперед: здесь действующему окну вообще не нужно ничего знать даже о классе получающего окна. Совет. При взаимодействии между окнами очень часто полезным оказывается метод Window. Activate(). Этот метод позволяет передавать команду активации нужному окну. (Еще также можно использовать свойство Window.IsActive для проверки того, является ли данное окно в текущий момент единственным активным окном.) В этом примере привязку можно сделать даже еще слабее. Вместо того, что вызывать метод в разных окнах, класс Application может просто возбуждать событие и позволять окнам самим выбирать, как на него реагировать.
Book_Pro_WPF-2.indb 229
19.05.2008 18:10:05
230
Глава 8 Получающее окно
Действующее окно
Обновляет
Обработчик события кнопки
Специальный метод DoUpdate() ЗЫ ВЫ
Класс Window
ВЫЗЫВАЕТ
ВА
ЕТ
Передает событие
Обновить
Класс Window Получающее окно
Специальный метод UpdateAll()
Класс Application
ВЫЗЫВАЕТ
Специальный метод DoUpdate() Обновляет
Класс Window
Рис. 8.4. Взаимодействие одного окна со многими На заметку! WPF может помогать абстрагировать логику приложения с помощью поддерживаемых команд, которые представляют собой предназначенные специально для приложений задачи и могут инициироваться любым способом. Подробнее об этом будет рассказываться в главе 10. Примеры на рис. 8.3 и 8.4 демонстрируют, как отдельные окна (обычно немодальные) могут инициировать действия внутри друг друга. Но существуют и более простые модели взаимодействия окон (такие как модели диалоговых окон), а также модели, которые дополняют данную (вроде моделей владения окнами). Именно о них и пойдет речь в следующих разделах.
Владение окнами .NET позволяет окну “владеть” другими окнами. Окна, имеющие окно-владельца, удобно применять для плавающих окон панелей инструментов и окон команд. Одним из примеров такого окна является окно Find and Replace (Найти и заменить) в Microsoft Word. Когда окно-владелец сворачивается, окно, которым оно владеет, тоже автоматически сворачивается. Когда имеющее владельца окно перекрывает окно, которое им владеет, оно всегда отображается сверху. Для поддержки владения окна класс Window предлагает два свойства: свойство Owner и свойство OwnedWindows. Свойство Owner представляет собой ссылку, которая указывает на окно, владеющее текущим окном (если таковое имеется), а свойство OwnedWindows — коллекцию всех окон, которыми владеет текущее окно (опять же, если таковые имеются). Настройка владения окна подразумевает просто установку свойства Owner, как показано ниже: // Создать новое окно. ToolWindow winTool = new ToolWindow(); // Обозначить текущее окно как являющееся владельцем. winTool.Owner = this; // Показать окно, принадлежащее окну-владельцу. winTool.Show();
Book_Pro_WPF-2.indb 230
19.05.2008 18:10:05
Окна
231
Окна, имеющее окно-владельца, всегда отображаются как немодальные. Чтобы удалить такое окно, нужно всего лишь установить для его свойства Owner значение null. На заметку! WPF не включает системы для построения многодокументных приложений (Multiple Document Interface — MDI). При необходимости в более сложном управлении окнами, придется разрабатывать его самостоятельно (или приобретать необходимый компонент у независимых разработчиков). Окно, имеющее владельца, может само владеть каким-нибудь другим окном, которое, в свою очередь, может владеть еще каким-нибудь окном и т.д. (хотя практическая пригодность такого дизайна весьма сомнительна). Единственным ограничением является то, что окно не может владеть самим собой, а также то, что два окна не могут владеть друг другом.
Модель диалогового окна Часто отображая окно как модальное, разработчик предлагает пользователю сделать какой-нибудь выбор. Код, отображающий окно, дожидается получения результата этого выбора и затем выполняет на его основании соответствующее действие. Такой дизайн называется моделью диалогового окна, а само отображаемое модальное окно — диалоговым окном. Такой дизайн можно легко корректировать путем создания в диалоговом окне общедоступного свойства. Это свойство может устанавливаться, когда пользователь делает в диалоговом окне выбор. Далее диалоговое окно может закрываться, а отображавший его код — проверять установленное для свойства значение и на его основании определять, какое действие должно быть выполнено следующим. (Имейте в виду, что даже когда окно закрывается, объект окна и информация обо всех его элементах управления все равно существует до тех пор, пока не закончится действие ссылающейся на него переменной.) К счастью, такая инфраструктура уже отчасти жестко закодирована в классе Window. У каждого окна имеется уже готовое свойство DialogResult, которое может принимать значение true, false или null. Обычно значение true означает, что пользователь выбрал продолжить операцию (например, щелкнул на кнопке OK), а значение false — что он отменил операцию. Лучше всего, когда результаты диалогового окна возвращаются в вызывающий код в виде значения, возвращаемого методом ShowDialog(). Это позволяет создавать, отображать и анализировать результаты диалогового окна с помощью всего лишь такого короткого кода: DialogWindow dialog = new DialogWindow(); if (dialog.ShowDialog() == true) { // Пользователь разрешил действие. Вперед! } else { // Пользователь отменил действие. }
На заметку! Использование свойства DialogResult не исключает возможности добавления в окно специальных свойств. Например, исключительно целесообразно использовать свойство DialogResult для информирования вызывающего кода о том, разрешено или отменено было действие, и для предоставления других важных деталей через специальные свойства. В случае обнаружения в свойстве DialogResult значения true вызывающий код далее может проверять эти другие свойства для извлечения необходимой ему информации.
Book_Pro_WPF-2.indb 231
19.05.2008 18:10:05
232
Глава 8
Также существует и еще один короткий путь. Вместо того чтобы устанавливать свойство DialogResult вручную после выполнения пользователем щелчка на кнопке, можно назначить кнопку кнопкой Accept (Принять) (путем установки для свойства IsDefault значения true). Тогда щелчок на этой кнопке будет автоматически приводить к установке для свойства DialogResult значения true. Подобным образом также можно сделать кнопку кнопкой Cancel (Отмена) (путем установки значения true для свойства IsCancel), в результате чего щелчок на ней будет автоматически приводить к установке для свойства DialogResult значения Cancel. (О свойствах IsDefault и IsCancel рассказывалось в главе 7 при рассмотрении кнопок.) На заметку! Модель диалогового окна в WPF отличается от той, что предлагается в Windows Forms. Там кнопки не предоставляют свойства DialogResult, из-за чего создавать можно только кнопки по умолчанию и кнопки отмены. Свойство DialogResult может принимать только значения true, false и null (последнее устанавливается для него изначально). Вдобавок щелчок на кнопке не приводит к автоматическому закрытию окна — код, выполняющий эту операцию, необходимо писать отдельно.
Общие диалоговые окна Операционная система Windows включает много встроенных диалоговых окон, доступ к которым можно получать через API-интерфейс Windows. Для некоторых из них WPF предоставляет классы-упаковщики. На заметку! Существует веская причина того, что WPF не включает упаковщики для абсолютно всех API-интерфейсов Windows. Одной из задач WPF является отделение от Windows API для получения возможности использования в других средах (например, в браузере) или переноса на другие платформы. Также многие из встроенных диалоговых окон уже начинают устаревать и потому не должны применяться в современных приложениях. Вдобавок в версии Windows Vista предпочтение уже отдается использованию не диалоговых окон, а основанных на задачах панелей и навигации. Наиболее приметным из этих классов является класс System.Windows.MessageBox, который предоставляет статический метод Show(). Этот код можно использовать для отображения стандартных окон сообщений Windows. Ниже показана наиболее распространенная перегруженная версия этого метода: MessageBox.Show("You must enter a name.", "Name Entry Error", MessageBoxButton.OK, MessageBoxImage.Exclamation); // "Требуется ввести имя.", "Ошибка при вводе имени"
Перечисление MessageBoxButton позволяет выбирать кнопки, которые должны отображаться в окне сообщений. К числу доступных вариантов относятся OK, OKCancel, YesNo и YesNoCancel. (Менее удобный для пользователя вариант AbortRetryIgnore не поддерживается.) Перечисление MessageBoxImage позволяет выбирать пиктограмму для окна сообщения (Information, Exclamation, Error, Hand, Question, Stop и т.д.). Для класса MessageBox в WPF предусмотрена специальная поддержка печатных функций, подразумевающих использование класса PrintDialog (о котором более подробно будет рассказываться в главе 20), а также классов OpenFileDialog и SaveFileDialog в пространстве имен Microsoft.Win32. Классы OpenFileDialog и SaveFileDialog здесь имеют кое-какие дополнительные функциональные возможности (часть из которых наследуются от класса FileDialog). Оба из них поддерживают строку фильтра, которая устанавливает разрешенные расши-
Book_Pro_WPF-2.indb 232
19.05.2008 18:10:05
Окна
233
рения файлов. Класс OpenFileDialog также предлагает свойства, которые позволяют проверять выбор пользователя (CheckFileExists) и предоставлять ему возможность выбирать сразу несколько файлов (Multiselect). Ниже показан пример кода, который отображает диалоговое окно OpenFileDialog и выбранные файлы в окне списка после закрытия этого диалогового окна. OpenFileDialog myDialog = new OpenFileDialog(); myDialog.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF" + "|All files (*.*)|*.*"; myDialog.CheckFileExists = true; myDialog.Multiselect = true; if (myDialog.ShowDialog() == true) { lstFiles.Items.Clear(); foreach (string file in myDialog.FileNames) { lstFiles.Items.Add(file); } }
Элементов, позволяющих выбирать цвета, указывать шрифт и просматривать папки, здесь нет (хотя при использовании классов System.Windows.Forms из .NET 2.0 они доступны).
Непрямоугольные окна Окна необычной формы часто являются товарным знаком ультрасовременных популярных приложений вроде редакторов фотографий, программ для создания кинофильмов и MP3-проигрывателей; скорее всего, они будут встречаться в WPF-приложениях даже еще более часто. В создании базового приложения нестандартной формы в WPF нет ничего сложного. Однако создание привлекательного профессионально выглядящего окна необычной формы требует немалых усилий — и, нередко, привлечения талантливого дизайнера графики для создания набросков и фоновой графики.
Простое окно нестандартной формы Базовая процедура для создания окна нестандартной формы подразумевает выполнение следующих шагов. 1. Установите для свойства Window.AllowsTransparency значение true. 2. Установите для свойства Window.WindowStyle значение None, чтобы скрыть не клиентскую область окна (рамку голубого цвета). Если этого не сделать, при попытке показать окно появится ошибка InvalidOperationException. 3. Установите для фона (свойства Background) прозрачный цвет (цвет Transparent, значение альфа-канала которого равняется нулю). Или же сделайте так, чтобы для фона использовалось изображение, имеющее прозрачные области (с нулевым значением альфа-канала). Эти три шага эффективно удаляют стандартный внешний вид окна (который специалисты по WPF часто называют хромом (chrome) окна). Для обеспечения эффекта окна необычной формы далее необходимо предоставить какое-то непрозрачное содержимое, имеющее нужную форму. Здесь возможны перечисленные ниже варианты.
Book_Pro_WPF-2.indb 233
19.05.2008 18:10:05
234
Глава 8
• Предоставить фоновую графику, используя файл такого формата, который поддерживает прозрачность. Например, для фона можно использовать файл PNG. Это простой прямолинейный подход, и он очень удобен, если приходится работать с дизайнерами, которые не разбираются в XAML. Однако из-за того, что окно будет визуализироваться с большим количеством пикселей и более высокими системными параметрами DPI фоновая графика может приобрести искаженный вид. Это также может представлять проблему и в случае разрешения пользователю изменять размеры окна.
• Использовать доступные в WPF функции для рисования формы, чтобы создать фон с векторным содержимым. Такой подход исключает потерю качества, какими бы ни были размеры окна и DPI-параметры системы. Однако в этом случае наверняка потребуется использовать средство проектирования, поддерживающее XAML. (Программа Expression Blend является наилучшим вариантом, если необходима интеграция с Visual Studio, но в принципе даже традиционные приложения могут предлагать функции для экспорта XAML через какой-то подключаемый модуль. К числу таких приложений относится Adobe Illustrator, соответствующий подключаемый модуль для которого доступен по адресу http:// www.mikeswanson.com/xamlexport.)
• Использовать более простой WPF-элемент, имеющий необходимую форму. Например, окно с замечательными скругленными краями можно создать с помощью элемента Border. Такой подход позволяет создавать окна с современным внешним видом в стиле Office без применения каких-либо дизайнерских навыков. Ниже в качестве примера приведен код создания пустого прозрачного окна с применением первого подхода и предоставлением файла PNG для прозрачных областей. A Sample Button Another Button
На рис. 8.5 показано это окно с расположенным за ним окном программы Notepad (Блокнот). Это окно необычной формы (состоящее из окружности и квадрата) имеет не только пробелы, сквозь которые может просматриваться находящееся за ним содержимое, но кнопки, которые выходят за границы изображения и накладываются на прозрачную область, из-за чего кажется, будто бы они существуют сами по себе, без окна. Те, кому доводилось программировать с использованием Windows Forms ранее, наверняка заметят, что окна нестандартной формы в WPF имеют более четкие края, особенно на изгибах. Все дело в том, что WPF умеет выполнять сглаживание между фоном окна и находящимся за ним содержимым для создания более гладкого края.
Book_Pro_WPF-2.indb 234
19.05.2008 18:10:05
Окна
235
Рис. 8.5. Окно необычной формы, использующее фоновый рисунок На рис. 8.6 показано другое, более простое окно необычной формы. В этом окне используется элемент Border со скругленными углами для придания окну простого и в то же время отчетливого внешнего вида. Компоновка тоже является упрощенной, поскольку исключает случайный выход содержимого за пределы границы, а размер границы может легко изменяться без наличия элемента Viewbox.
Рис. 8.6. Окно необычной формы, использующее элемент Border В этом окне содержится элемент Grid с тремя строками, которые используются для строки заголовка, строки нижнего колонтитула и размещаемого между ними содержимого. В строке с содержимым находится еще один элемент Grid, который имеет другой фон и может содержать другие необходимые элементы (в текущий момент в нем находится только один единственный элемент TextBlock). Ниже показан код разметки, с помощью которого создается такое окно.
Book_Pro_WPF-2.indb 235
19.05.2008 18:10:05
236
Глава 8
Content Goes Here
Для завершения внешнего вида этого окна осталось создать только кнопки, имитирующие размещаемые в правом верхнем углу стандартные кнопки для разворачивания, сворачивания и закрытия окна. При желании иметь возможность многократно использовать данное окно потребуется отыскать способ, которым можно было бы отделить стиль окна от его содержимого. Идеальным вариантом будет разработка для окна специального шаблона, с помощью которого разработанный необычный внешний вид можно будет применять к любому окну. Пример преобразования показанного здесь окна в пригодный для многократного использования шаблон будет представлен в главе 15.
Прозрачные окна с содержимым необычной формы В большинстве случаев фиксированная графика в WPF для создания окон необычной формы не используется. Вместо этого в таких окнах просто применяется совершенно прозрачный фон, на котором затем размещается уже имеющее нужную форму содержимое. (Примером этого является показанная на рис. 8.5 кнопка, которая “нависает” над совершенно прозрачной областью.) Преимущество такого подхода заключается в том, что он является модульным. Окно может состоять из множества отдельных компонентов, представляющих собой первоклассные элементы WPF. Но даже еще более важно то, что такой подход позволяет пользоваться другими функциональными возможностями WPF и создавать по-настоящему динамические пользовательские интерфейсы. Например, он позволяет создавать содержимое необычной формы с возможностью изменения его размеров или применять анимацию для обеспечения непрерывно выполняющихся эффектов прямо внутри окна. Сделать такое очень не просто, если графика находится в одном статическом файле.
Book_Pro_WPF-2.indb 236
19.05.2008 18:10:06
Окна На рис. 8.7 показан пример. Здесь окно содержит элемент Grid с одной единственной ячейкой. Эту ячейку совместно используют два элемента. Первый — элемент Path, который прорисовывает границу окна нестандартной формы и заливает ее градиентным узором, а второй — контейнер макета, в котором находится предназначенное для окна содержимое, перекрывающее элемент Path. В данном случае в качестве контейнера макета служит элемент StackPanel, но в принципе это может быть и какойто другой элемент (например, еще один элемент Grid или элемент Canvas для абсолютного позиционировании на основе координат). В этом элементе StackPanel находится кнопка закрытия (со знакомым значком X) и текст.
237
Рис. 8.7. Окно нестандартной формы, использующее элемент Path
На заметку! Хотя на рис. 8.5 и 8.6 показаны разные примеры, они являются взаимозаменяемыми. То есть любой из них можно создать как с помощью подхода, подразумевающего использование фона, так и с помощью подхода, подразумевающего прорисовывание формы. Однако подход с прорисовыванием формы обеспечивает большую гибкость, если необходимо иметь возможность динамически изменять форму в будущем, и наилучшее качество, если требуется возможность изменять размер окна. Ключевым компонентом в данном примере является элемент Path, который создает фон. Это простая векторная фигура, состоящая из ряда линий и дуг. Подробнее об элементе Path и других классах фигур в WPF будет рассказываться в главах 13 и 14. Ниже приведен весь код разметки, необходимый для создания данного элемента Path.
Book_Pro_WPF-2.indb 237
19.05.2008 18:10:06
238
Глава 8
В текущий момент элемент Path имеет фиксированный размер (так же, как и окно), однако его размер запросто можно сделать и изменяемым, поместив элемент в контейнер Viewbox, о котором говорилось в главе 5. Еще улучшить этот пример можно, придав кнопке для закрытия окна более убедительный внешний вид — например, с помощью векторного значка X, прорисовываемого на красной поверхности. Хотя для представления кнопки и обработки связанных с ней событий мыши можно было бы воспользоваться и отдельным элементом Path, лучше поступить следующим образом: изменить стандартный элемент управления Button путем применения шаблона (о чем более подробно будет рассказываться в главе 15) и затем сделать элемент Path, прорисовывающий значок X, частью этой измененной кнопки.
Перемещение окон нестандартной формы Одним из ограничений окон нестандартной формы является то, что в них отсутствует неклиентская область со строкой заголовка, позволяющая пользователю легко перетаскивать окно по рабочему столу. В Windows Forms это было не совсем приятная задача — приходилось либо обеспечивать реакцию на события мыши вроде MouseDown, MouseUp и MouseMove и перемещать окно вручную при выполнении пользователем щелчка и перетаскивания, либо перекрывать метод WndProc() и обрабатывать низкоуровневое сообщение WM_NCHITTEST. В WPF эта задача решается гораздо легче. Здесь в любое время можно инициировать режим перетаскивания окна путем вызова метода Window.DragMove(). Итак, для того чтобы позволить пользователю перетаскивать окно необычной формы, которое было показано в предыдущем примере, необходимо просто добавить и обработать для окна (или того элемента в этом окне, который затем будет выполнять ту же роль, что и строка заголовка) событие MouseLeftButtonDown:
К обработчику события потребуется добавить только одну строку кода: private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { this.DragMove(); }
Теперь окно будет следовать за курсором мыши по экрану до тех пор, пока пользователь не отпустит кнопку мыши.
Изменение размеров окон нестандартной формы Изменение размеров окна нестандартной формы — задача не из простых. Если форма окна хотя бы отчасти напоминает прямоугольник, наиболее простым подходом будет добавить в правом нижнем углу элемент захвата и изменения размера путем установки для свойства ResizeMode значения CanResizeWithGrip. Однако при размещении такого элемента предполагается, что окно имеет прямоугольную форму. Например, в случае создания окна с эффектом скругленных краев за счет использования объекта Border, как было показано ранее на рис. 8.6, такой прием может и сработать. Элемент захвата и изменения размера появится в правом нижнем углу и, в зависимости от того, насколько скругленным был сделан этот угол, разместится в пределах поверхности окна, которому принадлежит. Но в случае создания окна более экзотической формы с применением, например, элемента Path, как было показано ранее на рис. 8.7, такой подход точно не сработает — элемент захвата и изменения размера “зависнет” в пустой области рядом с окном.
Book_Pro_WPF-2.indb 238
19.05.2008 18:10:06
Окна
239
Если добавление элемента захвата и изменения размера не подходит для окна данной формы или если требуется разрешить пользователю изменять размеры окна путем перетаскивания его краев, придется приложить немного дополнительных усилий. В принципе в таком случае существует два основных подхода. Первый — использовать .NET-функцию вызова платформы (P/Invoke) для отправки сообщения Win32, изменяющего размер окна, а второй — просто отслеживать позицию курсора мыши при перетаскивании пользователем окна в одну сторону и изменять размер вручную установкой свойства Width. Ниже рассматривается пример применения второго подхода. Прежде чем воспользоваться любым из этих подходов, нужно придумать способ для определения момента наведения пользователем курсора мыши на край окна. В WPF это легче всего сделать, разместив вдоль каждого края окна специальный элемент. Быть видимым этому элементу вовсе необязательно — на самом деле он даже может быть полностью прозрачным и позволять окну проглядывать сквозь него. Его единственная задачей будет перехват событий мыши. Одним из лучших кандидатов на эту роль является скромный элемент Rectangle, который представляет собой элемент, прорисовывающий форму, и более подробно будет описываться в главе 13. Идеальным для этой задачи будет элемент Rectangle толщиной в 5 единиц. Ниже демонстрируется, как можно разместить элемент Rectangle, позволяющий изменять размер с правой стороны, в окне со скругленными краями, которое было показано на рис. 8.6. ...
В данном случае элемент Rectangle размещается в верхней строке, но для свойства RowSpan получает значение 3. Благодаря этому он растягивается на все три строки и занимает всю правую сторону окна. В свойстве Cursor указывается тот курсор мыши, который должен появляться при наведении мыши на этот элемент. В данном случае это курсор изменения размера под названием “запад-восток” — он имеет знакомую всем форму двухконечной стрелки, которая указывает вправо и влево. Обработчики событий элемента Rectangle переключают окно в режим изменения размера, когда пользователь щелкает на краю. Здесь необходимо захватить мышь для обеспечения уверенности в том, что события будут продолжать поступать даже в случае перемещения мыши за счет перетаскивания с поверхности прямоугольника в какуюнибудь сторону. Захват мыши снимается тогда же, когда пользователь отпускает левую кнопку мыши. bool isWiden = false; private void window_initiateWiden(object sender, MouseEventArgs e) { isWiden = true; } private void window_Widen(object sender, MouseEventArgs e) { Rectangle rect = (Rectangle)sender; if (isWiden) { rect.CaptureMouse(); double newWidth = e.GetPosition(this).X + 5;
Book_Pro_WPF-2.indb 239
19.05.2008 18:10:06
240
Глава 8 if (newWidth > 0) this.Width = newWidth; }
} private void window_endWiden(object sender, MouseEventArgs e) { isWiden = false; // Контрольное снятие захвата. Rectangle rect = (Rectangle)sender; rect.ReleaseMouseCapture(); }
На рис. 8.8 показано, как этот код будет выглядеть в действии.
Рис. 8.8. Изменение размеров окна нестандартной формы
Окна в стиле Vista Одним из вопиющих упущений в WPF является отсутствие хоть каких-либо управляемых классов, которые бы позволяли упаковывать новые функциональные возможности Vista. Нехватку интеграции с моделью безопасности UAC (которая может вынуждать создавать файлы манифестов, как рассказывалось в главе 3) наверняка уже заметили все. Таким же заметным является и отсутствие поддержки для расширения эффекта “стеклянного размытия” в рамках окон и создания диалоговых окон с использованием нового стиля задачных диалоговых окон. К счастью, ни одна из этих недостающих функциональных возможностей не является совсем уж недоступной. К любой из них можно получить доступ с помощью предлагаемой в .NET функции вызова платформы (P/Invoke), позволяющей делать неуправляемые вызовы интерфейса Win32 API. Более подробно о том, как можно использовать стеклянный эффект и новые диалоговые окна Vista будет рассказываться в следующих разделах. Но прежде чем двигаться дальше, важно отметить, что у всех новых функциональных возможностей Windows Vista имеется один очевидный и очень значительный недостаток: они не будут доступны в случае выполнения приложений WPF под управлением операционной системы, отличной от Windows Vista. Во избежание подобной проблемы следует обязательно писать код, проверяющий версию операционной системы и, при необходимости, аккуратно сокращающий доступные возможности. Например, он может переключаться с эквивалента Vista на традиционный OpenFileDialog при выполнении в Windows XP или пропускать любой код, который подразумевает применение стеклянного эффекта Vista.
Book_Pro_WPF-2.indb 240
19.05.2008 18:10:06
Окна
241
Самый простой способ определить, выполняется ли приложение на Windows Vista — это прочитать значение статического свойства OSVersion из класса System. Environment. Вот как это будет выглядеть: if (Environment.OSVersion.Version.Major >= 6) { // Функциональные возможности Vista поддерживаются. }
Использование стеклянного эффекта Windows Vista Одной из самых заметных особенностей во “внешнем виде” Vista являются оконные рамки с эффектом размытого стекла, через которые можно видеть другие окна и их содержимое. Эту особенность часто называют эффектом Aero Glass (где “Aero” — это имя пользовательского интерфейса Windows Vista). Приложения, выполняющиеся под управлением Windows Vista, получают эффект Aero Glass автоматически в неклиентской области окна. Если в WPF отображается стандартное окно со стандартной рамкой, и приложение выполняется на поддерживающем интерфейс Aero компьютере (на компьютере, где установлена любая версия Windows Vista, кроме Home Basic, имеется необходимая поддержка видеокарты и включена данная функция), окно получается с привлекающей глаз полупрозрачной рамкой. В некоторых приложениях этот эффект распространяется и на клиентскую область окна. К числу таких приложений относится Internet Explorer, где стеклянный эффект захватывает и строку адреса, а также проигрыватель Windows Media, где стеклянный эффект применяется и по отношению к элементам управления воспроизведением. Такие чудеса можно творить и в своих собственных приложениях, учитывая два описанных ниже ограничения.
• Область размытого стекла всегда начинается с краев окна. Это означает, что создать стеклянную “вставку” где-нибудь в середине окна нельзя. Однако добиться такого эффекта можно, и просто разместив на стеклянной рамке полностью непрозрачные элементы WPF.
• Не стеклянная область внутри окна всегда определяется как прямоугольник. В WPF нет классов для реализации такого эффекта. Вместо этого здесь требуется вызывать функцию DwmExtendFrameIntoClientArea()из Win32 API. (Префикс Dwm соответствует диспетчеру окон рабочего стола (Desktop Window Manager), который и отвечает за управление данным эффектом.) Вызов этой функции позволяет расширять рамку так, чтобы она захватывала и клиентскую область окна, делая толще какой-то один или все ее края. Ниже показано, как следует импортировать функцию DwmExtendFrameIntoClientArea() для того, чтобы иметь возможность вызвать ее из своего приложения: [DllImport("DwmApi.dll")] public static extern int DwmExtendFrameIntoClientArea( IntPtr hwnd, ref Margins pMarInset);
Также необходимо определить фиксированную структуру Margins, как показано ниже: [StructLayout(LayoutKind.Sequential)] public struct Margins { public int cxLeftWidth; public int cxRightWidth;
Book_Pro_WPF-2.indb 241
19.05.2008 18:10:06
242
Глава 8 public int cyTopHeight; public int cyBottomHeight;
}
Здесь имеется один камень преткновения. Как уже упоминалось ранее, в системе измерений WPF используются аппаратно-независимые единицы, размер которых устанавливается на основании параметров DPI системы. Однако в функции DwmExtendFrameIntoClientArea() применяются физические пиксели. Для уверенности в том, что элементы WPF будут присоединяться к расширенной стеклянной рамке независимо от параметров DPI системы, нужно обязательно сделать так, чтобы эти параметры DPI учитывались при расчетах. Самой простой способ извлечь значения DPI-параметров системы — это использовать класс System.Drawing.Graphics, который имеет два свойства — DpiX и DpiY, представляющие DPI-значения окна. Ниже показан вспомогательный метод, который берет дескриптор окна и набор единиц WPF и возвращает объект Margin с откорректированными соответствующим образом размерами в физических пикселях. public static Margins GetDpiAdjustedMargins(IntPtr windowHandle, int left, int right, int top, int bottom) { // Извлечение значений DPI-параметров системы. System.Drawing.Graphics g = System.Drawing.Graphics.FromHwnd(windowHandle); float desktopDpiX = g.DpiX; float desktopDpiY = g.DpiY; // Установка полей VistaGlassHelper.Margins margins = new VistaGlassHelper.Margins(); margins.cxLeftWidth = Convert.ToInt32(left * (desktopDpiX / 96)); margins.cxRightWidth = Convert.ToInt32(right * (desktopDpiX / 96)); margins.cyTopHeight = Convert.ToInt32(top * (desktopDpiX / 96)); margins.cyBottomHeight = Convert.ToInt32(right * (desktopDpiX / 96)); return margins; }
На заметку! К сожалению, класс System.Drawing.Graphics является частью Windows Forms. Поэтому для получения доступа к нему требуется добавлять ссылку на сборку System. Drawing.dll. Последнее, что осталось сделать — применить поля к окну с помощью функции
DwmExtendFrameIntoClientArea(). В следующем коде показан объединенный вспомогательный метод, который принимает параметры полей WPF и ссылку на окно WPF, после чего извлекает Win32-дескриптор окна, корректирует поля и пытается расширить стеклянную рамку. public static void ExtendGlass(Window win, int left, int right, int top, int bottom) { // Получение Win32-дескриптора для окна WPF. WindowInteropHelper windowInterop = new WindowInteropHelper(win); IntPtr windowHandle = windowInterop.Handle; // Корректировка полей так, чтобы в них учитывались параметры DPI системы. Margins margins = GetDpiAdjustedMargins(windowHandle, left, right, top, bottom); // Расширение стеклянной рамки. int returnVal = DwmExtendFrameIntoClientArea(windowHandle, ref margins); if (returnVal < 0) { throw new NotSupportedException("Operation failed."); } }
Book_Pro_WPF-2.indb 242
19.05.2008 18:10:06
Окна
243
В приводимом в настоящей главе коде все эти “ингредиенты” упаковываются в один единственный класс по имени VistaGlassHelper, который может вызываться из любого окна. Для того чтобы этот код работал, необходимо, чтобы он вызывался перед отображением окна. Идеальной возможностью для этого является событие Window.Loaded. Кроме того, нужно еще также не забыть установить для свойства Background значение Transparent для того, чтобы стеклянная рамка просматривалась сквозь поверхность рисования WPF. На рис. 8.9 показан пример, в котором утолщается верхний край стеклянной рамки.
Рис. 8.9. Расширение стеклянной рамки При создании такого окна содержимое в верхней части группируется в один элемент
Border. Это позволяет замерять высоту границы (т.е. элемента Border) и использовать это значение для расширения стеклянной рамки. (Конечно, стеклянная рамка устанавливается только один раз, а именно — при первом создании окна. В случае изменения содержимого или размеров окна и увеличения или сжатия элемента Border, этот элемент больше не будет присоединяться к стеклянной рамке.) Ниже показан весь необходимый для создания такого окна код разметки. Some content that's docked to the top. A Button Some text.
Book_Pro_WPF-2.indb 243
19.05.2008 18:10:06
244
Глава 8
A Button
Обратите внимание, что для фона второго элемента Border в этом окне, в котором размещается остальное содержимое, должен быть явно установлен белый цвет. В противном случае эта часть окна окажется абсолютно прозрачной. (По этой же причине у второго элемента Border не должно быть и никаких полей, иначе вокруг него будет отображаться прозрачная каемка.) Когда окно загружается, оно вызывает метод ExtendGlass() и передает ему новые координаты. Обычно стеклянная рамка имеет 5 единиц в ширину, но показанный ниже код утолщает ее верхний край. private void window_Loaded(object sender, RoutedEventArgs e) { try { VistaGlassHelper.ExtendGlass(this, 5, 5, (int)topBar.ActualHeight + 5, 5); } catch { // В случае выполнения этого кода в Windows XP // генерируется исключение DllNotFoundException. // В случае если не удается выполнение вызова // DwmExtendFrameIntoClientArea(), генерируется // исключение NotSupportedException. this.Background = Brushes.White; } }
При желании расширить стеклянный край так, чтобы он захватывал все окно целиком, следует просто передать для каждой стороны в качестве параметра поля значение -1. На рис. 8.10 показано, как будет выглядеть окно в таком случае.
Рис. 8.10. Полностью “стеклянное” окно
Book_Pro_WPF-2.indb 244
19.05.2008 18:10:07
Окна
245
При использовании эффекта Aero Glass также необходимо учитывать и то, как внешний вид содержимого будет меняться в случае размещения окна поверх других фоновых изображений. Например, в случае размещения в стеклянной области окна текста черного цвета, этот текст будет легче читаться на светлом фоне, чем на темном (хотя разборчивым он будет в обоих случаях). Для повышения степени удобочитаемости текста и обеспечения четкого отображения содержимого на разных фонах принято добавлять какой-нибудь эффект подсвечивания (glow effect). Например, черный цвет с белой подсветкой будет выглядеть одинаково разборчиво как на светлом, так и на темном фоне. Windows Vista предлагает свою неуправляемую функцию для рисования эффекта подсветки, которая называется DrawThemeTextEx(). Однако у WPF имеется множество собственных приемов, позволяющих получать точно такой же (если даже не лучший) результат. Двумя наиболее яркими примерами таких приемов являются использование декоративной кисти (fancy brush) для закрашивания текста и добавление к тексту растрового эффекта. (Оба из них будут более подробно рассматриваться в главе 13).
Другие интересные функции DWM В предыдущем примере было показано, как с помощью функции DwmExtendFrameIntoClientArea() можно создавать утолщенный стеклянный край (или даже вообще полностью стеклянное окно). Однако DwmExtendFrameIntoClientArea() является далеко не единственной полезной функцией в API-интерфейсе Windows. Существует еще несколько других API-функций, названия у которых тоже начинаются с букв DWM и которые тоже позволяют взаимодействовать с диспетчером окон рабочего стола. Например, функция DwmIsCompositionEnabled() может вызываться для проверки того, включен ли эффект Aero Glass, а функция DwmEnableBlurBehindWindow() — для применения стеклянного эффекта к какой-нибудь конкретной области в окне. Кроме того, еще также существует несколько функций, которые позволяют получать живое миниатюрное представление других приложений. Узнать самое необходимое обо всех этих функциях можно в MSDN-документации по диспетчеру окон рабочего стола, доступной по адресу http://tinyurl.com/333glv.
Диалоговые окна задач и файлов Хотя WPF включает знакомые файловые диалоговые окна вроде OpenFileDialog и OpenSaveDialog, классов для новых диалоговых окон, которые были представлены вместе с Windows Vista, в WPF нет. К числу недостающих элементов относятся переделанные диалоговые окна Open (Открытие файла) и Save (Сохранение файла) и совершенно новое диалоговое окно TaskDialog. Диалоговое окно TaskDialog представляет собой в некотором роде супермощное окно MessageBox. Оно включает область заголовка, область нижнего колонтитула и ряд дополнительных элементов управления, начиная от строки информации о ходе выполнения и заканчивая гиперссылками. Его можно использовать для отображения более дружественной версии окна сообщений, для приглашения пользователя дать ответ на вопрос и сбора входных данных, а также для показа общего сообщения типа “идет выполнение” в период выполнения кодом его работы. На рис. 8.11 приведены два простых примера. Хотя библиотеки WPF не включают никакой поддержки для диалоговых окон в стиле Vista, компания Microsoft выпустила просто незаменимый (но часто не замечаемый) образец, который иллюстрирует все наиболее сложные моменты. Этот образец входит в состав пакета Windows SDK .NET Framework 3.0 Samples, который можно загрузить по адресу http://tinyurl.com/36s6py. Вместо того чтобы загружать весь пакет образцов, можно загрузить только ту его часть, которая называется CrossTechnologySamples.exe
Book_Pro_WPF-2.indb 245
19.05.2008 18:10:07
246
Глава 8
и включает образцы, демонстрирующие применение новых диалоговых окон Windows Vista. Конкретный необходимый среди них образец называется проектом VistaBridge. В состав этого проекта VistaBridge входит библиотека классов, которая упаковывает требуемые функции Win32 (с помощью P/Invoke) и предоставляет свыше 30 классов более высокого уровня. Кроме того, в его состав также еще входит тестовое окно, демонстрирующее несколько способов применения диалогового окна TaskDialog, и элемент управления типа мастера. Хорошей отправной точкой является класс TaskDialog, с помощью которого как раз и были созданы окна, показанные на рис. 8.11. Для использования класса TaskDialog нужно просто создать его экземпляр, установить соответствующие свойства и вызвать метод Show(). Например, ниже показан код, с помощью которого было создано верхнее окно на рис. 8.11. TaskDialog taskDialog = new TaskDialog(); taskDialog.Content = "Are you sure you want to continue?";) taskDialog.StandardButtons = TaskDialogStandardButtons.YesNo; taskDialog.MainIcon = TaskDialogStandardIcon.Warning; taskDialog.Caption = "Confirm Format"; taskDialog.Instruction = "Confirm Drive Format"; taskDialog.FooterText = "NOTE: All data stored on the drive will be lost."; taskDialog.FooterIcon = TaskDialogStandardIcon.Information; TaskDialogResult result = taskDialog.Show(); if (result.StandardButtonClicked == TaskDialogStandardButton.Yes) { ... }
Объект TaskDialogResult упаковывает информацию, которую предоставил пользователь, в том числе информацию о любых выбранных им флажках или переключателях (для чего он использует свойства CheckBoxChecked и RadioButtonClicked). В данном примере у пользователя есть два варианта: он может щелкнуть на кнопке Yes (Да) или на кнопке No (Нет). За предоставление информации о том, на какой из этих кнопок щелкнул пользователь, отвечает свойство StandardButtonClicked.
Рис. 8.11. Диалоговые окна в стиле Vista
Book_Pro_WPF-2.indb 246
19.05.2008 18:10:07
Окна
247
Альтернативный подход — определить окно TaskDialog декларативно на языке XAML. Поскольку объект TaskDialog не является элементом WPF, объявлять его следует в разделе Window.Resources кода разметки, как показано ниже. ...
Далее этот объект можно извлечь по имени ключа и использовать необходимым образом: TaskDialog dialog = (TaskDialog)this.Resources["simpleWait"]; TaskDialogResult result = dialog.Show();
О системе ресурсов WPF более подробно будет рассказываться в главе 11. Для тех, кто желает пользоваться преимуществами специфических API-интерфейсов Vista, образец VistaBridge является наилучшей отправной точкой. На заметку! На момент написания этой книги в демонстрационном проекте VistaBridge присутствует один небольшой недостаток. Он подразумевает использование проектных файлов Visual Studio 2005 и не может нормально функционировать при преобразовании в Visual Studio 2008. Проблему представляет файл манифеста, который в Visual Studio 2008 требуется воссоздавать заново. Чтобы создать файл манифеста заново, нужно щелкнуть правой кнопкой мыши на проекте в окне Solution Explorer, выбрать в контекстном меню пункт AddNew Item (ДобавитьНовый элемент), выделить шаблон Application Manifest File (Файл манифеста приложения) и щелкнуть на кнопке Add (Добавить), а затем скопировать содержимое из существующего файла манифеста (входящего в состав проекта в виде файла поддержки) в новый файл манифеста, который был сгенерирован Visual Studio. Альтернативный вариант — загрузить исправленную версию проекта VistaBridge, входящую в состав примеров для данной главы.
Резюме В этой главе был предложен краткий обзор модели окон WPF. По сравнению с предыдущими технологиями наподобие Windows Forms, в WPF окно представляет собой модернизированный и упрощенный объект. Во многих классах это является преимуществом, потому что перекладывает ответственность на другие элементы и позволяет создавать приложения с более гибким дизайном (вроде основанных на использовании навигации систем, о которых будет рассказываться в следующей главе). Но в других случаях это является отражением того факта, что WPF пока что еще остается новой, не до конца развившейся технологией, которой не хватает поддержки предыдущего поколения продуктов Windows. Например, в WPF нет никакого встроенного способа для создания приложений MDI, окон с вкладками и стыкуемых окон. Все эти недостающие функции можно получить, приложив немного дополнительных усилий, но зачастую добиться идеального результата все равно очень трудно. По этой причине многие WPFразработчики, предпочитающие использовать основанный на применении окон дизайн, склонны обращаться к сторонним компонентам, по крайней мере, периодически.
Book_Pro_WPF-2.indb 247
19.05.2008 18:10:07
ГЛАВА
9
Страницы и навигация В
основе большинства традиционных приложений Windows лежит окно с различными панелями инструментов и меню. Панели инструментов и меню являются “двигателем” приложения — когда пользователь на них щелкает, происходит какое-то действие, и появляются другие окна. В документных приложениях может существовать несколько одинаковых по степени важности “главных” окон, которые открываются одновременно, но в целом модель та же. Пользователь проводит большую часть своего времени в какомто одном месте и переходит в другие отдельные окна, только когда это необходимо. Приложения Windows являются настолько привычными, что порой даже бывает трудно представить, каким еще образом можно разработать приложение. Однако создатели настольных приложений в последние несколько лет очень внимательно следили за разработками в Web, где применяется совершенно другая модель, основанная на использовании страниц, и осознали, что она удивительно хорошо подходит для построения определенных типов приложений. В качестве попытки предоставить этим разработчикам возможность создавать настольные приложения в стиле Web, в состав WPF была включена специальная система страничной навигации, которая, как читатель увидит в этой главе, представляет собой удивительно гибкую модель. В настоящее время страничная модель чаще всего применятся в простых, несерьезных приложениях (или для реализации небольших наборов функциональных возможностей в более сложных оконных приложениях). Однако страничные приложения являются прекрасным выбором, если требуется упростить процесс разработки. А все дело в том, что WPF позволяет создавать страничные приложения, запускающиеся непосредственно внутри браузера Internet Explorer или Firefox с ограниченным уровнем доверия. Это дает пользователям возможность запускать эти приложения, не выполняя никакой явной установки — от них требуется просто указать своим браузерам на нужное место и все. С этой моделью, которая называется моделью XBAP, читатель сможет более подробно ознакомиться во второй половине этой главы.
Общие сведения о страничной навигации Обычное Web-приложение на вид значительно отличается от традиционного клиентского программного обеспечения с множеством функций. Пользователи Web-сайта проводят время, перемещаясь с одной страницы на другую. Если не считать всплывающие рекламные сообщения, они никогда не видят одновременно более одной страницы. При решении задачи (например, размещении заказа или выполнении сложного поиска), им приходится проходить эти страницы в линейной последовательности от начала до конца. HTML не поддерживает сложных оконных возможностей настольных операционных систем, поэтому профессиональные Web-разработчики всегда полагаются на качествен-
Book_Pro_WPF-2.indb 248
19.05.2008 18:10:07
Страницы и навигация
249
ный дизайн и четкие понятные интерфейсы. Поскольку технологии Web-дизайна значительно усложнились, разработчики приложений Windows тоже начали замечать преимущества такого подхода. Но важнее всего то, что Web-модель является простой и хорошо отлаженной. Именно по этой причине пользователям-новичками часто оказывается легче разобраться в использовании Web-сайтов, чем в работе с Windows-приложениями, хотя очевидно, что вторые обладают куда большим количеством возможностей. В последнее время разработчики начали имитировать некоторые из соглашений Web и в настольных приложениях. Программное обеспечение для финансовых операций, подобное Microsoft Money, является главным примером использования Web-интерфейсов, проводящих пользователей через ряд определенных задач. Однако создание таких приложений часто оказывается более сложным процессом, чем создание традиционного оконного приложения, поскольку требует от разработчиков воссоздания базовых функциональных возможностей браузера, например, навигации. На заметку! В некоторых случаях разработчики создают Web-приложения, используя механизм браузера Internet Explorer. Именно такой подход как раз и применялся при разработке Microsoft Money, но для разработчиков, не имеющих дело с продуктами Microsoft, он будет слишком сложным. Хотя Microsoft и предоставляет привязки для Internet Explorer вроде элемента управления WebBrowser, создание целого приложения на основе эти функциональных возможностей является далеко не простой задачей и к тому же чревато потерей наилучших возможностей, предлагаемых традиционными Windows-приложениями. Благодаря WPF, больше нет причин искать какой-то компромисс, потому что в состав WPF входит встроенная модель страниц с уже готовыми средствами навигации. Лучше всего то, что эту модель можно применять для создания самых разнообразных страничных приложений, приложений, использующих только какие-то определенные страничные функции (например, в мастере или справочной системе), или приложений, функционирующих непосредственно в браузере.
Страничные интерфейсы Чтобы создать страничное приложение в WPF, нужно перестать применять для пользовательских интерфейсов в качестве контейнера наивысшего уровня класс Window и вместо него переключиться на класс System.Windows.Controls.Page. Модель для создания страниц в WPF во многом похожа на модель для создания окон. Хотя создавать объекты страниц можно и с помощью одного лишь кода, обычно для каждой страницы создается файл XAML и файл отделенного кода (code-behind file). При компиляции этого приложения компилятор создает производный класс страницы, который объединяет написанный разработчиком код с генерируемыми автоматически связующими элементами (наподобие полей, которые ссылаются на каждый именованный элемент на странице). Это тот же самый процесс, который описывался при рассмотрении компиляции оконных приложений в главе 2. На заметку! Страницу можно добавлять в любой проект WPF. Для этого в Visual Studio нужно выбрать из меню Project (Проект) команду Add Page (Добавить страницу). Хотя страницы и являются самым высокоуровневым компонентом пользовательского интерфейса при проектировании приложения, при его выполнении контейнером наивысшего уровня они уже не являются. Вместо этого они обслуживаются в другом контейнере. Именно в этом и состоит секрет гибкости, обеспечиваемой WPF в случае
Book_Pro_WPF-2.indb 249
19.05.2008 18:10:07
250
Глава 9
страничных приложений, ведь в качестве такого контейнера WPF позволяет использовать любой из нескольких следующих объектов:
• объект NavigationWindow, который представляет собой немного видоизмененную версию класса Window;
• объект Frame, находящийся внутри другого окна; • объект Frame, находящийся внутри другой страницы; • объект Frame, обслуживаемый непосредственно в Internet Explorer или Firefox. Обо всех них более подробно будет рассказываться далее в этой главе.
Простое страничное приложение с элементом NavigationWindow В качестве примера простейшего страничного приложения давайте создадим следующую страницу: This is a simple page. OK Close
Теперь изменим содержимое файла App.xaml так, чтобы в качестве начальной страницы использовался файл нашей страницы:
При запуске этого приложения WPF хватит “интеллектуальных способностей”, чтобы понять, что мы указываем ей на страницу, а не на окно. Она автоматически создаст новый объект NavigationWindow для выполнения роли контейнера и отобразит нашу страницу внутри него (рис. 9.1). Она также считает свойство WindowTitle и использует его значение в качестве заголовка окна. На заметку! Одно из отличий между страницей и окном заключается в том, что размер страницы обычно не устанавливается, поскольку он определяется обслуживающим ее контейнером (хостом). Если же для свойств Width и Height страницы все-таки устанавливаются какие-то значения, страница делается именно такого размера, но часть ее содержимого может быть усеченной, если размер хост-окна оказывается меньше, или размещенной по центру доступного пространства, если его размер оказывается больше. Объект NavigationWindow более или менее похож на обычное окно, за исключением кнопок навигации вперед и назад, которые отображаются в строке сверху. Поэтому не-
Book_Pro_WPF-2.indb 250
19.05.2008 18:10:07
Страницы и навигация
251
трудно догадаться, что класс NavigationWindow наследуется от класса Window и имеет небольшой дополнительный набор связанных с навигацией свойств.
Рис. 9.1. Страница в контейнере NaviagationWindow Извлечь ссылку на содержащий объект NavigationWindow можно с помощью следующего кода: // Извлечение ссылки на окно, содержащее текущую страницу. NavigationWindow win = (NavigationWindow)Window.GetWindow(this);
В конструкторе страницы этот код работать не будет, потому что на этом этапе страница пока что еще не находится внутри своего контейнера, поэтому нужно дождаться хотя бы, когда сработает событие Page.Loaded. Совет. Если возможно, такого подхода лучше вообще избегать и использовать вместо него свойства класса Page (и службу навигации, о которой еще будет рассказываться в этой главе). Иначе страница будет тесно связана с контейнером NavigationWindow, и потому ее нельзя будет использовать повторно в других хостах. При желании создать приложение, состоящее только из кода, для достижения эффекта, показанного на рис. 9.1, потребовалось бы создавать как страницу, так и навигационное окно. Код, который пришлось бы для этого использовать, показан ниже. NavigationWindow win = new NavigationWindow() win.Content = new Page1(); win.Show();
Класс Page Подобно Window, класс Page допускает наличие только одного единственного вложенного элемента. Однако класс Page не является элементом управления содержимым: он на самом деле наследуется непосредственно от класса FrameworkElement. Вдобавок класс Page является более простым и отлаженным чем класс Window. Он имеет небольшой набор дополнительных свойств, которые позволяют настраивать его внешний вид, взаимодействовать с контейнером только определенным, ограниченным образом и использовать навигацию. Все эти свойства перечислены в табл. 9.1.
Book_Pro_WPF-2.indb 251
19.05.2008 18:10:08
252
Глава 9
Таблица 9.1. Свойства класса Page Имя
Описание
Background
Принимает кисть, которая позволяет устанавливать заливку для фона.
Content
Принимает один элемент, который отображается на странице. Обычно в роли такого элемента выступает контейнер макета вроде элемента Grid или StackPanel.
Foreground, FontFamily и FontSize
Определяют используемый по умолчанию внешний вид для текста внутри страницы. Значения этих свойств наследуются элементами внутри страницы. Например, если устанавливается заливка переднего плана и размер шрифта, по умолчанию содержимое внутри страницы получает эти же настройки.
WindowWidth, WindowHeight и WindowTitle
Определяют внешний вид окна, в которое упаковывается страница. Эти свойства позволяют управлять хостом путем установки его ширины, высоты и заголовка. Однако они действуют только в том случае, если страница обслуживается в окне (а не во фрейме).
NavigationService
Возвращает ссылку на объект NavigationService, которую можно использовать для отправки пользователя на другую страницу программным путем.
KeepAlive
Определяет, должен ли объект страницы оставаться действующим после перехода пользователя на другую страницу. Об этом свойстве более подробно будет рассказываться далее в этой главе (в разделе “Хронология навигации”) при рассмотрении возможности восстановления страниц из хронологии навигации.
ShowsNavigationUI
Определяет, должны ли в обслуживающем данную страницу хосте отображаться навигационные элементы управления (т.е. кнопки “назад” и “вперед”). По умолчанию имеет значение true.
Title
Устанавливает имя, которое должно применяться для страницы в хронологии навигации. Хост не использует свойство Title для установки заголовка в строке заголовка: для этой цели у него есть свойство WindowTitle.
Также важно обратить внимание на отсутствующие компоненты, в частности на то, что в классе Page нет эквивалентов для методов Hide() и Show(), доступных в классе Window. Если потребуется показать другую страницу, придется воспользоваться навигацией.
Гиперссылки Наиболее простой способ позволить пользователю перемещаться с одной страницы на другую — это гиперссылки. В WPF гиперссылки являются не отдельными, а внутристрочными потоковыми элементами, которые обязательно должны размещаться внутри другого поддерживающего их элемента. (Причина такого дизайна связана с тем, что гиперссылки и текст часто используются вперемешку. Подробнее о потоковом содержимом и компоновке текста речь пойдет в главе 19.) Например, ниже показано объединение текста и ссылок в элементе TextBlock, который является самым практичным контейнером для гиперссылок. This is a simple page. Click here to go to Page2.
Book_Pro_WPF-2.indb 252
19.05.2008 18:10:08
Страницы и навигация
253
Рис. 9.2. Ссылка на другую страницу Щелчки на ссылке можно обрабатывать двумя способами: можно реагировать на событие Click и использовать код для выполнения какой-нибудь задачи, а можно просто направлять пользователя на другую страницу. Однако существует и более простой подход. Класс Hyperlink также включает свойство NavigateUri, которое можно устанавливать так, чтобы оно указывало на любую другую страницу в приложении. В таком случае при щелчке на гиперссылке пользователи будут попадать на целевую страницу автоматически. На заметку! Свойство NavigateUri работает только в том случае, если гиперссылка размещается на странице. При желании использовать гиперссылку в оконном приложении для позволения пользователям выполнять какую-нибудь задачу, запускать какую-то Web-страницу или открывать новое окно, придется обрабатывать событие RequestNavigate и писать код самостоятельно. Гиперссылки не являются единственным способом для перехода с одной страницы на другую. NavigationWindow включает две заметные кнопки: “назад” и “вперед” (если только они не скрываются путем установки для свойства Page.ShowsNavigationUI значения false). Щелкая на этих кнопках, пользователи могут перемещаться по навигационной последовательности на одну страницу назад или на одну страницу вперед. Как и в окне браузера, пользователи также еще могут щелкать на стрелке раскрывающегося списка, отображаемой по краям этих кнопок, и просматривать всю последовательность, а также “перепрыгивать” сразу на несколько страниц назад или вперед (рис. 9.3).
Рис. 9.3. Хронология просмотренных страниц Подробнее о том, как работает хронология страниц и какие у нее имеются ограничения, будет рассказываться далее в этой главе, в разделе “Хронология навигации”.
Book_Pro_WPF-2.indb 253
19.05.2008 18:10:08
254
Глава 9
На заметку! В случае перехода на новую страницу, у которой нет свойства WindowTitle, у окна сохраняется тот же заголовок, который у него был на предыдущей странице. Если свойство WindowTitle не устанавливается ни на одной странице, заголовок окна остается пустым.
Навигация по Web-сайтам Интересно то, что также можно создавать и гиперссылку, указывающую на Web-содержимое. Когда пользователь щелкает на такой ссылке, в области страницы загружается целевая Web-страница: Visit the website www.prosetech.com.
Однако, используя такой прием, следует обязательно присоединять обработчик событий к событию Application.DispatcherUnhandledException или Application. NavigationFailed. Дело в том, что попытка посещения Web-сайта может оказаться неудачной, если компьютер не подключен к сети, сайт недоступен или Web-содержимое отсутствует. В таком случае из сетевого стека возвращается ошибка вроде “404: File Not Found” (404: файл не найден), которая воплощается в исключение WebException. Для аккуратной обработки этого исключения и предотвращения неожиданного завершения работы приложения его необходимо нейтрализовать с помощью следующего обработчика: private void App_NavigationFailed(object sender, NavigationFailedEventArgs e) { if (e.Exception is System.Net.WebException) { MessageBox.Show("Website " + e.Uri.ToString() + " cannot be reached."); // Не удается найти Web-сайт // Нейтрализация ошибки так, чтобы приложение продолжало свою работу. e.Handled = true; } }
NavigationFailed — это всего лишь одно из нескольких навигационных событий, которые определяются в классе Application. Полный список будет приведен позже, в табл. 9.2. На заметку! Попав на Web-страницу, пользователи смогут щелкать на доступных на ней ссылках и переходить на другие Web-страницы, оставляя ваше содержимое далеко позади. На самом деле, они смогут вернуться на вашу WPF-страницу только в том случае, если воспользуются хронологией навигации для возврата назад, или если эта страница будет отображаться в специальном окне (о котором более подробно будет рассказываться далее в этой главе), и это окно будет включать элемент управления, позволяющий вернуться обратно к вашему содержимому. В случае отображения страниц с внешних Web-сайтов нельзя делать много вещей. Например, нельзя запретить пользователю переходить на какие-то конкретные страницы или сайты. Также нельзя и взаимодействовать с Web-страницей с помощью объектной модели документов HTML DOM. Это означает, что сканировать страницу для поиска ссылок или изменять ее динамически тоже нельзя. Выполнение всех этих задач становится возможным только в случае использования элемента управления WebBrowser, который входит в состав Windows Forms. О функциональной совместимости Windows Forms более подробно будет рассказываться в главе 25.
Book_Pro_WPF-2.indb 254
19.05.2008 18:10:08
Страницы и навигация
255
Навигация по фрагментам Последним приемом, который можно использовать с гиперссылкой, является навигация по фрагментам. Добавив знак # в конце NavigateUri, а за ним — имя элемента, можно сразу же переходить к конкретному элементу управления на странице. Однако такой прием работает, только если целевая страница является прокручиваемой (а таковой она является тогда, когда использует элемент управления ScrollViewer или обслуживается в Web-браузере). Ниже приведен соответствующий пример: Review the full text.
Когда пользователь щелкает на этой ссылке, приложение переходит на страницу под названием Page2 и прокручивает ее до элемента по имени myTextBox. Страница прокручивается вниз до тех пор, пока элемент myTextBox не появится в самом ее верху (или насколько возможно близко к ее верхнему краю, что зависит от размера содержимого страницы и содержащего окна). Однако фокуса целевой элемент не получает.
Размещение страниц во фрейме Элемент NavigationWindow является удобным контейнером, но не единственным вариантом. Страницы также можно размещать и непосредственно внутри других окон или даже внутри других страниц. Это подразумевает возможность создания чрезвычайно гибкой системы, поскольку означает, что одну и ту же страницу можно использовать многократно разными способами в зависимости от типа приложения, которое требуется создать. Чтобы вставить страницу внутрь окна, нужно воспользоваться классом Frame. Класс Frame представляет собой элемент управления содержимым, который может удерживать любой элемент, но особенно полезен именно в качестве контейнера для страницы. Он включает свойство под названием Source, которое указывает на подлежащую отображению страницу XAML. Ниже показан код обычного окна, которое упаковывает кое-какое содержимое в элементе StackPanel и размещает элемент Frame в отдельном столбце: This is ordinary window content. Close
Book_Pro_WPF-2.indb 255
19.05.2008 18:10:08
256
Глава 9
На рис. 9.4 показан результат. Граница вокруг фрейма (элемента Frame) отображает содержимое страницы. Останавливаться на одном фрейме не обязательно. Можно легко создать и окно с множеством фреймов и указать им всем на разные страницы.
Рис. 9.4. Окно со страницей, вставленной во фрейм Как видно на рис. 9.4, в этом примере отсутствуют знакомые кнопки навигации. Дело в том, что для свойства Frame.NavigationUIVisibility по умолчанию устанавливается значение Automatic. Из-за этого навигационные кнопки появляются только тогда, когда в списке посещений уже присутствуют какие-то страницы. Чтобы проверить это, перейдите на новую страницу, и вы увидите, как внутри фрейма появятся эти кнопки (рис. 9.5).
Рис. 9.5. Фрейм с кнопками для навигации Значение свойства NavigationUIVisibility можно изменить на Hidden, если необходимо, чтобы навигационные кнопки не отображались никогда, или на Visible, если требуется, чтобы они отображались с самого начала. Наличие навигационных кнопок внутри фрейма является хорошим дизайном, если во фрейме находится содержимое, отделенное от основного потока приложения (например, он служит для отображения контекстно-зависимой справки или содержания последовательного руководства). Но в других случаях может быть нужно, чтобы они отображались в верхней части окна. Для этого потребуется изменить контейнер наивысшего уровня с Window на NavigationWindow. В таком случае окно будет включать навигаци-
Book_Pro_WPF-2.indb 256
19.05.2008 18:10:08
Страницы и навигация
257
онные кнопки. Находящийся внутри этого окна фрейм автоматически привяжет себя к этим кнопкам, благодаря чему пользователь получит внешний вид, подобный показанному на рис. 9.3, за исключением разве что того, что в данном окне сейчас также присутствует дополнительное содержимое. Совет. В окно можно добавлять столько объектов Frame, сколько нужно. Например, с помощью трех отдельных фреймов можно запросто создать окно, позволяющее пользователю просматривать задачи приложения, справочную документацию и внешний Web-сайт.
Размещение страниц в другой странице Объекты Frame дают возможность создавать более сложные композиции окон. Как уже упоминалось в предыдущем разделе, в одном окне можно использовать сразу несколько фреймов (объектов Frame). Однако, помимо этого, фрейм еще также можно размещать внутри другой страницы и тем самым создавать так называемую вложенную страницу. На самом деле этот процесс выглядит абсолютно точно так же — объект Frame просто добавляется внутрь разметки страницы. Вложенные страницы представляют более сложную ситуацию в плане навигации. Например, предположим, что вы посещаете страницу и затем щелкаете на ссылке во вложенном фрейме. Что произойдет, если вы после этого щелкните на кнопке возврата? По сути, все страницы во фрейме выстраиваются в один список. Так что при первом щелчке на кнопке возврата вы вернетесь на предыдущую страницу во вставленном фрейме, а если щелкните на этой кнопке еще раз, то вернетесь уже на посещенную до этого родительскую страницу. Эта последовательность действий проиллюстрирована на рис. 9.6. Обратите внимание, что навигационная кнопка возврата назад становится доступной только на втором шаге.
Рис. 9.6. Навигация с вложенной страницей Такая модель навигации является по большей части достаточно понятной, поскольку предполагает наличие в списке предыдущих страниц по одному элементу для каждой посещенной страницы. Однако бывают случаи, когда вложенный фрейм играет менее важную роль, например, показывает различные представления одних и тех же данных или позволяет просматривать многочисленные страницы справочного содержимого. В таких случаях проход по всем страницам во вложенном фрейме может показаться неудобным или отнимающим много времени процессом и привести к желанию исполь-
Book_Pro_WPF-2.indb 257
19.05.2008 18:10:08
258
Глава 9
зовать навигационные элементы для управления навигацией только родительского фрейма, т.е. сделать так, чтобы при щелчке на кнопке возврата пользователь сразу же попадал на предыдущую родительскую страницу. Для получения такого эффекта потребуется установить для свойства JournalOwnership вложенного фрейма значение OwnsJournal. Это заставит фрейм применять свою собственную, отдельную хронологию страниц, в результате чего он также еще по умолчанию получит свои собственные навигационные кнопки, позволяющие перемещаться назад и вперед именно по его собственному содержимому (рис. 9.7). Если эти кнопки не нужны, к свойству JournalOwnership можно будет добавить свойство NavigationUIVisibility и с его помощью просто скрыть их, как показано ниже:
После этого вложенный фрейм будет восприниматься просто как фрагмент динамического содержимого внутри страницы. С точки зрения пользователя никаких навигационных возможностей у него не будет.
Рис. 9.7. Вложенная страница, которая владеет своим журналом и поддерживает навигацию
Размещение страниц в Web-браузере Последний способ использования страничных приложений с возможностями навигации подразумевает применение Internet Explorer. Однако такой подход требует создания приложения XBAP (XAML Browser Application — браузерное приложение XAML). В Visual Studio приложение XBAP представляет собой отдельный шаблон проекта и должно специально выбираться (вместо стандартного приложения WPF Windows) при создании проекта и желании получить возможность обслуживания страниц в браузере. Более подробно модель XBAP будет рассматриваться чуть позже в этой главе.
Получение окна с правильными размерами На самом деле существуют два типа страничных приложений, которые описаны ниже. • Автономные приложения Windows, в которых страницы используются для части или всего их пользовательского интерфейса. Такой подход следует применять при необходимости интегрировать в приложение мастер или желании создать простое приложение, ориентированное на выполнение каких-то задач. В таком случае навигационные и журнальные функции позволят упростить кодирование.
Book_Pro_WPF-2.indb 258
19.05.2008 18:10:09
Страницы и навигация
259
• Браузерные приложения (приложения XBAP), которые обслуживаются в Internet Explorer и имеют ограниченные полномочия. Такой подход следует применять при желании получить облегченную, основанную на Web-технологиях модель развертывания. Тем, кто создает приложение первого типа, не нужно устанавливать свойство Application. StartupUri так, чтобы оно указывало на страницу. Вместо этого можно создать объект NavigationWindow вручную и затем загрузить свою первую страницу внутри него (как показывалось ранее) или же вставить свои страницы в специальное окно с помощью элемента управления Frame. И в том и в другом случае будет возможность установить размер окна приложения, что является важным для придания приложению презентабельного вида при его первом запуске. У тех же, кто создает приложение второго типа, возможностей изменить размер содержащего окна Web-браузера не будет, поэтому им обязательно следует устанавливать свойство StartupUri так, чтобы оно указывало на страницу.
Хронология страниц Теперь, когда мы рассказали о страницах и различных способах их размещения, можно переходить к более подробному рассмотрению используемой WPF модели навигации. В этом разделе речь пойдет о том, как именно работают гиперссылки WPF и каким образом восстанавливаются страницы, когда пользователь снова к ним возвращается.
Более детальное рассмотрение URI-адресов в WPF Вам наверняка интересно узнать, как именно работают свойства вроде Application. StartupUri, Frame.Source и Hyperlink.NavigateUri. В приложении, которое состоит из несвязанных XAML-файлов и выполняется в браузере, этот процесс выглядит довольно просто: при щелчке на гиперссылке браузер интерпретирует ссылку на страницу как относительный URI-адрес и ищет указанную XAML-страницу в текущей папке. Но в скомпилированном приложении, страницы перестают быть доступными в виде отдельных ресурсов: они компилируются в XAML и вставляются в сборку. А как на них можно ссылаться с помощью URI? Эта система работает благодаря способу, которым WPF обращается к ресурсам приложения (об этом пойдет речь в главе 11). При щелчке на гиперссылке в скомпилированном XAML-приложении URI все равно интерпретируется как относительный путь. Однако он является относительным по отношению к базовому URI приложения. Гиперссылка, указывающая на Page1.xaml, фактически интерпретируется следующим образом: pack://application:,,,/Page1.xaml
Этот синтаксис называется упакованным URI (pack URI) и состоит из трех частей:
• схема (pack://), указывающая способ, которым можно найти ресурс; • авторитетный источник (application:,,,), указывающий на контейнер, в котором содержится ресурс (в данном случае таковым является сборка);
• путь (/Page1.xaml), указывающий на точное местонахождение этого ресурса относительно контейнера. Другими словам упакованный URI — это путь, который извлекает скомпилированный XAML-ресурс из сборки. С этой системой связано несколько преимуществ. Относительные URI-адреса можно использовать в гиперссылках, и они будут работать независимо от того, является ли
Book_Pro_WPF-2.indb 259
19.05.2008 18:10:09
260
Глава 9
приложение скомпилированным или (что менее типично) хранится в виде несвязанных XAML-файлов. Сейчас у читателя наверняка возник вопрос о том, зачем же тогда нужно знать, как работают URI-адреса XAML, если весь процесс проходит так гладко. Главная причина состоит в том, что может потребоваться создать приложение, позволяющее переходить на XAML-страницы, которые хранятся в другой сборке. На самом деле для такого дизайна есть веские основания. Поскольку страницы могут применяться в разных контейнерах, запросто может возникнуть желание повторно использовать один и тот же набор страниц как в приложении XBAP, так и в обычном приложении Windows. В таком случае можно развернуть просто две версии приложения — браузерную и настольную. Чтобы избежать дублирования кода, все страницы, которые планируется использовать повторно, следует поместить в отдельную сборку библиотеки классов (DLL), на которую затем можно сослаться в обоих проектах приложений. Это потребует внесения изменения в URI-адреса. При наличии страницы в одной сборке, указывающей на страницу в другой сборке, нужно будет использовать следующий синтаксис: pack://application:,,,/PageLibrary;component/Page1.xaml
Здесь компонент называется PageLibrary, а путь ,,,PageLibrary;component/ Page1.xaml указывает на скомпилированную и вставленную внутри него страницу Page1.xaml. Конечно, вы вряд ли пожелаете использовать абсолютный путь. Вместо него логичнее будет применять в URI-адресах следующий более короткий относительный путь: /PageLibrary;component/Page1.xaml
Совет. При создании сборки SharedLibrary для получения правильных ссылок, импортированных пространств имен и параметров приложения следует использовать шаблон проекта под названием Custom Control Library (WPF) (Библиотека специальных элементов управления (WPF)).
Хронология навигации Хронология страниц в WPF работает точно так же, как и в браузере. При каждом переходе на новую страницу текущая страница добавляется в список предыдущих страниц. При щелчке на кнопке возврата страница добавляется в список следующих страниц. В случае возвращения на одну страницу и перехода уже с нее на новую страницу список следующих страниц очищается. Поведение списков предыдущих и следующих страниц выглядит довольно просто, но внутренние механизмы, обеспечивающие работу этих списков, являются гораздо более сложными. Например, предположим, что вы посещаете страницу с двумя текстовыми полями, вводите в них что-нибудь и двигаетесь дальше. Если вы после этого вернетесь обратно на эту страницу, то увидите, как WPF восстановит состояние текстовых полей, т.е. отобразит в них снова все, что вы вводили. На заметку! Между возвращением на страницу через хронологию навигации и выполнением щелчка на ссылке, которая направляет на эту же самую страницу, существует большая разница. Например, при переходе со страницы Page1 на страницу Page2 и затем снова на страницу Page1 с помощью соответствующих ссылок WPF создаст три отдельных объекта страницы. При втором отображении страницы Page1 WPF создаст ее уже как отдельный экземпляр со своим собственным состоянием. Однако в случае возврата на первую страницу Page1 за счет двукратного выполнения щелчка на кнопке возврата WPF сохранит ее в исходном состоянии.
Book_Pro_WPF-2.indb 260
19.05.2008 18:10:09
Страницы и навигация
261
Вы можете предположить, что WPF поддерживает состояние предыдущих посещенных страниц путем удержания объекта страницы в памяти. Проблема такого дизайна заключается в том, что связанные с памятью накладные расходы в сложном приложении с множеством страниц в таком случае могут оказаться слишком большими. Поэтому WPF не может считать поддержание объекта страницы безопасной стратегией. Вместо этого при покидании страницы WPF сохраняет информацию о состоянии всех ее элементов управления и затем уничтожает ее. При возврате на эту страницу WPF просто создает страницу заново (из исходного XAML-файла) и затем восстанавливает состояние элементов управления. Такая стратегия означает меньшее количество накладных расходов, поскольку для сохранения нескольких деталей о состоянии элементов управления требуется гораздо меньше памяти, нежели для хранения страницы и всего ее визуального дерева объектов. Глядя на эту систему, возникает один интересный вопрос, а именно — каким образом WPF решает, какие детали нужно сохранить? А вот каким: WPF анализирует все дерево элементов страницы и просматривает имеющиеся у этих элементов свойства зависимостей. У свойств, подлежащих сохранению, имеется один небольшой фрагмент дополнительных метаданных — флаг журнала, который указывает, что они должны помещаться в журнал навигации. Этот флаг устанавливается с помощью объекта FrameworkPropertyMetadata при регистрации свойства зависимостей, как описывалось в главе 6. Присмотревшись к системе навигации поближе, вы увидите, что у многих свойств нет флага журнала. Например, если вы установите свойство Content элемента управления содержимым или свойство Text элемента TextBlock с помощью кода, ни одна из этих деталей не будет восстановлена при возвращении на страницу. То же самое будет и, если вы динамически установите свойство Foreground или Background. Однако если вы установите свойство Text элемента TextBox, свойство IsSelected элемента CheckBox или свойство SelectedIndex элемента ListBox, все эти детали останутся. А что можно сделать, если такое поведение не подходит, т.е. если вы устанавливаете множество свойств динамически и хотите, чтобы ваши страницы сохраняли всю их информацию? Существует несколько вариантов. Самый мощный предполагает применение свойства Page.KeepAlive, которое по умолчанию имеет значение false. Когда для этого свойства устанавливается значение true, WPF не применяет описывавшийся выше механизм сериализации. Вместо этого WPF оставляет объекты всех страниц в действующем состоянии. Благодаря этому, при возврате обратно на страницу она оказывается именно в таком виде, в котором и была. Конечно, у этого варианта есть один недостаток, заключающийся в увеличении накладных расходов, связанных с памятью, поэтому его следует применять только для нескольких страниц, которые действительно в нем нуждаются. Совет. В случае использования свойства KeepAlive для сохранения страницы в действующем состоянии, при следующем возврате к ней событие Initialized генерироваться не будет. (Для страниц, которые не сохраняются в действующем состоянии, а “возвращаются к жизни” с помощью системы журнализации WPF, такое событие будет инициироваться при каждом их посещении пользователем.) Если такое поведение не подходит, тогда следует обработать события Unloaded и Loaded, которые генерируются всегда. Второй вариант — выбрать другой дизайн, способный передавать информацию по кругу. Например, можно создать предназначенные для возврата информации страничные функции (описываемые далее в этой главе). Используя страничные функции вместе с дополнительной логикой инициализации, можно разработать свою собственную систему для извлечения из страницы важной информации и ее сохранения в подходящем месте.
Book_Pro_WPF-2.indb 261
19.05.2008 18:10:09
262
Глава 9
С хронологией навигации WPF связан еще один недостаток. Как будет показано далее в главе, можно написать код, который будет динамически создавать объект страницы и затем переходить на нее. В такой ситуации обычный механизм сохранения состояния страницы работать не будет. У WPF нет ссылки на XAML-документ страницы, поэтому ей не известно, как реконструировать страницу. (А если страница создается динамически, у нее может и вообще не быть соответствующего XAML-документа.) В такой ситуации WPF всегда будет сохранять объект страницы в памяти, каким бы ни было значение свойства KeepAlive.
Добавление специальных свойств Обычно все поля в классе страницы утрачивают свои значения при уничтожении этой страницы. При желании добавить в класс страницы какие-то специальные свойства и сделать так, чтобы они сохраняли свои значения, можно просто соответствующим образом установить флаг журнала. Однако предпринять подобное действие в отношении обычного свойства или поля нельзя. Поэтому в таком случае потребуется создать в классе страницы свойство зависимостей. О свойствах зависимостей уже рассказывалось в главе 6. Чтобы создать свойство зависимостей, необходимо выполнить два шага. Во-первых, нужно создать определение свойства зависимостей, а во-вторых — добавить процедуру обычного свойства, устанавливающую или извлекающую значение этого свойства зависимостей. Чтобы определить свойство зависимостей, необходимо создать статическое поле, подобное показанному ниже: private static DependencyProperty MyPageDataProperty;
По соглашению поле, определяющее свойство зависимостей, должно иметь точно такое же имя, как и обычное свойство, но со словом Property в конце. На заметку! В данном примере используется приватное свойство зависимостей. А все дело в том, что единственный код, которому нужно получать доступ к этому свойству, находится в классе страницы, где оно и определено. Чтобы завершить определение, необходим статический конструктор, регистрирующий определение свойства зависимостей. Именно здесь указываются службы, которые должны применяться со свойством зависимостей (вроде поддержки для связывания данных, анимации и журнализации): static PageWithPersistentData() { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.Journal = true; MyPageDataProperty = DependencyProperty.Register( "MyPageDataProperty", typeof(string), typeof(PageWithPersistentData), metadata, null); }
Далее можно создать обычное свойство, упаковывающее данное свойство зависимостей. Однако при написании процедур извлечения и установки следует использовать методы GetValue() и SetValue(), которые определяются в базовом классе DependencyObject: private string MyPageData { set { SetValue(MyPageDataProperty, value); } get { return (string)GetValue(MyPageDataProperty); } }
Book_Pro_WPF-2.indb 262
19.05.2008 18:10:09
Страницы и навигация
263
Теперь осталось только добавить все эти детали в одну страницу (в данном примере это страница PageWithPersistentData); после этого значение свойства MyPageData будет автоматически подвергаться сериализации при покидании пользователем этой страницы и восстанавливаться при его возвращении.
Служба навигации Пока что все рассмотренные возможности навигации подразумевали в основном применение гиперссылок. Когда такой подход работает, он элегантен и прост. Однако в некоторых случаях бывает необходимо иметь больший контроль над процессом навигации. Например, гиперссылки прекрасно подходят, если страницы используются для воспроизведения фиксированной, линейной последовательности шагов, которую пользователь должен проходить от начала до конца (вроде мастера). Однако если нужно, чтобы пользователь выполнял только какие-то небольшие последовательности шагов и возвращался на общую страницу, или же требуется сконфигурировать последовательность шагов на основании каких-нибудь других деталей (например, предыдущих действий пользователя), необходимо нечто большее.
Программная навигация Можно сделать так, чтобы значения для свойств Hyperlink.NavigateUri и Frame. Source устанавливались динамически. Однако самый гибкий и мощный подход предполагает использование службы навигации WPF. Получать доступ к этой самой службе навигации можно через контейнер, в котором размещается страница (например, объект Frame или NavigationWindow), но такой подход ограничивает страницы так, что после этого их можно использовать в контейнере только такого типа. Поэтому лучше всего получать доступ к службе навигации через статический метод NavigationService. GetNavigationService(). Вы просто передаете этому методу ссылку на свою страницу, а он возвращает действующий объект NavigationService, позволяющий реализовать навигацию программно: NavigationService nav; nav = NavigationService.GetNavigationService(this);
Этот код работает независимо от того, какой контейнер используется для обслуживания страниц. На заметку! Объект NavigationService нельзя использовать ни в конструкторе страницы, ни на этапе срабатывания события Page.Initialized. Больше всего для этого подходит событие Page.Loaded. Класс NavigationService предлагает ряд методов, которые можно применять для запуска навигации. Наиболее популярным из них является метод Navigate(). Он позволяет переходить на страницу на основании ее URI: nav.Navigate(new System.Uri("Page1.xaml", UriKind.RelativeOrAbsolute));
или за счет создания соответствующего объекта страницы: Page1 nextPage = new Page1(); nav.Navigate(nextPage);
Если возможно, лучше применять URI, поскольку это позволяет системе журнализации WPF сохранять данные страницы, не удерживая в памяти все дерево ее объектов. Когда методу Navigate() передается объект страницы, в памяти сохраняется весь объект.
Book_Pro_WPF-2.indb 263
19.05.2008 18:10:09
264
Глава 9
Однако создание объекта страницы вручную может быть и вынужденной мерой, если в страницу требуется передать какую-нибудь информацию. Передать эту информацию можно либо с помощью специального конструктора класса страницы (что является наиболее распространенным подходом), либо путем вызова в данном классе страницы после его создания еще одного специального метода. В случае добавления в страницу нового конструктора следует обязательно сделать так, чтобы он вызывал метод InitializeComponent() для обработки кода разметки и создания объектов элементов управления. На заметку! Решив использовать программную навигацию, вы сами определяете, что применять: навигационные кнопки, гиперссылки или что-нибудь еще. Как правило, для принятия решения о том, на какую страницу следует переходить, в обработчике событий используется условный код. Навигация в WPF осуществляется асинхронным образом. А это значит, что запрос на навигацию можно отменять до того, как он будет выполнен, посредством вызова метода NavigationService.StopLoading(). Вдобавок еще можно использовать и метод Refresh() для повторной загрузки страницы. И, наконец, класс NavigationService еще также предоставляет методы GoBack() и GoForward(), которые позволяют перемещаться по спискам предыдущих и следующих страниц. Это удобно в ситуации, когда разработчик создает свои собственные навигационные элементы управления. Оба эти метода выдают исключение InvalidOperation Exception при попытке перехода на страницу, которой не существует (например, при попытке вернуться назад, находясь на первой странице). Во избежание таких ошибок перед вызовом соответствующих методов следует проверять значения булевских свойств CanGoBack и CanGoForward.
События навигации Класс NavigationService также предоставляет полезный набор событий, которые можно использовать для реагирования на навигацию. Наиболее распространенной причиной реагирования на навигацию является необходимость выполнения по ее завершении какой-то задачи. Например, если страница размещается внутри рамки в обычном окне, может быть нужно, чтобы по завершении навигации в окне обновлялся текст строки состояния. Поскольку навигация осуществляется асинхронным образом, возврат из метода Navigate() происходит до появления целевой страницы. В некоторых случаях разница во времени может оказаться значительной, например, как в случае перехода к несвязанной XAML-странице, расположенной на Web-сайте (или XAML-странице, находящейся в какой-то другой сборке, которая инициирует Web-загрузку), или в случае, когда страница включает выполняющийся длительное время код в обработчике событий Initialized или Loaded. Процесс навигации в WPF описан ниже. 1. Определяется местонахождение страницы. 2. Извлекается информация о странице. (Если страница находится на удаленном сайте, тогда она на этом этапе загружается.) 3. Устанавливается местонахождение всех необходимых странице и связанных с ней ресурсов (например, изображений) и выполняется их загрузка. 4. Осуществляется синтаксический анализ страницы и генерируется дерево ее объектов. На этом этапе страница запускает события Initialized (если только она не восстанавливается из журнала) и Loaded.
Book_Pro_WPF-2.indb 264
19.05.2008 18:10:09
Страницы и навигация
265
5. Страница визуализируется. 6. Если URI включает фрагмент, WPF переходит сразу же к этому элементу. В табл. 9.2 перечислены события, генерируемые классом NavigationService в течение этого процесса. Эти события навигации также предоставляются классом Application и навигационными контейнерами (такими как NavigationWindow и Frame). При наличии более одного навигационного контейнера это дает возможность обрабатывать процесс навигации в разных контейнерах по отдельности. Однако встроенного способа для обработки навигационных событий одной единственной страницы не существует. После присоединения к навигационному контейнеру службы навигации и обработчика событий он продолжает генерировать события при переходе со страницы на страницу (или до тех пор, пока обработчик событий не будет удален). В целом это означает, что навигацию легче всего обрабатывать на уровне приложения. События навигации нельзя подавлять с помощью свойства RoutedEventArgs. Handled. А все потому, что они являются не маршрутизируемыми событиями, а обычными событиями .NET. Совет. Навигационным событиям можно передавать данные из метода Navigate(). Нужно просто использовать ту из перегруженных версий метода Navigate(), которая в качестве параметра принимает дополнительный объект. Этот объект делается доступным в событиях Navigated, NavigationStopped и LoadCompleted через свойство NavigationEventArgs. ExtraData. Это свойство можно использовать, например, для отслеживания времени поступления запроса на навигацию.
Таблица 9.2. События класса NavigationService Имя
Описание
Navigating
Процесс навигации вот-вот начнется. Это событие можно отменить и тем самым предотвратить выполнение навигации.
Navigated
Процесс навигации начался, но целевая страница еще не были извлечена.
NavigationProgress
Процесс навигации уже идет полным ходом, и часть данных страницы уже загружена. Это событие вызывается периодически для предоставления информации о ходе навигации. В частности, оно предоставляет информацию о количестве данных, которые уже были загружены (NavigationProgressEventArgs.BytesRead), и общем объеме данных, которые требуется загрузить (NavigationProgressEvent Args.MaxBytes). Это событие запускается после извлечения каждого следующего килобайта данных.
LoadCompleted
Страница прошла синтаксический анализ. Однако события Initialized и Loaded еще не были сгенерированы.
FragmentNavigation
Страница подготавливается к прокручиванию до целевого элемента. Это событие срабатывает только в случае, если используется URI c информацией о фрагменте.
NavigationStopped
Процесс навигации был отменен с помощью метода StopLoading().
NavigationFailed
Процесс навигации не удался из-за того, что не получилось обнаружить или загрузить целевую страницу. Это событие можно использовать для нейтрализации исключения до того, как оно появится и превратится в необрабатываемое исключение приложения. Нужно просто установить для NavigationFailedEventArgs.Handled значение true.
Book_Pro_WPF-2.indb 265
19.05.2008 18:10:10
266
Глава 9
Управление журналом Описанные до этого приемы позволяют создать приложение с возможностями линейной навигации и сделать процесс навигации легко адаптируемым (например, применив условную логику, так чтобы пользователи по пути направлялись к разным шагам), но все равно ограничивают вас базовым подходом, подразумевающим проход от начала до конца. На рис. 9.8 показана такая топология навигации, которая является типичной при создании простых мастеров, основанных на описании задач. Пунктирными линиями обозначены интересующие нас шаги — когда пользователь покидает группу страниц, представляющих логическую задачу.
Вызывающая страница Готово
Navigate()
Отмена
Назад Начальная страница
Navigate()
Назад Страница 1
Navigate()
Страница 2
Рис. 9.8. Линейная навигация Если вы попробуете реализовать такой дизайн с использованием процесса навигации WPF, то обнаружите, что в нем не достает одной детали. В частности, нужно, чтобы по завершении процесса навигации пользователем (либо из-за отмены им операции на одном из этапов, либо из-за выполнения им задачи вручную) очищался список предыдущих страниц. Если работа приложения “крутится” вокруг одного главного окна, которое не основано на процессе навигации, это не проблема. При запуске пользователем задачи, подразумевающей использование страницы, приложение может просто создавать новый объект NavigationWindow, позволяющий пользователю выполнить ее. По завершении задачи, оно, соответственно, может просто уничтожать этот объект. Однако если все приложение основано на процессе навигации, тогда ситуация усложняется. В таком случае необходимо придумать способ для очистки списка предыдущих посещавшихся страниц в случае отмены или завершения задачи для того, чтобы пользователь не мог вернуться назад на один из промежуточных шагов. К сожалению, WPF не предлагает особых возможностей для управления стеком навигации. Все, что она предлагает — это два метода в классе NavigationService: AddBackEntry() и RemoveBackEntry(). В данном примере требуется именно метод RemoveBackEntry() . Он берет самый недавний элемент в списке предыдущих страниц и удаляет его. Вдобавок метод RemoveBackEntry() еще также возвращает объект JournalEntry, который описывает этот элемент. Он сообщает URI-адрес (через свойство Source) и имя, которое тот имеет в навигационном журнале (через свойство Name). Не забывайте, что имя устанавливается на основании значения свойства Page.Title.
Book_Pro_WPF-2.indb 266
19.05.2008 18:10:10
Страницы и навигация
267
При желании сделать так, чтобы по завершении задачи удалялось сразу несколько записей, метод RemoveBackEntry() придется вызывать несколько раз. Здесь возможны два варианта. Если требуется удалить весь список предыдущих страниц, можно применить свойство CanGoBack для определения момента, когда будет достигнут его конец: while (nav.CanGoBack) { nav.RemoveBackEntry(); }
Альтернативный вариант — продолжать удалять элементы до тех пор, пока не будет удалена начальная точка задачи. Например, если выполнение задачи начинается на странице ConfigureAppWizard.xaml, по его завершении можно использовать такой код: string pageName; while (pageName != "ConfigureAppWizard.xaml") { JournalEntry entry = nav.RemoveBackEntry(); pageName = System.IO.Path.GetFileName(entry.Source.ToString()); }
Этот код берет полный URI, который хранится в свойстве JournalEntry.Source, и усекает его до имени страницы с помощью статического метода GetFileName() класса Path (который с URI-идентификаторами работает не менее эффективно). Использование свойства Title сделало бы кодирование более удобным, но оно не столь надежно. Поскольку заголовок страницы отображается в хронологии навигации и является видимым для пользователя, он представляет собой фрагмент информации, который в случае локализации приложения потребуется переводить на другие языки. А это чревато нарушением кода, который ожидает жестко закодированного заголовка страницы. И даже если приложение не планируется локализовать, нетрудно представить другой сценарий с изменением заголовка страницы, например, для того, чтобы тот был более понятным или более описательным. Кстати, все элементы в списке предыдущих и следующих страниц можно просматривать с помощью свойств BackStack и ForwardStack навигационного контейнера (вроде NavigationWindow или Frame). Однако получать эту информацию через класс NavigationService нельзя. В любом случае эти свойства предоставляют простые и доступные только для чтения объекты JournalEntry. Вносить изменения в списки они не позволяют, и поэтому настоящая необходимость в них возникает крайне редко.
Добавление в журнал специальных элементов Вместе с методом RemoveBackEntry() класс NavigationService также предоставляет метод AddBAckEntry(). Целью этого метода является разрешать сохранять в списке предыдущих страниц “виртуальные” записи. Например, предположим, что у вас есть одна страница, которая позволяет пользователю решать довольно сложную задачу конфигурирования. Если нужно сделать так, чтобы пользователь мог возвращаться к предыдущему состоянию этого окна, вы можете сохранить его с помощью метода AddBackEntry(). Несмотря на то что это всего лишь одна единственная страница, она может иметь несколько соответствующих записей в списке. Вопреки возможным ожиданиям, при вызове метода AddBackEntry() объект JournalEntry передавать не нужно. (На самом деле класс JournalEntry имеет защищенный конструктор, поэтому ваш код не может создавать его экземпляр.) Вместо этого потребуется создать специальный класс, унаследованный от абстрактного класса System.Windows.Navigation.CustomContentState и сохраняющий всю необходимую информацию. Например, взгляните на приложение, показанное на рис. 9.9, которое позволяет перемещать элементы из одного списка в другой.
Book_Pro_WPF-2.indb 267
19.05.2008 18:10:10
268
Глава 9
Рис. 9.9. Динамический список Теперь предположим, что необходимо, чтобы состояние этого окна сохранялось при каждом перемещении элемента из одного списка в другой. Первое, что потребуется — это класс, унаследованный от CustomContentState и отслеживающий эту необходимую вам информацию. В данном случае вам нужно просто записать содержимое обоих списков. Поскольку этот класс будет сохраняться в журнале (для того, чтобы ваша страница могла при необходимости “восстанавливаться”), он должен допускать сериализацию. [Serializable()] public class ListSelectionJournalEntry : CustomContentState { private List sourceItems; private List targetItems; public List SourceItems { get { return sourceItems; } } public List TargetItems { get { return targetItems; } } ...
Это дает хорошее начало, но все равно еще нужно много чего сделать. Например, вы наверняка не захотите, чтобы страница появлялась в хронологии навигации с одним и тем же заголовком множество раз. Вместо этого, вероятно, потребуется использовать какое-то более описательное имя. Для этого придется переопределить свойство JournalEntryName. В данном примере никакого очевидного и логичного способа для описания состояния обоих списков нет. Поэтому имеет смысл позволить странице самой выбирать имя при сохранении записи в журнале. В таком случае страница сможет добавлять описательное имя на основании самого последнего действия (вроде Added Blue или Removed Yellow). Чтобы создать такой дизайн, вам нужно всего лишь сделать свойство JournalEntryName зависимым от переменной, установить которую можно и прямо в конструкторе: ... private string _journalName; public override string JournalEntryName {
09_Pro-WPF2.indd 268
20.05.2008 16:23:51
Страницы и навигация
269
get { return _journalName; } } ...
Система навигации WPF будет обращаться к вашему свойству JournalEntryName для получения имени, которая она должна показывать в списке. Следующий шаг состоит в переопределении метода Replay(). WPF вызывает этот метод, когда пользователь переходит к записи в списке предыдущих или следующих страниц, позволяя применять предыдущее сохраненное состояние. Существуют два подхода, которые можно использовать в методе Replay(). Первый — извлечь ссылку на текущую страницу с помощью свойства NavigationService.Content, а затем привести эту страницу к типу соответствующего класса страницы и вызвать любой требуемый для реализации задуманного изменения метод. А второй подход (который иллюстрируются здесь) — положиться на обратный вызов: ... private ReplayListChange replayListChange; public override void Replay(NavigationService navigationService, NavigationMode mode) { this.replayListChange(this); } ...
Делегат ReplayListChange здесь не показан, но выглядит он довольно-таки просто. Он представляет метод с одним параметром, а именно — объектом ListSelection JournalEntry . Далее страница может извлекать информацию списка из свойств SourceItems и TargetItems и восстанавливать состояние. Сделав все это, остается только создать конструктор, принимающий всю необходимую информацию, а точнее — два списка элементов, заголовок, который должен использоваться в журнале, и делегата, который должен запускаться при возникновении необходимости в повторном применении состояния к странице. ... public ListSelectionJournalEntry( List sourceItems, List targetItems, string journalName, ReplayListChange replayListChange) { this.sourceItems = sourceItems; this.targetItems = targetItems; this.journalName = journalName; } }
Чтобы добавить эту функциональную возможность в страницу, потребуется выполнить три описанных ниже шага. 1. Вызвать в подходящее время метод AddBackReference() для сохранения в хронологии навигации дополнительной записи. 2. Обработать обратный вызов ListSelectionJournalEntry для восстановления окна при проходе пользователя по хронологии. 3. Реализовать в своем классе страницы интерфейс IProvideCustomContentState и его единственный метод GetContentState(). При переходе пользователя на другую страницу в хронологии метод GetContentState() вызывается службой навигации. Это позволяет вернуть экземпляр своего специального класса, который будет храниться как состояние текущей страницы.
09_Pro-WPF2.indd 269
20.05.2008 16:25:19
270
Глава 9
На заметку! Интерфейс IProvideCustomContentState является часто пропускаемой, но очень существенной деталью. Когда пользователь выполняет навигацию с помощью списка следующих или предыдущих страниц, должны происходить две вещи: страница должна добавлять текущее представление в журнал (с помощью IProvideCustomContentState), а затем восстанавливать выбранное представление (с помощью обратного вызова ListSelectionJournalEntry). Для начала при каждом выполнении щелчка на кнопке Add (Добавить) нужно создать новый объект ListSelectionJournalEntry и вызвать метод AddBackReference() для того, чтобы предыдущее состояние сохранялось в хронологии. Этот процесс выносится в отдельный метод для того, чтобы его можно было использовать в нескольких местах на странице (например, при выполнении щелчка либо на кнопке Add (Добавить), либо на кнопке Remove (Удалить)): private void cmdAdd_Click(object sender, RoutedEventArgs e) { if (lstSource.SelectedIndex != -1) { // Определяем наиболее подходящее имя для использования // в хронологии навигации. NavigationService nav = NavigationService.GetNavigationService(this); string itemText = lstSource.SelectedItem.ToString(); string journalName = "Added " + itemText; // Обновляем журнал (с помощью приведенного ниже метода). nav.AddBackEntry(GetJournalEntry(journalName)); // Теперь вносим изменение. lstTarget.Items.Add(itemText); lstSource.Items.Remove(itemText); } } private ListSelectionJournalEntry GetJournalEntry(string journalName) { // Извлекаем информацию о состоянии обоих списков // (с помощью вспомогательного метода). List source = GetListState(lstSource); List target = GetListState(lstTarget); // Создаем с помощью этой информации специальный объект состояния. // Указываем обратному вызову на метод Replay в этом классе. return new ListSelectionJournalEntry( source, target, journalName, Replay); }
Подобный процесс можно использовать и при выполнении щелчка на кнопке Remove (Удалить). Следующее, что потребуется сделать — обработать обратный вызов в методе Replay() и обновить списки, как показано ниже: private void Replay(ListSelectionJournalEntry state) { lstSource.Items.Clear(); foreach (string item in state.SourceItems) { lstSource.Items.Add(item); } lstTarget.Items.Clear(); foreach (string item in state.TargetItems) { lstTarget.Items.Add(item); } }
И, наконец, последний шаг — реализовать в странице интерфейс IProvideCustom
ContentState:
Book_Pro_WPF-2.indb 270
19.05.2008 18:10:10
Страницы и навигация
271
public partial class PageWithMultipleJournalEntries : Page, IProvideCustomContentState
Интерфейс IProvideCustomContentState определяет один единственный метод под названием GetContentState(). В этом методе необходимо сохранить состояние для страницы точно таким же образом, как и при щелчке на кнопке Add (Добавить) или Remove (Удалить). Единственным отличием является то, что его не нужно добавлять с помощью метода AddBackReference(). Вместо этого его следует предоставить WPF через возвращаемое значение. public CustomContentState GetContentState() { // Мы не сохраняли самое последнее действие, поэтому // используем для заголовка просто имя страницы. return GetJournalEntry("PageWithMultipleJournalEntries"); }
Не забывайте о том, что служба навигации WPF вызывает метод GetContentState(), когда пользователь переходит на другую страницу с помощью кнопки возврата назад или перехода вперед. WPF берет возвращаемый объект CustomContentState и сохраняет его в журнале для текущей страницы. Здесь возможна одна особенность: в случае выполнения пользователем нескольких действий и их отмены путем возвращения обратно по хронологии навигации, “отмененные” действия будут иметь в хронологи жестко закодированное имя страницы (PageWithMultipleJournalEntries), а не более описательное исходное имя (вроде Added Orange). Чтобы улучшить способ обработки этой детали, можно сохранить журнальное имя для страницы в классе страницы с помощью переменной экземпляра. В загружаемом коде для этого примера такой дополнительный шаг выполняется. Это усложняет пример. В таком случае при запуске приложения и осуществлении манипуляций со списками, в хронологии будут отображаться несколько записей (рис. 9.10).
Рис. 9.10. Специальные записи в журнале
Страничные функции Пока что было продемонстрировано, как можно передавать информацию странице (за счет программного создания экземпляра страницы, его конфигурирования и последующей передачи методу NavigationService.Navigate()), но как возвращать
Book_Pro_WPF-2.indb 271
19.05.2008 18:10:10
272
Глава 9
информацию из страницы еще не показывалось. Самый простой (и наименее структурированный) подход — сохранить информацию в какой-то статической переменной приложения, так чтобы она было доступна любому другому классу в программе. Однако такой дизайн является далеко не самым лучшим, если требуется всего лишь способ для передачи простых битов информации с одной страницы на другую, и не нужно, чтобы эта информация находилась в памяти в течение долгого времени. Добавление глобальных переменных усложнит вычисление зависимостей (т.е. того, какими страницами используется каждая из этих переменных), а также значительно затруднит многократное использование страниц и обслуживание приложения. Другой подход, который предлагает WPF, заключается в применении класса PageFunction. Класс PageFunction представляет собой производную версию класса Page с возможностью возвращения результата. В некоторой степени класс PageFunction напоминает диалоговое окно, в то время как класс Page больше похож на обычное окно. Чтобы создать класс PageFunction в Visual Studio, нужно сначала щелкнуть правой кнопкой мыши на проекте в окне Solution Explorer и выбрать в контекстном меню команду AddNew Item (ДобавитьНовый элемент), а затем перейти в категорию WPF, выбрать шаблон Page Function (WPF) (Страничная функция (WPF)), ввести имя требуемого файла и щелкнуть на кнопке Add (Добавить). Код разметки класса PageFunction практически идентичен тому, что используется для класса Page. Единственным отличием является лишь корневой элемент, который выглядит как , а не как . С технической точки зрения PageFunction является полиморфным классом. Он принимает один единственный параметр, который указывает, данные какого типа должны использоваться для возвращаемого им значения. По умолчанию для каждого нового класса PageFunction этим параметром является строка (что означает, что в качестве возвращаемого значения он должен возвращать одну строку). Однако эту деталь можно легко изменять путем изменения значения атрибута TypeArguments в элементе . Ниже приведен пример, в котором класс PageFunction возвращает экземпляр специального класса под названием SelectedProduct. Для поддержки такой структуры имя соответствующего пространства имен (NavigationApplication) в элементе отображается на подходящий префикс XML (local), который затем и применяется при установке значения для атрибута TypeArguments.
Это объявление указывает, что данный класс PageFunction будет возвращать вызывающей странице объект Product. Кстати, в случае установки в коде разметки значения для атрибута TypeArguments указывать эту же информацию в объявлении класса не нужно. Вместо этого анализатор XAML сгенерирует правильный класс автоматически. Это означает, что для объявления приведенного выше класса PageFunction будет достаточно и следующего кода: public partial class SelectProductPageFunction { ... }
Book_Pro_WPF-2.indb 272
19.05.2008 18:10:10
Страницы и навигация
273
Хотя и следующий более явный код тоже будет работать также хорошо: public partial class SelectProductPageFunction: PageFunction { ... }
Visual Studio использует именно такой более явный синтаксис при создании
PageFunction. По умолчанию все новые классы PageFunction, которые создает Visual Studio, порождаются от PageFunction. Класс PageFunction требует, чтобы все выполняемые в нем процессы навигации обрабатывались программно. Поэтому при выполнении щелчка на кнопке или ссылке, приводящей к завершению задачи, код должен вызывать метод PageFunction. OnReturn(), после чего нужно, чтобы предоставлялся либо подлежащий возврату объект (каковым должен являться экземпляр указанного в объявлении класса), либо значение null, указывающее, что задача не была выполнена. Ниже приведен пример с двумя соответствующими обработчиками событий. private void lnkOK_Click(object sender, RoutedEventArgs e) { // Возвращаем информацию о выборе. OnReturn(new ReturnEventArgs(lstProducts.SelectedValue)); } private void lnkCancel_Click(object sender, RoutedEventArgs e) { // Указываем, что ничего не было выбрано. OnReturn(null); }
В применении класса PageFunction тоже нет ничего сложного. Вызывающая страница должна создавать экземпляр PageFunction программно из-за необходимости в присоединении обработчика событий к событию PageFunction.Returned. (Этот дополнительный шаг требуется потому, что метод NavigationService.Navigate() является асинхронным и возвращается немедленно.) SelectProductPageFunction pageFunction = new SelectProductPageFunction(); pageFunction.Return += new ReturnEventHandler( SelectProductPageFunction_Returned); this.NavigationService.Navigate(pageFunction);
Когда пользователь завершает работу с PageFunсtion и щелкает на ссылке, вызывающей метод OnReturn(), генерируется событие PageFunction.Returned. Возвращаемый объект становится доступным через свойство ReturnEventArgs.Result. private void SelectProductPageFunction_Returned(object sender, ReturnEventArgs e) { Product product = (Product)e.Result; if (e != null) lblStatus.Text = "You chose: " + product.Name; }
Обычно метод OnReturn() обозначает конец задачи, после которого не нужно, чтобы у пользователя была возможность возврата обратно к PageFunction. Исключить для него такую возможность можно, конечно, и с помощью метода NavigationService. RemoveBackEntry() , однако существует более простой подход. Каждый класс PageFunction также еще предлагает свойство по имени RemoveFromJournal. Если для этого свойства устанавливается значение true, страница автоматически удаляется из хронологии при вызове метода OnReturn(). Добавление в приложение данного класса PageFunction даст возможность использовать другую топологию навигации, например, назначить одну страницу централь-
Book_Pro_WPF-2.indb 273
19.05.2008 18:10:11
274
Глава 9
ным ядром и позволить пользователям выполнять различные задачи через страничные функции, как показано на рис. 9.11. Часто один элемент PageFunction будет вызывать другой элемент PageFunction. В таком случае для обработки процесса навигации по его завершении рекомендуется использовать связанную последовательность вызовов OnReturn(). Другими словами, в случае если элемент PageFunction1 вызывает элемент PageFunction2, который затем обращается к элементу PageFunction3, при вызове элементом PageFunction3 метода OnReturn() должен срабатывать обработчик событий Returned в элементе PageFunction2, который затем должен вызывать метод OnReturn(), приводящий к срабатыванию события Returned в элементе PageFunction1, который должен вызывать метод OnReturn() последним и завершать весь процесс. В зависимости от дизайна, может оказаться необходимым, чтобы возвращаемый объект передавался по всей последовательности до тех пор, пока не достигнет корневой страницы.
Вызывающая страница
Navigate()
OnReturn()
Центральное ядро навигации
OnReturn()
OnReturn() Navigate()
Страница
Navigate()
Страница
OnReturn() Navigate()
Страница
Рис. 9.11. Линейная навигация
Book_Pro_WPF-2.indb 274
19.05.2008 18:10:11
Страницы и навигация
275
Приложения XBAP Приложения XBAP (XAML Browser Application — браузерное приложение XAML) — это приложения, которые выполняются внутри браузера. Приложения XBAP являются полноценными приложениями WPF, но имеют несколько основных отличий, описанных ниже.
• XBAP-приложения выполняются внутри окна браузера. Они могут занимать всю область изображения Web-страницы или размещаться где-то внутри обычного HTML-документа с помощью дескриптора (как будет показано чуть позже). На заметку! С технической точки зрения WPF-приложение любого типа, включая приложение XBAP, запускается в виде отдельного процесса, управляемого общеязыковой исполняющей средой (CLR). Нам кажется, что приложение XBAP выполняется “внутри” браузера просто потому, что оно отображает все свое содержимое в окне браузера. Это отличает его от модели, применяемой в элементах управления ActiveX (и приложениях Silverlight), которые на самом деле загружаются внутри процесса браузера.
• XBAP-приложения имеют ограниченные права. Хотя приложение XBAP и можно сконфигурировать так, чтобы оно запрашивало разрешение на полный доступ, задача состоит в том, чтобы использовать приложение XBAP как облегченную модель развертывания, позволяющую пользователям запускать приложения WPF без подвергания риску выполнения какого-нибудь потенциально небезопасного кода. Приложению XBAP предоставляются те же разрешения, что и приложению .NET, которое запускается из Web или локальной интрасети, да и механизм, налагающий эти ограничения (т.е. отвечающий за безопасность доступа к коду) — тот же. Это означает, что по умолчанию приложение XBAP не может записывать файлы, взаимодействовать с ресурсами компьютера (например, реестром), подключаться к базам данных или отображать полнофункциональные окна.
• XBAP-приложения не инсталлируются. При запуске приложения XBAP это приложение загружается и помещается в кэш браузера. Однако инсталлированным на компьютере оно не остается. Это дает Web-модель мгновенного обновления — другими словами, при каждом возврате пользователя к данному приложению загружается новая версия этого приложения (разумеется, если оно отсутствует в кэше). Преимущество приложений XBAP состоит в том, что они позволяют работать безо всяких подсказок и приглашений. При наличии установленной версии .NET 3.5, клиент может открывать приложение XBAP в браузере и начинать работать с ним точно так же, как с Java-аплетом, Flash-фильмом или оснащенной JavaScript-сценариями Web-страницей. Никакого приглашения выполнить инсталляцию и предупреждения по поводу безопасности не появляется. Очевидно, что расплачиваться за это приходится следованием очень жестко ограниченной модели безопасности. Если приложению требуется больше возможностей (например, ему необходимо выполнять чтение или запись произвольных файлов, взаимодействовать с базой данных, работать с системным реестром Windows и т.д.), тогда лучше будет сделать его автономным приложением Windows и обеспечить упрощенным (хотя и не полностью бесшовным) процессом развертывания с помощью механизма ClickOnce, о котором более подробно будет рассказываться в главе 27.
Book_Pro_WPF-2.indb 275
19.05.2008 18:10:11
276
Глава 9
Требования XBAP Для запуска любого приложения WPF, в том числе XBAP, на клиентском компьютере должен быть установлен компонент .NET Framework 3.0 или 3.5. Windows Vista включает .NET 3.0, поэтому компьютеры, работающие под управлением Windows Vista, автоматически распознают приложения XBAP. (То, какая именно версия .NET потребуется для запуска приложения XBAP — .NET 3.0 или .NET 3.5 — зависит от используемых функциональных возможностей WPF и выбранной в качестве целевой версии .NET, о чем более подробно рассказывалось в главе 1.) В настоящее время существует только два браузера, которые способны запускать приложения XBAP: это Internet Explorer (версии 6 или выше) и Firefox (версии 2 или выше). У Internet Explorer 7 имеется одна дополнительная возможность — он способен распознавать файлы .xbap, даже если компонент .NET 3.0/3.5 не установлен. Когда пользователь запрашивает файл .xbap, Internet Explorer просто отображает ему окно с приглашением установить .NET 3.5 (как показано на рис. 9.12).
Рис. 9.12. Попытка запустить приложение XBAP в Internet Explorer 7.0 без .NET 3.5
Создание приложения XBAP Любое страничное приложение может становиться приложением XBAP, хотя Visual Studio для создания такого приложения вынуждает создавать новый проект с помощью шаблона WPF Browser Application (Браузерное WPF-приложение). Разница состоит в четырех ключевых элементах в файле проекта .csproj: True False .xbap Internet
Эти дескрипторы указывают WPF обслуживать приложение в браузере (HostInBrowser), не устанавливая его насовсем (Install), а помещать его в кэш вместе с другими файлами Internet, использовать расширение .xbap (ApplicationExtension) и запрашивать разрешения только зоны Internet (TargetZone). Четвертый дескриптор является необязательным: как будет показано далее, с технической точки зрения создание приложения XBAP с большим количеством разрешений считается возможным. Однако приложения XBAP практически всегда выполняются с ограниченным набором разрешений, доступных в зоне Internet, что является самым сложным аспектом для успешного программирования такого приложения. Совет. Файл .csproj также включает и другие связанные с приложением XBAP дескрипторы, которые гарантируют правильный процесс отладки. Самый простой способ изменить приложение XBAP на страничное приложение с автономным окном (или наоборот) — это создать новый проект желаемого типа и затем импортировать все страницы из старого проекта.
Book_Pro_WPF-2.indb 276
19.05.2008 18:10:11
Страницы и навигация
277
Создав приложение XBAP, можно приступать к разработке страниц и кодировать их точно так же, как и при использовании NavigationWindow. Например, сначала можно указать одну из страниц в качестве значения для свойства StartupUri в файле App.xaml, затем — скомпилировать приложение, что приведет к генерации файла .xbap, а потом — запросить этот файл .xbap в Internet Explorer или Firefox. Далее, при условии, что в системе установлен компонент .NET 3.0, приложение запустится автоматически в режиме ограниченного уровня доверия, как показано на рис. 9.13. На заметку! Проекты XBAP имеют жестко закодированный путь отладки. Это означает, что перемещение проекта XBAP из одной папки в другую приведет к потере возможности выполнения его отладки в Visual Studio. Устранить эту проблему можно, дважды щелкнув на элементе Properties (Свойства) в окне Solution Explorer, выбрав раздел Debug (Отладка) и обновив соответствующим образом путь в текстовом поле Command Line Arguments (Аргументы командной строки).
Рис. 9.13. Приложение XBAP в браузере Приложение XBAP выполняется точно так же, как и обычное приложение WPF, если только не пытаться выполнить какие-то запрещенные действия (вроде отображения автономного окна). В случае запуска приложения в Internet Explorer 7 (версии, которая поставляется с Windows Vista), место кнопок, отображаемых в NavigationWindow, занимают кнопки браузера, и они же и позволяют просматривать списки предыдущих и следующих страниц. В более ранних версиях Internet Explorer и в Firefox в верхней части страницы отображается совершенно другой набор навигационных кнопок (не столь удобный).
Развертывание приложения XBAP Хотя для приложения XBAP и можно создавать программу установки (и запускать его с локального жесткого диска), в выполнении такого шага необходимость возникает редко. Вместо этого лучше просто скопировать скомпилированное приложение в какойто сетевой ресурс или виртуальный каталог. На заметку! Добиться подобного эффекта можно и применяя несвязанные файлы XAML. Если приложение полностью состоит из страниц XAML и не имеет никаких файлов отделенного кода, его вообще не нужно компилировать. Вместо этого лучше поступить следующим образом: просто разместить соответствующие файлы .xaml на Web-сервере и позволить пользователям
Book_Pro_WPF-2.indb 277
19.05.2008 18:10:11
278
Глава 9
обозревать их напрямую. Конечно, очевидно, что несвязанные XAML-файлы не могут делать столько же, сколько их скомпилированные аналоги, но они вполне подходят, если требуется всего лишь отобразить документ, графику или анимацию, либо же если все необходимые функциональные возможности добавляются с помощью декларативных выражений привязки. К сожалению, развертывание приложения XBAP не является столь же простым процессом, как просто копирование файла .xbap. В таком случае в одну и ту же папку фактически требуется скопировать три перечисленных ниже файла.
• ИмяПриложения.exe. В этом файле содержится скомпилированный IL-код, который имеется в любом приложении .NET.
• ИмяПриложения.exe.manifest. Этот файл представляет собой XML-документ с перечнем требований данного приложения (наподобие версии .NET-сборок, использованных для компиляции кода). Если приложение работает с какими-то другими DLL-файлами, их можно сделать доступными в том же самом виртуальном каталоге, что и приложение, и тогда они будут загружаться автоматически.
• ИмяПриложения.xbap. Файл .xbap — это еще один XML-документ. Он представляет точку входа в приложение — другими словами, это файл, который пользователь должен запросить в браузере, чтобы установить данное приложение XBAP. Код разметки в файле .xbap указывает на файл приложения и включает цифровую подпись, в которой используется выбранный разработчиком для проекта ключ. После размещения этих файлов в подходящем месте можно попробовать запустить приложение, запросив его файл .xbap в Internet Explorer или Firefox. То, находятся файлы на локальном жестком диске либо на удаленном Web-сервере, не имеет никакого значения — их можно запрашивать абсолютно одинаково. Совет. Несмотря на соблазн, запускать файл .exe не следует. Если это сделать, ничего не произойдет. Вместо этого нужно просто дважды щелкнуть на файле .xbap в окне проводника Windows (или ввести путь к этому файлу вручную прямо в Internet Explorer). В любом случае присутствовать должны обязательно все три файла, и браузер должен быть способным распознавать файлы с расширением .xbap. Начав загрузку файла .xbap, браузер отобразит страницу с информацией о ходе процесса загрузки (рис. 9.14). Этот процесс загрузки, по сути, представляет собой тот же процесс инсталляции и подразумевает копирование приложения .xbap в локальный кэш Internet-файлов. При возвращении пользователя к этому же самому удаленному месту во время последующих посещений использоваться уже будет версия, находящаяся в кэше (за исключением случая, если на сервере окажется более новая версия данного приложения XBAP, о чем более подробно речь пойдет в следующем разделе). При создании нового приложения XBAP среда Visual Studio также включает в него генерируемый автоматически файл сертификата с именем вроде ИмяПриложения_ TemporaryKey.pfx. В этом сертификате содержится открытый и секретный ключи, которые используются для добавления в файл .xbap подписи. В случае публикации обновления для приложения его потребуется подписать с помощью точно такого же ключа, дабы не нарушить целостность цифровой подписи. Вместо временного ключа может возникнуть желание создать свой собственный ключ (который бы затем можно было делить между проектами и защищать паролем). Чтобы сделать это, необходимо просто дважды щелкнуть на узле Properties (Свойства) под своим проектом в окне Solution Explorer и затем использовать опции на вкладке Signing (Подпись).
Book_Pro_WPF-2.indb 278
19.05.2008 18:10:11
Страницы и навигация
279
Рис. 9.14. Первый запуск приложения .xbap
Обновление приложения XBAP При отладке приложения XBAP среда Visual Studio всегда компонует приложение заново и загружает его самую последнюю версию в браузере. Никаких дополнительных шагов предпринимать не требуется. Однако этого не происходит, если приложение XBAP запрашивается прямо в браузере. При запуске приложений XBAP подобным образом существует потенциальная проблема. В случае повторной компоновки приложения, его развертывания в том же самом месте и повторного запроса в браузере, вовсе не обязательно загружается обновленная версия. Вместо этого продолжает использоваться более старая копия приложения из кэша. То же самое происходит, даже если закрыть и заново открыть окно браузера, щелкнуть в окне браузера на кнопке Refresh (Обновить) и увеличить версию сборки приложения XBAP. Можно вручную очистить кэш ClickOnce, но очевидно, что такое решение не является удобным. Вместо этого лучше обновить информацию о публикации, которая хранится в файле .xbap, так, чтобы браузер смог понять, что развернутая заново версия приложения XBAP является его новой версией. Обновления версии сборки не достаточно для инициации процесса обновления — вместо этого необходимо обновить опубликованную версию. На заметку! Этот дополнительный шаг является обязательным, потому что функция загрузки и помещения файла .xbap в кэш создается с использованием деталей из ClickOnce — технологии развертывания, о которой более подробно будет рассказываться в главе 27. ClickOnce использует версию публикации для определения того, когда должно быть применено обновление. Это позволяет компоновать приложение множество раз для тестирования (каждый раз с различным номером версии сборки), а версию публикации увеличивать только при желании развернуть новый выпуск. Самый простой способ скомпоновать приложение заново и применить новую версию публикации — это выбрать в меню Visual Studio команду BuildPublish [ProjectName] (Ск омпоноватьОпубликовать [Имя проекта]) (и затем щелкнуть на кнопке Finish (Готово)). Использовать файлы публикации (которые находятся в папке Publish (Публикация) под
Book_Pro_WPF-2.indb 279
19.05.2008 18:10:11
280
Глава 9
каталогом проекта) не нужно, потому что новый сгенерированный файл .xbap в папке Debug (Отладка) и Release (Выпуск) будет указывать на новую версию публикации. Все, что требуется — развернуть этот файл .xbap (вместе с файлом .exe и .manifest) в подходящем месте. При следующем запросе файла .xbap браузер загрузит новые файлы приложения и поместит в кэш их. Просмотреть текущую версию публикации можно, дважды щелкнув на элементе Properties (Свойства) в окне Solution Explorer, отобразив вкладку Publish (Публикация) и посмотрев на параметры в разделе Publish Version (Версия публикации) в нижней части этой вкладки. Флажок Automatically Increment Revision with Each Publish (Автоматически увеличивать номер выпуска при каждой публикации) должен быть обязательно отмечен, дабы версия публикации автоматически увеличивалась при публикации приложения, что очевидным образом помечает его как новый выпуск.
Безопасность приложения XBAP Самое сложное при создании приложения XBAP — это оставаться в рамках ограниченной модели безопасности. Обычно приложение XBAP запускается с разрешениями зоны Internet. Так происходит даже в том случае, если приложение XBAP запускается с локального жесткого диска. Каркас .NET Framework использует безопасность доступа к коду (одну из своих основных функций, которая предлагается, начиная с версии 1.0) для ограничения того, что разрешено делать приложению XBAP. В целом ограничения соответствуют ограничениям аналогичного кода Java или JavaScript в странице HTML. Например, визуализировать графику, проигрывать анимацию, использовать элементы управления, показывать документы и воспроизводить звуки разрешено, а получать доступ к ресурсам компьютера, таким как файлы, системный реестр Windows, базы данных и т.д. — нет. На заметку! Те, кому доводилось разрабатывать приложения Windows Forms с помощью .NET 2.0, могут вспомнить, что ClickOnce позволяет приложениям повышать свой уровень доверия через специальное окно с предупреждением о безопасности. Если приложению требуется больше разрешений, чем предоставляется в зоне Internet, перед глазами пользователей появляется специальное окно с устрашающим предупреждением о безопасности и опцией, позволяющей разрешить запуск приложения. Приложения XBAP работают не так. Здесь у пользователя нет возможности повышать уровень разрешений, поэтому запустить приложение, требующее разрешений, которые выходят за рамки зоны безопасности Internet, не получится. Один из простых способов узнать, разрешено ли выполнение того или иного действия — написать какой-то тестовый код и испробовать его. Также все необходимые детали можно найти в документации по WPF. В табл. 9.3 приведен краткий перечень наиболее важных поддерживаемых и запрещенных функций. А что происходит при попытке использовать функцию, применение которой не допустимо в зоне Internet? Обычно приложение дает сбой при запуске проблемного кода и генерирует исключение SecurityException. В качестве альтернативного варианта можно сконфигурировать приложение так, чтобы оно запрашивало разрешение, в котором пользователь получает ошибку при первом открытии файла .xbap и попытке запустить приложение. (Чтобы запросить разрешение, нужно в Visual Studio дважды щелкнуть на узле Properties (Свойства), перейти на вкладку Security (Безопасность) и изменить значение требуемого разрешения с Zone Default (Зона по умолчанию) на Include (Включить).)
Book_Pro_WPF-2.indb 280
19.05.2008 18:10:12
Страницы и навигация
281
Таблица 9.3. Ключевые функциональные возможности WPF и зона Internet Разрешается использовать
Не разрешается использовать
Все основные элементы управления, включая элемент управления RichTextBox.
Элементы управления Window Forms (посредством функциональной совместимости).
Окна типа Page, MessageBox и OpenFileDialog.
Автономные окна и другие диалоговые окна (такие как диалоговое окно SaveFileDialog).
Изолированное хранилище (с ограничением в 512 Кбайт)
Доступ к файловой системе и доступ к системному реестру.
Двух- и трехмерные рисунки, аудио и видео, потоковые и XPS-документы, а также анимацию.
Некоторые растровые эффекты (вероятно, из-за того, что они подразумевают применение неуправляемого кода).
“Эмулируемое” перетаскивание (код, реагирующий на события перемещения мыши).
Перетаскивание Windows.
Web-службы ASP.NET (.asmx) и службы WCF (Windows Communications Foundation).
Наиболее развитые функциональные возможности WCF (вроде возможности транспортировки данных не по протоколу HTTP, возможности использования инициируемых на сервере соединений и возможности применения протоколов WS-*) и обмен данными с любым не тем сервером, на котором находится приложение XBAP.
На рис. 9.15 показан результат запуска обычного приложения XBAP, пытающегося выполнить запрещенное действие и не имеющего кода для обработки результирующего исключения SecurityException.
Рис. 9.15. Необрабатываемое исключение в приложении XBAP
Приложения XBAP с полным доверием Существует возможность создания приложения XBAP, запускаемого с полным доверием, хотя поступать так не рекомендуется. Для этого нужно просто дважды щелкнуть на узле Properties (Свойства) в окне Solution Explorer, перейти на вкладку Security (Безопасность) и выбрать опцию This Is a Full Trust Application (Это приложение с полным доверием). Однако после этого пользователи больше не смогут запускать это приложе-
Book_Pro_WPF-2.indb 281
19.05.2008 18:10:12
282
Глава 9
ние с Web-сервера или виртуального каталога. Вместо этого для уверенности в том, что приложение сможет выполняться с полным доверием потребуется выполнить один из описанных ниже шагов.
• Сделать так, чтобы приложение запускалось с локального жесткого диска. (Файл .xbap можно запускать как исполняемый с помощью двойного щелчка на нем или на его ярлыке.) Может также возникнуть желание использовать программу установки для автоматизации процесса установки.
• Добавить используемый для подписания сборки сертификат (который по умолчанию находится в файле .pfx) в хранилище Trusted Publishers (Надежные серверы публикации) на целевом компьютере. Сделать это можно с помощью утилиты certmgr.exe.
• Предоставить полное доверие Web-сайту или сетевому компьютеру, на котором будет развертываться файл .xbap. Для этого потребуется инструмент конфигурирования Microsoft .NET 2.0 Framework Configuration Tool (найти который можно, открыв меню Start (Пуск), выбрав в нем пункт Control Panel (Панель управления) и отобразив раздел Administrative Tools (Администрирование)). Первый вариант является самым простым, однако все варианты подразумевают выполнение на компьютере каждого устанавливающего данное приложение пользователя неудобного шага по настройке или развертыванию и поэтому идеальными их никак не назовешь. На заметку! Если приложению требуется полный набор разрешений, лучше рассмотреть вариант создания автономного приложения WPF и его развертывания с помощью технологии ClickOnce (о которой более подробно будет рассказываться в главе 27). Истинная задача модели XBAP заключается в предоставлении возможности создания WPF-аналога традиционному Web-сайту на основе HTML и JavaScript (или аплету на базе Flash).
Комбинирование приложений XBAP и автономных приложений Пока что было показано, как иметь дело с приложениями XBAP, способными запускаться с разным уровнем доверия. Однако существует еще одна возможность. Можно взять одно и то же приложение и развернуть его и как приложение XBAP, и как автономное приложение, использующее контейнер NavigationWindow (о котором речь шла в начале главы). В такой ситуации тестировать разрешения вовсе не обязательно. Может оказаться достаточным написать условную логику, проверяющую свойство BrowserInteropHelper.IsBrowserHosted и подразумевающую, что обслуживаемое в браузере приложение автоматически запускается с набором разрешений зоны Internet. Свойство IsBrowserHosted возвращает true, если приложение выполняется внутри браузера. К сожалению, переключение с автономного приложения на приложение XBAP и наоборот является непростой задачей, потому что Visual Studio не предоставляет прямой поддержки для этого. Однако другие разработчики уже создали средства, упрощающие этот процесс. Одним из таких средств является гибкий шаблон проекта Visual Studio, доступный по адресу http://scorbs.com/2006/06/04/vs-template-flexibleapplication. Он позволяет создавать один единственный файл проекта и выбирать между приложением XBAP и автономным приложением с помощью списка конфигурации сборки. Вдобавок он предоставляет константу компилятора, которую можно использовать для условной компиляции кода в любом из сценариев, и свойство приложения, которое можно применять для создания выражений привязки, условно показывающих или скрывающих определенные элементы на основании конфигурации сборки.
Book_Pro_WPF-2.indb 282
19.05.2008 18:10:12
Страницы и навигация
283
Другой вариант — поместить страницы в допускающую многократное использование сборку библиотеки классов. Тогда можно подготовить два высокоуровневых проекта: один, создающий контейнер NavigationWindow и загружающий внутри него первую страницу, и другой, запускающий страницу прямо как приложение XBAP. Это упростит обслуживание решения, но, скорее всего, также потребует написания и кое-какого условного кода, проверяющего свойство IsBrowserHosted и специфические объекты CodeAccessPermission.
Кодирование с обеспечением различных уровней безопасности В некоторых ситуациях может потребоваться создать приложение, способное функционировать в разных контекстах безопасности, например, приложение XBAP, которое могло бы запускаться как локально (с полным доверием), так и с Web-сайта. В таком случае главное — написать гибкий код, способный избегать неожиданных исключений SecurityException. Каждое отдельное разрешение в модели безопасности доступа к коду представлено классом, унаследованным от класса CodeAccessPermission. Этот класс как раз и можно применять для проверки того, выполняется ли код с должным разрешением. Секрет заключается в вызове метода CodeAccessPermission.Demand(), который запрашивает разрешение. Этот запрос не удается (приводя к генерации исключения SecurityException), если у приложения нет необходимого разрешения. Ниже показана простая функция, позволяющая выполнять проверку на предмет наличия должного разрешения: private bool CheckPermission(CodeAccessPermission requestedPermission) { try { // Пытаемся получить это разрешение. requestedPermission.Demand(); return true; } catch { return false; } }
Эту функцию можно использовать для написания кода, подобного показанному ниже, где выполняется проверка на предмет того, разрешено ли вызывающему коду осуществлять запись в файл перед попыткой совершить такую операцию: // Создаем разрешение на запись в файл. FileIOPermission permission = new FileIOPermission( FileIOPermissionAccess.Write, @"c:\highscores.txt"); // Выполняем проверку на предмет наличия этого разрешения. if (CheckPermission(permission)) { // (Выполнение записи в файл разрешено.) } else { // (Выполнение записи в файл не разрешено. Ничего не делаем // или отображаем соответствующее сообщение.) }
Book_Pro_WPF-2.indb 283
19.05.2008 18:10:12
284
Глава 9
Очевидным недостатком этого кода является то, что для управления обычным потоком программы он полагается на обработку исключений, чего делать не рекомендуется (потому что это чревато как “засорением” кода, так и дополнительными накладными расходами). Альтернативный вариант — просто попробовать выполнить операцию (например, запись в файл) и затем перехватить любое возникшее в результате этого исключение SecurityException. Однако при таком подходе увеличивается риск возникновения проблемы на полпути до завершения задачи, когда выполнить восстановление или очистку становится труднее.
Изолированное хранилище Во многих случаях имеется возможность переключиться на менее мощную функциональную возможность, если необходимое разрешение отсутствует. Например, хотя записывать данные в произвольных местах на жестком диске коду, выполняющемуся с разрешениями зоны Internet, нельзя, работать с изолированным хранилищем ему можно. Изолированное хранилище представляет собой виртуальную файловую систему, которая позволяет записывать данные в небольшой относящийся к определенному пользователю и определенному приложению слот пространства. Реальное местоположение на жестком диске скрыто (т.е. узнать заранее, куда именно будут записываться данные, невозможно), а объем общего доступного пространства составляет 512 Кбайт. В Windows Vista для размещения обычно применяется путь вида c:\Users\[ИмяПользователя]\AppData\Local\IsolatedStorage\ [ИдентификаторGUID]. Данные в изолированном хранилище одного пользователя другим пользователям, не имеющим прав администратора, никогда не доступны. На заметку! Изолированное хранилище является .NET-эквивалентом долговременных cookie-наборов в обычной Web-странице — оно позволяет сохранять небольшие фрагменты информации в специальном месте, имеющем необходимые элементы управления для предотвращения злонамеренных атак (вроде кода, пытающегося заполнить жесткий диск или заменить системный файл). Подробную информацию об изолированном хранилище можно найти в справке .NET. Однако в его использовании в принципе нет ничего сложного, поскольку оно предоставляет ту же самую основанную на концепции потоков модель, что и обычный доступ к файлам. Все, что требуется — использовать типы в пространстве имен System.IO.IsolatedStorage. Сначала обычно нужно вызвать метод IsolatedStorageFile.GetUserStoreFor Application(), чтобы извлечь ссылку на изолированное хранилище текущего пользователя и приложения. (Каждое приложение получает отдельное хранилище.) Затем можно создать в том месте виртуальный файл с помощью IsolatedStorageFileStream. Например: // Создаем разрешение на запись в файл. string filePath = System.IO.Path.Combine(appPath, "highscores.txt"); FileIOPermission permission = new FileIOPermission( FileIOPermissionAccess.Write, filePath); // Выполняем проверку на предмет наличия этого разрешения. if (CheckPermission(permission)) { // Записываем данные на локальный жесткий диск. try { using (FileStream fs = File.Create(filePath)) { WriteHighScores(fs); } } catch { ... } }
Book_Pro_WPF-2.indb 284
19.05.2008 18:10:12
Страницы и навигация
285
else { // Записываем данные в изолированное хранилище. try { IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication(); using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream( "highscores.txt", FileMode.Create, store)) { WriteHighScores(fs); } } catch { ... } }
Еще также можно использовать методы вроде IsolatedStorageFile.GetFileNames() и IsolatedStorageFile.GetDirectoryNames() для перечисления содержимого изолированного хранилища текущего пользователя и приложения. Следует иметь в виду, что в случае создания обычного приложения XBAP, которое будет развертываться в Web, уже известно, что разрешения FileIOPermission для локального жесткого диска (или любого другого места) не будет, а это значит, что применять показанный здесь условный код нет никакого смысла. Вместо этого лучше сделать так, чтобы код сразу же переходил к классам изолированного хранилища. Совет. Объем данных, умещающихся в изолированном хранилище, можно увеличить, упаковав свои операции записи данных в файл с помощью DeflateStream или GZipStream. Оба этих типа определены в пространстве имен System.IO.Compression и применяют сжатие для сокращения количества требуемых байт.
Имитация диалоговых окон с помощью элемента управления Popup Еще одной ограниченной функциональной возможностью в XBAP является открытие вторичного окна. Во многих случаях вместо отдельных окон можно будет использовать навигацию и множество страниц и не вспоминать об этой возможности. Однако в некоторых случаях удобнее будет использовать именно всплывающее окно для отображения какого-то сообщения или суммарных данных. В автономных приложениях Windows для этой цели применяется модальное диалоговое окно. В приложениях XBAP для этой цели можно использовать элемент управления Popup, о котором уже говорилось в главе 7. В целом все довольно просто. Сначала нужно определить элемент управления Popup в своем коде разметки, установив для его свойства StaysOpen значение true, чтобы он оставался в открытом состоянии до тех пор, пока не будет закрыт. (Использовать свойство PopupAnimation и AllowsTransparency не имеет никакого смысла, поскольку на Web-странице они все равно работать не будут.) А затем следует включить подходящие кнопки вроде OK и Cancel (Отмена) и установить для свойства Placement значение Center, чтобы всплывающее окно появлялось посередине окна браузера. Ниже показан простой пример.
Book_Pro_WPF-2.indb 285
19.05.2008 18:10:12
286
Глава 9
Please enter your name. OK Cancel
В подходящий момент (например, при выполнении щелчка на кнопке) нужно отключить остальную часть пользовательского интерфейса и отобразить элемент Popup. Отключить пользовательский интерфейс можно, установив для свойства IsEnabled какого-то высокоуровневого контейнера, подобного StackPanel или Grid, значение false. (Также еще можно установить значение gray для свойства Background страницы, что будет привлекать внимание пользователя к окну Popup.) Чтобы отобразить элемент управления Popup, нужно просто установить для его свойства IsVisible значение true. Ниже показан обработчик событий, отображающий элемент Popup, который был определен ранее: private void cmdStart_Click(object sender, RoutedEventArgs e) { DisableMainPage(); } private void DisableMainPage() { mainPage.IsEnabled = false; this.Background = Brushes.LightGray; dialogPopUp.IsOpen = true; }
При выполнении пользователем щелчка на кнопке OK или Cancel (Отмена), нужно закрыть окно Popup путем установки для его свойства IsVisible значения false и снова включить остальную часть пользовательского интерфейса: private void dialog_cmdOK_Click(object sender, RoutedEventArgs e) { // Копируем имя из элемента Popup в основную страницу. lblName.Content = "You entered: " + txtName.Text; EnableMainPage(); } private void dialog_cmdCancel_Click(object sender, RoutedEventArgs e) { EnableMainPage(); } private void EnableMainPage() { mainPage.IsEnabled = true; this.Background = null; dialogPopUp.IsOpen = false; }
Book_Pro_WPF-2.indb 286
19.05.2008 18:10:13
Страницы и навигация
287
На рис. 9.16 показан этот элемент Popup в действии.
Рис. 9.16. Имитация диалогового окна с помощью элемента управления Popup Подход с применением элемента управления Popup для создания такого обходного пути имеет одно серьезное ограничение. Для гарантии того, что элемент управления Popup не сможет использоваться для фальсификации настоящих системных диалоговых окон, размер его окна ограничивается размером окна браузера. При наличии большого окна Popup и маленького окна браузера это может означать усечение части содержимого. Одно из решений, которое демонстрируется в примере кода для этой главы — упаковать все содержимое элемента управления Popup в ScrollViewer, установив для свойства VerticalScrollBarVisibility значение Auto. Существует еще один, даже еще более странный способ для отображения диалогового окна на странице WPF. Он подразумевает применение библиотеки Windows Forms из .NET 2.0. В частности, он позволяет безопасно создавать и отображать экземпляр класса System.Windows.Forms.Form (или любой специальной формы, унаследованной от Form), поскольку в таком случае разрешения на выполнение неуправляемого кода не требуется. На самом деле он даже позволяет отображать форму как немодальную, так, чтобы страница продолжала реагировать на действия пользователя. Единственный недостаток состоит в том, что в таком случае поверх формы автоматически появляется всплывающее окно с сообщением о безопасности, которое остается там до тех пор, пока пользователь на нем не щелкнет (рис. 9.17). Также имеются и ограничения в плане того, что можно отображать на форме. Элементы управления Windows Forms отображать можно, а содержимое WPF — нет. Пример использования этого приема можно Рис. 9.17. Использование для диалогового найти в демонстрационном коде для этой окна формы .NET 2.0 главы.
Book_Pro_WPF-2.indb 287
19.05.2008 18:10:13
288
Глава 9
Вставка XBAP-приложения в Web-страницу Обычно XBAP-приложение загружается прямо в браузере и потому занимает все доступное пространство. Однако допустим и другой вариант: можно сделать так, чтобы XBAP-приложение отображалось внутри HTML-страницы вместе с остальным HTMLсодержимым. Все, что для этого требуется — создать HTML-страницу и добавить в нее дескриптор , указывающий на файл .xbap: An HTML Page That Contains an XBAP Regular HTML Content More HTML Content
Методика с добавлением дескриптора применяется относительно редко, но позволяет использовать несколько новых приемов. Например, она позволяет отображать более одного XBAP-приложения в одном и том же окне браузера, а также создавать управляемые WPF графические элементы управления для боковой панели в Windows Vista. На заметку! Приложения WPF не имеют прямой поддержки для графических элементов управления Vista, но их можно вставлять в такие элементы управления с помощью дескриптора . Главный недостаток заключается в том, что накладные расходы, связанные с приложением WPF, выше таковых для обычной HTML- или JavaScript-страницы. Также еще имеются некоторые сложности и со способом, которым приложение WPF обрабатывает ввод мыши. Пример применения этой методики и подробное описание ее ограничений можно найти по адресу http://tinyurl.com/38e5se.
Резюме В этой главе была подробно рассмотрена модель навигации WPF. В частности, было показано, как создавать страницы, размещать их в различных контейнерах и применять навигацию WPF для перехода от одной страницы к другой. Также здесь была рассмотрена и модель XBAP, которая позволяет создавать WPFприложения в стиле Web, выполняющиеся в браузере. Поскольку приложения XBAP все равно требуют наличия .NET Framework, существующие Web-приложения и Flash-игры, которые мы все знаем и любим, они не заменят. Однако они запросто могут служить альтернативным способом предоставления пользователям Windows богатого содержимого и графики. Например, компания вроде Microsoft легко могла бы создать с помощью XBAP альтернативный интерфейс для такого популярного Web-приложения, как Hotmail. Для построения успешных XBAP-приложений следует помнить об ограничениях и особенностях кода, на привыкание к которым может уйти некоторое время. На заметку! Те, кто планирует создавать приложения WPF, запускающиеся в Web-браузере через Internet, могут рассмотреть вариант использования родственной WPF, но менее масштабной технологии под названием Silverlight 2.0. Хотя эта технология и не является такой же мощной, как WPF, она содержит значительную часть модели WPF и предлагает дополнительную поддержку для межплатформенного применения. (Например, приложения Silverlight 2.0 могут запросто запускаться в браузере Safari на компьютере Mac.) Более подробную информацию о Silverlight можно найти по адресу http://silverlight.net.
Book_Pro_WPF-2.indb 288
19.05.2008 18:10:13
ГЛАВА
10
Команды В
главе 6 рассказывалось о маршрутизируемых событиях, которые можно использовать для ответа на множество различных действий мыши и клавиатуры. Однако события являются компонентом довольно низкого уровня. В реальном приложении функциональные возможности делятся на задачи, имеющие более высокий уровень. Эти задачи могут инициироваться различными действиями и через различные элементы пользовательского интерфейса, включая главные меню, контекстные меню, клавиатурные комбинации и панели инструментов. WPF позволяет определять эти задачи, называемые командами, и подключать элементы управления к ним, избегая необходимости писать повторяющийся код обработки событий. Даже еще более важно то, что функция команд управляет состоянием пользовательского интерфейса путем автоматического отключения элементов управления при недоступности связанных команд. Она также предоставляет центральное место для хранения (и локализации) текстовых заголовков команд. В этой главе речь пойдет о том, как использовать заготовленные классы команд в WPF, как связывать их с элементами управления и как определять свои собственные команды. Также здесь будут рассмотрены ограничения модели команд, такие как отсутствие журнала хронологии команд и отсутствие поддержки для используемой на уровне приложения функции Undo, и показано, как создавать свои собственные аналоги.
Общие сведения о командах В хорошо спроектированном приложении Windows логика приложения находится не в обработчиках событий, а закодирована в имеющих более высокий уровень методах. Каждый из этих методов представляет одну решаемую приложением “задачу” (task). Каждая задача может полагаться на дополнительные библиотеки (вроде отдельно компилируемых компонентов, в которых инкапсулируется бизнес-логика или доступ к базе данных). Пример таких отношений показан на рис. 10.1. Наиболее очевидным способом использования такого дизайна является добавление обработчиков событий везде, где они нужны, и применение каждого из них для вызова соответствующего метода приложения. По сути, в таком случае код окна превращается в облегченную коммутационную панель, которая реагирует на ввод и пересылает запросы внутрь приложения. Хотя такой дизайн является вполне разумным, он не экономит никаких усилий. Многие задачи приложения могут инициироваться по различным маршрутам, из-за чего часто все равно приходится писать несколько обработчиков событий, вызывающих один и тот же метод приложения. Именно в этом нет особой проблемы (потому что код коммутационной панели так прост), но жизнь гораздо усложняется, когда приходится иметь дело с состоянием пользовательского интерфейса.
Book_Pro_WPF-2.indb 289
19.05.2008 18:10:13
290
Глава 10
Window File New Open Save Print
Класс Window
Класс ApplicationTasks
Класс PrintServiceClass
Обработчик события mnuPrint_Click()
PrintDocument()
PrintPage()
Обработчик события window_KeyDown()
SaveDocument()
Обработчик события cmdPrint_Click()
OpenDocument()
...
...
Print
Рис. 10.1. Отображение обработчиков событий на задачу Понять, о чем идет речь, поможет простой пример. Предположим, что есть программа, в состав которой входит метод по имени PrintDocument(). Этот метод может инициироваться четырьмя способами: через главное меню (путем выбора в меню File (Файл) команды Print (Печать)), через контекстное меню (путем выполнения щелчка правой кнопкой мыши в пустой области и выбора в появившемся контекстном меню команды Print (Печать)), с помощью клавиатурной комбинации () и с помощью соответствующей кнопки в панели инструментов. В определенных точках жизненного цикла приложения задача PrintDocument() должна быть недоступной. Это подразумевает отключение соответствующих команд в двух меню и кнопки в панели инструментов таким образом, чтобы на них нельзя было выполнять щелчок, а также игнорирование клавиатурной комбинации . Написание делающего это кода (и добавление кода, включающего данные элементы управления позже) — очень непростой подход. Даже еще хуже то, что допущение в нем ошибки может привести к тому, что различные блоки кода состояния будут перекрываться неправильно, оставляя элемент управления в активном состоянии даже тогда, когда он не должен быть доступен. Написание и отладка подобного кода является одним из наименее приятных аспектов разработки Windows-приложений. К удивлению многих опытных разработчиков Windows-приложений, в наборе инструментальных средств Windows Forms не было никаких функциональных возможностей, которые могли бы облегчать выполнение подобных операций. Разработчики могли создавать необходимую им инфраструктуру самостоятельно, но большинство из них предпочитало этого не делать. К счастью, WPF заполняет этот пробел, предлагая новую командную модель, которая предоставляет две следующих важных возможности:
• делегирование событий подходящим командам; • поддержание включенного состояния элемента управления в синхронизированном виде с помощью состояния соответствующей команды. Командная модель WPF является не настолько прямолинейной, как хотелось бы. Для подключения к модели маршрутизируемых событий ей требуется несколько отдельных компонентов, о которых еще будет рассказываться в этой главе. Однако в концептуальном плане она является достаточно простой. На рис. 10.2 показано, как основанное на командах приложение изменяет дизайн, приводившийся на рис. 10.1. Теперь каждое действие, которое инициирует печать (т.е. щелчок на кнопке, щелчок на элементе меню и нажатие клавиатурной комбинации ), отображается на одну и ту же команду. Эта команда с помощью привязки соединяется в коде со всего лишь одним обработчиком событий.
Book_Pro_WPF-2.indb 290
19.05.2008 18:10:13
Команды
291
Window
Класс Window
File New Open Save Print
CommandBinding
Обработчик событий commandBinding_Executed()
...
Класс ApplicationTasks
Класс PrintServiceClass
PrintDocument()
PrintPage()
SaveDocument()
OpenDocument() Print ...
Рис. 10.2. Отображение событий на команду Система команд WPF является прекрасным средством для упрощения дизайна приложения. Однако в ней все равно имеются кое-какие серьезные пробелы. В частности, WPF не поддерживает:
• отслеживание команд (например, хронология ранее выполненных команд); • “невыполнимые” команды; • команды, которые имеют состояние и могут находиться в различных “режимах” (например, команда, которая может включаться и отключаться).
Модель команд WPF Модель команд WPF состоит из удивительного количества подвижных частей. Ключевыми в ней являются четыре следующих компонента.
• Команды. Команда представляет задачу приложения и следит за тем, когда она может быть выполнена. Однако кода, выполняющего задачу приложения, команды на самом деле не содержат.
• Привязки команд. Каждая привязка (binding) подразумевает соединение команды с имеющей к ней отношение логикой приложения, отвечающей за облуживание определенной области пользовательского интерфейса. Такой факторизованный дизайн очень важен, потому что одна и та же команда может использоваться в нескольких местах в приложении и иметь в каждом из них разное предназначение. Для обеспечения подобного поведения как раз и служат разные привязки одной и той же команды.
• Источники команд. Источник команды инициирует команду. Например, и элемент управления MenuItem, и элемент управления Button могут служить источниками команд. Щелчок на них в таком случае будет приводить к выполнению привязанной команды.
• Целевые объекты команд. Целевой объект команды — это элемент, для которого предназначена данная команда, т.е. элемент, на котором она выполняется. Например, команда Paste может вставлять текст в элемент TextBox, а команда OpenFile — отображать документ в элементе DocumentViewer. Целевой объект может быть важен, а может быть и неважен, что зависит от природы команды. В следующих разделах вы сможете более подробно ознакомиться с тем, что собой представляет первый компонент — команда WPF.
Book_Pro_WPF-2.indb 291
19.05.2008 18:10:13
292
Глава 10
Интерфейс ICommand Сердцем модели команд WPF является интерфейс System.Windows.Input.ICommand, определяющий способ, в соответствие с которым работают команды. Этот интерфейс включает два метода и событие: public interface ICommand { void Execute(object parameter); bool CanExecute(object parameter); event EventHandler CanExecuteChanged; }
В простой реализации в методе Execute() должна содержаться логика приложения, касающаяся задачи (например, печати документа). Однако, как будет показано в следующем разделе, WPF является немного более совершенной технологией. Она использует метод Execute() для запуска более сложного процесса, который, в конечном счете, заканчивается возбуждением события, обрабатываемого в совершенно другом месте в приложении. Это дает разработчику возможность использовать готовые классы команд и включать в них свою собственную логику, а также гибкость применения одной команды (например, Print) в нескольких различных местах. Метод CanExecute() возвращает информацию о состоянии команды, а именно — значение true, если она включена, и значение false, если она отключена. Методы Execute() и CanExecute() принимают дополнительный объект-параметр, который можно использовать для передачи с ними любой другой необходимой информации. И, наконец, событие CanExecuteChanged вызывается при изменении состояния. Для любых использующих данную команду элементов управления оно является сигналом о том, что им следует вызвать метод CanExecute() и проверить состояние команды. Это часть связующего элемента, который позволяет источникам команд (вроде элемента управления Button или элемента управления MenuItem) автоматически включать себя, когда команда доступна, и отключать, когда она не доступна.
Класс RoutedCommand При создании своих собственных команд реализовать интерфейс ICommand напрямую не обязательно. Вместо этого можно использовать класс System.Windows. Input.RoutedCommand , который реализует этот интерфейс автоматически. Класс RoutedCommand является единственным классом в WPF, который реализует интерфейс ICommand. Другими словами, все команды WPF представляют собой экземпляры класса RoutedCommand (или производного от него класса). Одна из ключевых концепций, лежащих в основе модели команд в WPF, состоит в том, что класс RoutedCommand не содержит никакой логики приложения. Он просто представляет команду. Это означает, что один объект RoutedCommand обладает теми же возможностями, что и другой. Класс RoutedCommand добавляет дополнительную инфраструктуру для туннелирования и перемещения событий. Если интерфейс ICommand инкапсулирует идею команды — действие, которое может инициироваться и быть или не быть доступным, то класс RoutedCommand изменяет команду так, чтобы она могла подобно пузырьку (bubble) подниматься вверх по иерархии элементов WPF до подходящего обработчика событий. Для поддержки маршрутизируемых событий класс RoutedCommand реализует интерфейс ICommand как приватный и затем добавляет немного отличающиеся версии его методов. Наиболее заметным изменением является то, что методы Execute() и
Book_Pro_WPF-2.indb 292
19.05.2008 18:10:14
Команды
293
CanExecute() теперь принимают дополнительный параметр. Ниже показано, как выглядят новые сигнатуры этих методов: public void Execute(object parameter, IInputElement target) {...} public bool CanExecute(object parameter, IInputElement target) {...}
target — это целевой элемент, в котором начинается обработка события. Это событие начинает обрабатываться в целевом элементе и затем поднимается вверх до находящихся на более высоком уровне контейнеров до тех пор, пока приложение не использует его для выполнения подходящей задачи. (Для обработки события Executed элементу необходима помощь еще одного класса — класса CommandBinding.) Помимо этого класс RoutedCommand также вводит три свойства: Name (представляющее имя команды), OwnerType (представляющее класс, членом которого является данная команда) и коллекция InputGestures (представляющая любые клавиши, клавиатурные комбинации или действия с мышью, которые также могут применяться для вызова данной команды).
Зачем командам нужно перемещение событий? При первом взгляде на модель команд WPF трудно сразу же понять точно, почему команды WPF требуют использования маршрутизируемых событий. В конце концов, разве не должен объект команды заботиться о ее выполнении независимо от того, как она вызывается? Если бы для создания своих собственных команд интерфейс ICommand нужно было использовать напрямую, это было бы так. Код нужно было бы жестко кодировать внутри команды, так чтобы он работал одинаково независимо от того, что приводит к ее инициации. В перемещении событий (event bubbling) не было бы абсолютно никакой необходимости. Однако WPF использует ряд заготовленных команд. Классы этих команд не содержат никакого реального кода. Они являются просто удобно определенными объектами, которые представляют общую задачу приложения (например, печать документа). Для выполнения действий над этими командами необходимо использовать привязку (binding), вызывающую в коде соответствующее событие (см. рис. 10.2). Для обеспечения возможности выполнения данного события в одном месте, даже если оно возбуждается разными источниками команд в одном и том же окне, как раз и необходима мощь перемещения событий. Из этого вытекает интересный вопрос: “Зачем тогда вообще использовать заготовленные команды?”. Не лучше ли бы было заставлять выполнять всю работу специальные классы команд, вместо того, чтобы полагаться на обработчики событий? Во многом такой дизайн был бы проще. Однако преимущество заготовленных команд состоит в том, что они предлагают гораздо более удобные возможности для интеграции. Например, предположим, что некий сторонний разработчик создал элемент управления DocumentView, использующий заготовленную команду Print. Если в вашем приложении применяется такая же заготовленная команда, вам не придется прилагать никаких дополнительных усилий для включения в него возможности печати. С этой точки зрения команды являются одним из главных компонентов сменной архитектуры WPF.
Класс RoutedUICommand Большинство команд, с которыми разработчику доведется иметь дело, будут не объектами RoutedCommand, а экземплярами класса RoutedUICommand, который наследуется от класса RoutedCommand. (На самом деле все заготовленные команды, которые предоставляет WPF, являются именно объектами RoutedUICommand.)
Book_Pro_WPF-2.indb 293
19.05.2008 18:10:14
294
Глава 10
Класс RoutedUICommand предназначен для команд с текстом, который должен отображаться где-нибудь в пользовательском интерфейсе (например, текстом для элемента меню или текстом подсказки для кнопки в панели инструментов). Он добавляет одно единственное свойств — Text, в котором и указывается подлежащий отображению текст для данной команды. Преимущество определения текста команды с командой (а не непосредственно с элементом управления) состоит в том, что это позволяет выполнять локализацию в одном месте. Однако если текст команды никогда не должен отображаться нигде в пользовательском интерфейсе, класс RoutedCommand подходит ничуть не меньше. На заметку! Применять в пользовательском интерфейсе текст RoutedUICommand вовсе не обязательно. На самом деле могут существовать веские причины для того, чтобы использовать что-нибудь другое. Например, предпочтение может быть отдано применению вместо текста “Print Document” (Печать документа) просто слова “Print” (Печать), а в некоторых случаях — даже вообще полной замене текста небольшим графическим изображением.
Библиотека команд Разработчики WPF учли тот факт, что в каждом приложении может использоваться огромное количество команд, и что многие команды могут быть общими для множества приложений. Например, во всех приложениях, предназначенных для обработки документов, будут присутствовать свои собственные версии команд New (Создать), Open (Открыть) и Save (Сохранить). Поэтому для уменьшения объема усилий, необходимых для создания таких команд, они включили в состав WPF библиотеку базовых команд, в которой содержится более 100 команд. Все эти команды доступны через статические свойства пяти соответствующих статических классов.
• ApplicationCommands. Этот класс предоставляет общие команды, включая команды, связанные с буфером обмена (такие как Copy (Копировать), Cut (Вырезать) и Paste (Вставить)), и команды, касающиеся обработки документов (вроде New (Создать), Open (Открыть), Save (Сохранить), SaveAs (Сохранить как), Print (Печать) и т.д.).
• NavigationCommands. Этот класс предоставляет команды, используемые для навигации, включая те, что предназначены для страничных приложений (наподобие команды BrowseBack (Назад), BrowseForward (Вперед) и NextPage (Переход)), и те, что подходят для приложений, предназначенных для работы с документами (вроде команды IncreaseZoom (Масштаб) и Refresh (Обновить)).
• EditingCommands. Этот класс предоставляет длинный перечень команд, предназначенных по большей части для редактирования документов, включая команды для перемещения (MoveToLineEnd (Переход в конец строки), MoveLeftByWord (Переход влево на одно слово), MoveUpByPage (Переход на одну страницу вверх) и т.д.), выделения содержимого (SelectToLineEnd (Выделение до конца строки), SelectLeftByWord (Выделение слова слева)) и изменения форматирования (ToggleBold (Выделение полужирным) и ToggleUnderline (Выделение подчеркиванием)).
• MediaCommands. Этот класс включает набор команд для работы с мультимедиа (среди которых команда Play (Воспроизвести), Pause (Пауза), NextTrack (Переход к следующей композиции) и IncreaseVolume (Увеличение громкости)).
Book_Pro_WPF-2.indb 294
19.05.2008 18:10:14
Команды
295
Класс ApplicationCommands предоставляет ряд основных команд, которые наиболее часто используются во всех типах приложений, поэтому с ними стоит ознакомиться. Полный перечень команд этого класса выглядит так:
• New (Создать) • Open (Открыть) • Save (Сохранить) • SaveAs (Сохранить как) • Close (Закрыть) • Print (Печать) • PrintPreview (Предварительный просмотр) • CancelPrint (Отмена печати) • Copy (Копировать) • Cut (Вырезать) • Paste (Вставить) • Delete (Удалить) • Undo (Отменить) • Redo (Повторить) • Find (Найти) • Replace (Заменить) • SelectAll (Выделить все) • Stop (Остановить) • ContextMenu (Контекстное меню) • CorrectionList (Список исправлений) • Properties (Свойства) • Help (Справка) Например, ApplicationCommands.Open является статическим свойством, которое предоставляет объект RoutedUICommand. Этот объект представляет в приложении команду “Open” (Открыть). Поскольку ApplicationCommands.Open представляет собой статическое свойство, во всем приложении может существовать всего лишь один экземпляр команды Open. Однако применяться он может по-разному, в зависимости от его источника, т.е. того места, где он встречается в пользовательском интерфейсе. Свойство RoutedUICommand.Text отображает имя каждой команды, добавляя, где нужно, пробелы между словами. Например, для команды ApplicationCommands. SelectAll оно отображает текст “Select All” (Выделить все). (Свойство Name отображает тот же самый текст, но без пробелов.) Свойство RoutedUICommand.OwnerType возвращает тип объекта для класса ApplicationCommands, поскольку команда Open является статическим свойством этого класса. Совет. Свойство Text команды можно изменять перед его привязкой к окну (например, путем использования соответствующего кода в конструкторе окна или класса приложения). Поскольку команды представляют собой статические объекты, являющиеся глобальными для всего приложения, изменение текста команды влияет на нее везде, где она встречается в пользовательском интерфейсе. В отличие от свойства Text, свойство Name изменять нельзя.
Book_Pro_WPF-2.indb 295
19.05.2008 18:10:14
296
Глава 10
Как уже говорилось, все эти отдельные объекты команд являются всего лишь маркерами, не имеющими никакой реальной функциональности. Однако у многих из них имеется одна дополнительная функция: привязка ввода по умолчанию. Например, команда ApplicationCommands.Open отображается на комбинацию клавиш . После привязки этой клавиатурной комбинации к команде и ее добавления в окно в виде источника данной команды она становится активной, даже если команда и не отображается нигде в пользовательском интерфейсе.
Выполнение команд Пока что мы более подробно рассмотрели команды, а именно — их базовые классы и интерфейсы, а также библиотеку команд, которую WPF предлагает для использования. Однако ни одного примера применения этих команд еще не приводилось. Как уже объяснялось ранее, объект RoutedUICommand не имеет никаких жестко закодированных функциональных возможностей. Он просто представляет команду. Для инициализации этой команды необходимо использовать источник команды (или специальный код), а для ответа на нее — привязку команды с переадресацией ее выполнения обычному обработчику событий. Об этих двух компонентах как раз и пойдет речь в следующих разделах
Источники команд Команды в библиотеке команд всегда доступны. Самый простой способ инициировать их — это привязать к элементу управления, реализующему интерфейс ICommandSource; сюда относятся элементы управления, унаследованные от ButtonBase (Button, CheckBox и т.д.), а также отдельные объекты ListBoxItem, элемент Hyperlink и элемент MenuItem. Интерфейс ICommandSource предоставляет три свойства, которые перечислены в табл. 10.1.
Таблица 10.1. Свойства интерфейса ICommandSource Имя
Описание
Command
Указывает на связанную команду. Это единственная требуемая деталь.
CommandParameter
Предоставляет любые другие данные, которые должны отправляться с командой.
CommandTarget
Идентифицирует элемент, в котором должна выполняться данная команда.
Например, ниже показан код, в котором с помощью свойства Command кнопка связывается с командой ApplicationCommands.New: New
WPF является достаточно разумной для того, чтобы выполнять поиск по всем пяти описанным выше классам-контейнерам команд, а это значит, что предыдущую строку кода можно записать и короче: New
Однако такой синтаксис может показаться менее явным и, следовательно, менее понятным, потому что он не указывает, в каком именно классе содержится команда.
Book_Pro_WPF-2.indb 296
19.05.2008 18:10:14
Команды
297
Привязки команд При присоединении команды к источнику команды вы увидите нечто интересное: источник команды будет автоматически отключен. Например, если вы создадите показанную в предыдущем разделе кнопку New (Создать), она появится как затененная и недоступная для щелчка, как будто для ее свойства IsEnabled было установлено значение false (рис. 10.3). А все потому, что кноп- Рис. 10.3. Команда без привязки ка запросила состояние команды. Из-за отсутствия у команды привязки она считается отключенной. Чтобы изменить такое положение дел, потребуется создать для команды привязку и указать три перечисленных ниже вещи.
• Действие, которое должно выполняться при инициировании команды. • Способ определения того, может ли команда быть выполнена, т.е. доступна ли она. (Это указывать не обязательно. В случае, когда эта деталь опускается, команда всегда является доступной при условии наличия присоединенного обработчика событий.)
• Область, на которую должно распространяться действие команды. Например, она может как ограничиваться одной единственной кнопкой, так и распространяться на все окно (что встречается чаще). Ниже показан фрагмент кода, в котором создается привязка для команды New. Этот код может быть добавлен к конструктору окна: // Создание привязки CommandBinding binding = new CommandBinding(ApplicationCommands.New); // Присоединение обработчика событий binding.Executed += NewCommand_Executed; // Регистрация привязки this.CommandBindings.Add(binding);
Важно обратить внимание, что готовый объект CommandBinding добавляется в коллекцию CommandBindings содержащего окна. Это работает за счет перемещения событий. По сути, происходит следующее: при выполнении щелчка на кнопке событие CommandBinding.Executed “поднимается” от уровня кнопки до уровня содержащих элементов. Хотя обычно все привязки добавляются в окно, свойство CommandBindings на самом деле определено в базовом классе UIElement. Это означает, что оно поддерживается любым элементом. Например, приведенный пример работал бы точно также, даже если бы привязка команды была добавлена и прямо в код использующей эту команду кнопки (хотя тогда использовать ее повторно с каким-то другим элементом более высокого уровня было бы невозможно). Для получения наибольшей гибкости, привязки команд обычно добавляются в окно наивысшего уровня. Если необходимо использовать ту же самую команду в более чем одном окне, привязку потребуется создавать в обоих окнах. На заметку! Также еще можно обрабатывать событие CommandBinding.PreviewExecuted, которое сначала возбуждается в контейнере наивысшего уровня (т.е. окне) и затем посредством туннелирования опускается до уровня кнопки. Как рассказывалось в главе 6, туннелирование событий (event tunneling) применяется для перехвата и остановки события перед его завершением. Если для свойства RoutedEventArgs.Handled устанавливается значение true, событие Executed не будет возникать никогда.
Book_Pro_WPF-2.indb 297
19.05.2008 18:10:14
298
Глава 10
В предыдущем коде предполагается, что в том же самом классе имеется готовый к получению команды обработчик событий по имени NewCommand_Executed. Ниже показан пример простого кода для отображения источника команды: private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New command triggered by " + e.Source.ToString()); }
Теперь при запуске приложения кнопка является доступной (рис. 10.4). В случае выполнения на ней щелчка возбуждается событие Executed, которое затем поднимается до уровня окна и обрабатывается показанным ранее обработчиком NewCommand(). На этом этапе WPF сообщает об источнике событие (кнопке). Объект ExecutedRoutedEventArgs также позволяет извлечь ссылку на команду, которая была вызвана (т.е. команду ExecutedRoutedEventArgs.Command), и любую дополнительную информацию, которая была передана вместе с ней (параметр ExecutedRoutedEventArgs.Parameter). В данном примере никакая дополнительная информация не передавалась, поэтому значением параметра ExecutedRoutedEventArgs.Parameter будет null. (При желании передать дополнительную информацию нужно было бы установить свойство CommandParameter источника команды, а при желании передать часть информации, извлекаемой из какого-то другого элемента управления, это свойство нужно было бы установить, используя выражение привязки, как будет показано далее в этой главе.)
Рис. 10.4. Команда с привязкой
На заметку! В этом примере реагирующий на команду обработчик событий находится внутри кода того же окна, в котором создается команда. Все те же правила хорошей организации кода применимы и к этому примеру — другими словами, окно, где необходимо, должно делегировать свою работу другим компонентам. Например, если команда подразумевает открытие файла, можно использовать специально созданный вспомогательный класс файла для сериализации и десериализации информации. Подобным образом в случае создания команды, подразумевающей обновление отображаемых данных, можно использовать ее для вызова в компоненте базы данных метода, выполняющего выборку необходимых данных. Пример такой команды был показан на рис. 10.2. В предыдущем примере привязка команды была сгенерирована с помощью кода. Однако команды так же легко можно привязывать и декларативным образом с помощью XAML, если требуется упростить лежащий в основе кода файл. Необходимый для этого код разметки выглядит так:
Book_Pro_WPF-2.indb 298
19.05.2008 18:10:14
Команды
299
New
К сожалению, в Visual Studio не предлагается никакой поддержки для определения привязок команд во время проектирования, а также предоставляется относительно слабая поддержка для подключения элементов управления и команд. Окно Properties (Свойства) позволяет устанавливать для элемента управления свойство Command (Команда), однако вводить точное имя команды нужно самому — никакого удобного раскрывающегося списка возможных вариантов команд для выбора не предусмотрено.
Использование множества источников команд Пример с кнопкой немного напоминает обходной путь для инициации обычного события. Однако дополнительный уровень команды начинает приобретать отчетливый смысл при добавлении большего количества использующих эту команду элементов управления. Например, мы можем добавить элемент меню, также работающий с командой New:
Обратите внимание, что данный объект MenuItem для команды New не устанавливает свойство Header. Все дело в том, что элемент управления MenuItem способен извлекать текст из команды в случае, если свойство Header не устанавливается. (У элемента Button такой возможности нет.) Может показаться, что это является довольно незначительным удобством, но оно играет очень важную роль, если планируется локализация приложения на разных языках. В таком случае изменить текст в одном месте (за счет установки в командах свойства Text) легче, чем отслеживать его во всех окнах. У класса MenuItem есть еще одна приятная функция. Он автоматически выбирает первую клавишу быстрого вызова команды, которая содержится в коллекции Command.InputBindings (если таковая имеется). В случае объекта ApplicationsCommands.New это означает, что в меню рядом с текстом появляется клавиатурная Рис. 10.5. Элемент меню, использующий команду комбинация (рис. 10.5). На заметку! Чего у MenuItem нет, так это функции отображения подчеркнутой клавиши доступа. WPF не имеет возможности, позволяющей узнавать, какие команды могут размещаться в меню вместе, поэтому и не может определять подходящие клавиши доступа. Это означает, что при желании использовать в качестве клавиши быстрого доступа (так, чтобы она отображалась в подчеркнутом виде при открытии меню с помощью клавиатуры, и чтобы пользователь мог инициировать команду New путем нажатия этой клавиши), соответствующий текст меню придется задать вручную, не забыв поставить перед клавишей доступа символ подчеркивания. То же самое придется сделать и в случае клавиши быстрого доступа для кнопки.
Book_Pro_WPF-2.indb 299
19.05.2008 18:10:15
300
Глава 10
Обратите внимание, что создавать еще одну привязку команды для элемента меню не нужно. Одна привязка, созданная в предыдущем разделе, теперь применяется двумя разными элементами управления, оба из которых передают свою работу одному и тому же обработчику событий.
Точная настройка текста команды Способность меню автоматически извлекать текст элемента команды может вызвать вопрос о том, а можно ли такое же делать с другими классами ICommandSource, например, с элементом управления Button. Можно, но это требует приложения дополнительных усилий. В частности, для многократного использования текста команды существуют два способа. Первый подразумевает извлечение текста прямо из статического объекта команды. XAML позволяет делать подобное с помощью расширения Static. Ниже показан пример кода, который извлекает имя команды “New” (Создать) и использует его в качестве текста для кнопки:
Проблема такого подхода состоит в том, что он предполагает просто вызов на объекте команды метода ToString(), что позволяет получить имя команды, но не ее текст. (В случае команд, состоящих из множества слов, лучше использовать текст, а не имя команды, потому что текст включает пробелы.) Данную проблему можно устранить, но для этого потребуется приложить гораздо больше усилий. Также еще одна проблема связана со способом, которым одна кнопка дважды использует одну и ту же команду, что увеличивает вероятность случайного извлечения текста не из той команды. Поэтому предпочтительным решением считается применение выражения привязки данных. Эта привязка данных является немного необычной, поскольку подразумевает привязку к текущему элементу, захват используемого объекта Command и извлечение его свойства Text. Весь необходимый (и довольно-таки длинный синтаксис) показан ниже:
На рис. 15.8 показана определяемая этим шаблоном кнопка. В этом примере при наведении пользователем на кнопку курсора мыши используется градиентная заливка. Однако градиентная заливка всегда размещается по середине кнопки. При желании создать более экзотический эффект, например, градиентную заливку, которая следует за курсором мыши, потребуется использовать анимацию или писать код. В главе 24 будет приводиться пример со специальным классом Chrome, реализующим такой эффект.
Рис. 15.8. Кнопка с градиентной заливкой
Использование шаблонов со стилями
У такого дизайна есть одно ограничение. Шаблон элемента управления, по сути, жестко кодирует ряд деталей вроде цветовой схемы. Это означает, что при желании использовать в кнопке ту же самую комбинацию элементов (Border, Grid, Rectangle и ContentPresenter) и организовать их тем же самым образом, но при этом предоставить другую цветовую схему, придется создавать новую копию шаблона, ссылающуюся на другие ресурсы кисти. Это необязательно будет проблемой (в конце концов, детали компоновки и форматирования могут быть связаны настолько тесно, что их все равно нельзя будет разделять), однако может действительно ограничить возможность повторного использования данного шаблона элемента управления. Если шаблон содержит сложную комбинацию элементов, которую точно нужно будет применять многократно с разными деталями форматирования (каковым чаше всего оказываются цвета и шрифты), тогда лучше извлечь эти детали из шаблона и поместить их в стиль.
Book_Pro_WPF-2.indb 450
19.05.2008 18:10:37
451
Шаблоны элементов управления
Чтобы это получилось, шаблон придется переделать. Вместо того чтобы использовать жестко закодированные цвета, нужно будет извлечь эту информацию из свойств элемента управления с помощью привязок шаблона. Ниже приведен пример упрощенного шаблона для показанной ранее специализированной кнопки. В этом шаблоне некоторые детали, а именно — поле фокуса и граница с закругленными углами и толщиной в 2 единицы — воспринимаются как фундаментальные и не изменяющиеся. Кисти фона и границы, однако, являются конфигурируемыми. Единственный оставшийся триггер отвечает за отображение поля фокуса:
Связанный стиль применяет этот шаблон элемента управления, устанавливает цвета границы и фона и добавляет триггеры для изменения фона в зависимости от состояния кнопки:
Book_Pro_WPF-2.indb 451
19.05.2008 18:10:37
452
Глава 15
В идеале все триггеры можно было бы оставить в шаблоне элемента управления, поскольку они представляют поведение элемента управления и используют стиль просто для установки базовых свойств. К сожалению, здесь это невозможно из-за необходимости предоставить стилю возможность устанавливать цветовую схему. На заметку! Если установить триггеры и в шаблоне элемента управления, и в стиле, предпочтение будет отдаваться триггерам стиля. Чтобы использовать этот новый шаблон, нужно установить не свойство Template, а свойство Style: A Simple Button with a Custom Template
Теперь можно создать новые стили, использующие тот же шаблон, но привязываемые к другим стилями для применения новой схемы цветов. У такого подхода есть одно важное ограничение. Использовать свойство Setter. TargetName в этом стиле нельзя, поскольку он не содержит шаблона элемента управления (а просто ссылается на него). Поэтому возможности стиля и триггеров являются несколько ограниченными. Он не могут проникать глубоко в визуальное дерево для изменения того или иного аспекта вложенного элемента. Вместо этого стилю нужно устанавливать свойство элемента управления, а вложенному в этот элемент управления элементу — привязывать это свойство с помощью привязки шаблона.
Шаблоны и специальные элементы управления Обе указанных проблемы, а именно — необходимость определять поведение элемента управления в стиле с помощью триггеров и отсутствие возможности воздействовать на конкретные элементы — можно обойти путем создания специального шаблона. Например, можно создать класс, унаследованный от класса Button и включающий такие дополнительные свойства, как HighlightBackground, DisabledBackground и PressedBackground, а затем выполнить привязку к этим свойствам в шаблоне элемента управления и просто установить для них значения в стиле безо всяких триггеров. Однако такой подход имеет свои недостатки. Он вынуждает применять в пользовательском интерфейсе другой элемент управления (например, не просто Button, а CustomButton). Это усложняет процесс разработки приложения. Обычно со специальных шаблонов элементов управления на специальные элементы управления переходят в одном из следующих случаев. • Если элемент управления требует серьезных изменений в его функциональности. Например, имеется специальная кнопка, и эта кнопка добавляет новые функциональные возможности, которые требуют использования новых свойств и методов. • Если планируется сделать элемент управления доступным в отдельной сборке библиотеки классов, чтобы его можно было использовать и настраивать для множества других приложений. В таком случае требуется более высокий уровень стандартизации, чем возможен при применении одних только шаблонов элементов управления. Более подробную информацию о том, как создавать специальный элемент управления, можно найти в главе 24.
Автоматическое применение шаблонов В текущем примере каждая кнопка сама отвечает за подключение к соответствующему шаблону с помощью свойства Template или Style. Такой подход имеет смысл, если шаблон элемента управления применяется для создания определенного эффекта в
Book_Pro_WPF-2.indb 452
19.05.2008 18:10:38
Шаблоны элементов управления
453
определенном месте приложения. Но он менее удобен, если специальный внешний вид требуется обеспечить для каждой кнопки во всем приложении. В таком случае, скорее всего, возникнет желание сделать так, чтобы все кнопки в приложении получили новый шаблон автоматически. Добиться такого эффекта можно путем применения шаблона со стилем. Секрет заключается в использовании типизированного стиля, влияющего на элементы соответствующего типа автоматически и устанавливающего свойство Template. Ниже приведен пример стиля, который можно было бы поместить в коллекцию ресурсов словаря ресурсов для придания кнопкам нового вида:
Book_Pro_WPF-2.indb 464
19.05.2008 18:10:39
Шаблоны элементов управления
465
Элементом наивысшего уровня в этом шаблоне является объект Border, отвечающий за границу окна. Внутри него находится элемент управления Grid с тремя строками. Содержимое элемента управления Grid поделено следующим образом.
• В верхней строке содержится строка заголовка, состоящая из обычного элемента TextBlock, который отображает заголовок и кнопку закрытия окна. Заголовок окна извлекается из свойства Window.Title с помощью привязки шаблона.
• В средней строке содержится вложенный объект Border с остальным содержимым окна. Это содержимое вставляется с помощью элемента ContentPresenter. Элемент ContentPresenter упаковывается в элемент AdornerDecorator, который гарантирует размещение декоративного слоя поверх содержимого элементов.
• В третьей строке содержится еще один элемент ContentPresenter. Однако этот элемент не использует стандартную привязку для извлечения своего содержимого из свойства Window.Content. Вместо этого он извлекает его явным образом из свойства Window.Tag. Как правило, таким содержимым является обычный текст, но оно может включать и содержимое любого элемента, которое хочет использовать разработчик. На заметку! Свойство Tag используется потому, что в классе Window нет никакого свойства для хранения текста нижнего колонтитула. Однако возможен и другой вариант, а именно — создать специальный класс, унаследованный от Window и включающий необходимое свойство Footer.
• В третьей строке еще также находится элемент захвата и изменения размера. Триггер отображает этот элемент тогда, когда для свойства Window.ResizeMode устанавливается значение CanResizeWithGrip. Здесь не показаны две детали: довольно-таки неинтересный стиль для элемента захвата и изменения размера (который просто создает небольшой узор из точек для использования в качестве такого элемента) и кнопка для закрытия окна (с небольшим знаком X на красном квадратном фоне). В этой разметке также отсутствуют и детали форматирования вроде градиентной кисти, закрашивающей фон, и свойств, создающих аккуратно закругленную граничную каемку. Просмотреть полную версию этой разметки можно в прилагаемом к этой главе примере кода. Шаблон окна применяется с помощью простого стиля. Этот стиль также устанавливает значения для трех главных свойств класса Window, которые делают его прозрачным, что дает возможность создать границу и фон окна с помощью элементов управления WPF:
Book_Pro_WPF-2.indb 465
19.05.2008 18:10:39
466
Глава 15
На этом этапе можно уже переходить к использованию специального окна. Например, можно создать окно, устанавливающее стиль и добавляющее кое-какое базовое содержимое, подобное показанному ниже: This is a test. OK
На рис. 15.12 можно видеть результат. Здесь имеется только одна проблема. В настоящий момент у данного окна отсутствует большая часть требуемого для окон поведения. Например, это окно нельзя перетаскивать по рабочему столу, нельзя изменять его размер и пользоваться кнопкой для его закрытия. Для выполнения этих действий необходим код. Добавить необходимый код можно двумя способами: путем расширения данного примера до специального унаследованного от Window класса или путем создания класса отделенного кода для словаря ресурсов. Подход со специальным элементом управления предусматривает более удобную инкапРис. 15.12. Шаблон окна, пригодный суляцию и позволяет расширить общедоступный интерфейс окна (например, добавить полезные медля многократного использования тоды и свойства для применения в приложении). Однако подход с классом отделенного кода является относительно упрощенной альтернативой и позволяет расширить возможности шаблона элемента управления, не изменив при этом используемых приложением базовых классов элементов управления. В данном примере будет продемонстрирован именно этот подход. (Подход со специальным элементом управления будет рассматриваться в главе 24.) Как создавать класс отделенного кода для словаря ресурсов уже рассказывалось ранее в этой главе, в разделе “Обложки, выбираемые пользователем”. Создав файл кода, добавить код обработки событий уже нетрудно. Единственной проблемой является то, что этот код будет выполняться в объекте словаря ресурсов, а не внутри объекта окна. А это означает, что для получения доступа к текущему окну нельзя использовать ключевое слово this. К счастью, существует удобная альтернатива: свойство FrameworkElement. TemplatedParent. Например, чтобы сделать окно перетаскиваемым, необходимо перехватить событие мыши в строке заголовка и инициировать перетаскивание. Ниже показана переделанная версия TextBlock, в которой при выполнении пользователем щелчка кнопкой мыши подключается обработчик событий:
Теперь в класс отделенного кода для словаря ресурсов можно добавить следующий обработчик событий:
Book_Pro_WPF-2.indb 466
19.05.2008 18:10:39
Шаблоны элементов управления
467
private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Window win = (Window) ((FrameworkElement)sender).TemplatedParent; win.DragMove(); }
Чтобы сделать окно допускающим изменение размера, необходимо в его правой и нижней части добавить два невидимых прямоугольника. Эти прямоугольники могут получать события мыши и вызывать обработчики событий, отвечающие за изменения размера окна. Ниже приведен код разметки, необходимый для того, чтобы сконфигурировать элемент управления Grid в шаблоне так, чтобы он поддерживал возможность изменения размера окна.
Далее показаны обработчики событий, отвечающие за изменение размера окна. Булевское поле isResizing следит за переходом в режим изменения размера окна, а поле resizeType — за направлением, в котором изменяется этот размер. private bool isResizing = false; // Используем атрибут Flags для того, чтобы разрешить одновременное // изменение значений размера Width и Height (активизировать которое // можно с помощью правого нижнего угла окна). [Flags()] private enum ResizeType { Width, Height } private ResizeType resizeType; private void window_initiateResizeWE(object sender, MouseEventArgs e) { isResizing = true; resizeType = ResizeType.Width; } private void window_initiateResizeNS(object sender, MouseEventArgs e) { isResizing = true; resizeType = ResizeType.Height; } private void window_endResize(object sender, MouseEventArgs e) { isResizing = false;
Book_Pro_WPF-2.indb 467
19.05.2008 18:10:40
468
Глава 15 // Удостоверяемся в том, что захват освобожден. Rectangle rect = (Rectangle)sender; rect.ReleaseMouseCapture();
} private void window_Resize(object sender, MouseEventArgs e) { Rectangle rect = (Rectangle)sender; Window win = (Window)rect.TemplatedParent; if (isResizing) { rect.CaptureMouse(); if (resizeType == ResizeType.Width) { double width = e.GetPosition(win).X + 5; if (width > 0) win.Width = width; } if (resizeType == ResizeType.Height) { double height = e.GetPosition(win).Y + 5; if (height > 0) win.Height = height; } } }
И, наконец, ниже приведен похожий код, отвечающий за обработку щелчка на кнопке закрытия окна: private void cmdClose_Click(object sender, RoutedEventArgs e) { Window win = (Window) ((FrameworkElement)sender).TemplatedParent; win.Close(); }
На этом данный пример завершается, предоставляя шаблон специального окна со встроенным поведением. Этот шаблон может быть применен к любому обычному окну WPF. Конечно, для придания данному окну внешнего вида, достаточно привлекательного для современного приложения, все равно потребуется приложить немало дополнительных усилий. Однако этот пример демонстрирует последовательность шагов, которые необходимо выполнить, чтобы создать сложный шаблон элемента управления, и показывает, как можно добиться результатов, для достижения которых в предыдущих каркасах пользовательских интерфейсов требовалось разрабатывать специальные элементы управления.
Простые стили Как было показано, создание нового шаблона для обычного элемента управления может оказаться очень обстоятельной задачей. А все дело в том, что все требования шаблона элемента управления не всегда очевидны. Например, шаблон типичного элемента управления ScrollBar требует использования двух объектов RepeatButton и бегунка (Track). Шаблоны других элементов управления, в свою очередь, требуют применения элементов с определенными именами, начинающимися с PART_. В случае специального окна нужно обязательно проверять, чтобы в шаблоне определялся декоративный слой, потому что он будет необходим некоторым элементам управления. Хотя узнать об этих деталях можно путем изучения шаблонов, используемых для элементов управления по умолчанию, эти шаблоны часто являются слишком сложными
Book_Pro_WPF-2.indb 468
19.05.2008 18:10:40
Шаблоны элементов управления
469
и включают детали и привязки, поддерживать которые нет никакой особой необходимости. К счастью, существует и другое место, с которого можно начинать: это демонстрационный проект SimpleStyles. Проект SimpleStyles предоставляет коллекцию простых, отлаженных шаблонов для всех стандартных элементов управления WPF, что делает их удобной “отправной точкой” для любого разработчика специальных элементов управления. В отличие от стандартных шаблонов элементов управления, в этих шаблонах используются стандартные цвета, вся работа выполняется декларативно (без классов Chrome) и отсутствуют необязательные части вроде привязок шаблона для редко применяемых свойств. Предназначением проекта SimpleStyles является предоставление разработчикам практичной отправной точки, которой они могли бы пользоваться для создания своих собственных шаблонов элементов управления с улучшенной графикой. На рис. 15.13 показана примерно половина доступных в этом проекте элементов управления.
Рис. 15.13. Элементы управления WPF с базовыми стилями Примеры применения шаблона SimpleStyles поставляются вместе с Visual Studio. Поэтому найти их можно либо отыскав в справочной системе Visual Studio раздел под названием Styling with ControlTemplates sample (Создание стилей с помощью образца ControlTemplates), либо загрузив демонстрационные образцы кода для этой главы. Совет. Проект SimpleStyles является одним из “скрытых сокровищ” WPF. Доступные в нем шаблоны являются более простыми для понимания и удобными для совершенствования по сравнению со стандартными шаблонами элементов управления. При потребности придать обычному элементу управления специальный внешний вид всегда лучше начинать именно с этого проекта.
Book_Pro_WPF-2.indb 469
19.05.2008 18:10:40
470
Глава 15
Резюме В этой главе были рассмотрены основы создания шаблонов. Однако для создания сложного шаблона все равно еще придется немало повозиться. Зачастую наилучшей отправной точкой является изучение других примеров специальных шаблонов элементов управления, которые в изобилии доступны в Web. Ниже перечислены два неплохих источника.
• В Web доступно множество разработанных вручную затененных кнопок со стеклянными эффектами и эффектами мягкой подсветки. В частности, по адресу
http://blogs.msdn.com/mgrayson/archive/2007/02/16/creating-a-glassbutton-the-complete-tutorial.aspx можно найти целое готовое учебное пособие по созданию привлекательной стеклянной кнопки в Expression Blend.
• По адресу http://msdn.microsoft.com/msdnmag/issues/07/01/Foundations доступна замечательная статья из журнала MSDN Magazine о шаблонах элементов управления с примерами шаблонов, в которых простые элементы рисования используются новыми, оригинальными способами. Например, элемент управления CheckBox заменяется рычагом “вверх-вниз”, бегунок визуализируется с помощью трехмерной вкладки, элемент управления ProgressBar преобразуется в термометр и т.д. Чтобы не вводить эти ссылки вручную, можно воспользоваться соответствующим списком на странице, посвященной этой книге, которая находится по адресу http:// www.prosetech.com.
Book_Pro_WPF-2.indb 470
19.05.2008 18:10:40
ГЛАВА
16
Привязка данных П
ривязка данных — это освященная годами традиция извлечения информации из объекта и отображения ее в пользовательском интерфейсе приложения без написания нудного кода, который выполняет всю эту работу. Часто “толстые” клиенты используют двунаправленную привязку данных, что добавляет возможности “заталкивания” информации из пользовательского интерфейса обратно в некоторый объект — опять же, с минимальным кодированием, либо вообще без оного. Поскольку многие Windows-приложения связаны с данными (и все они в определенное время нуждаются во взаимодействии с данными), привязка данных находится в центре внимания такой технологии пользовательских интерфейсов, как WPF. Разработчики, пришедшие к WPF с опытом работы в Windows Forms, найдут в привязке данных WPF много схожего с тем, к чему они привыкли. Как и в Windows Forms, привязка данных WPF позволяет создавать привязки, которые извлекают информацию практически из любого свойства любого элемента. WPF также включает набор списочных элементов управления, которые могут обрабатывать целые коллекции информации и позволяют осуществлять навигацию по этой информации. Однако произошли и существенные изменения в способах привязки данных, которая происходит “за кулисами”, появилась некоторая впечатляющая новая функциональность, а также возможности тонкой настройки. Многие концепции остались прежними, но код изменился. В этой главе вы изучите способы применения привязки данных WPF. Мы создадим декларативные привязки для извлечения нужной информации из элементов и других объектов. Вы также узнаете, как подключать эту систему к лежащей в основе базе данных, независимо от того, планируете ли вы использовать стандартные объекты данных ADO.NET или строить свои собственные классы данных.
Основы привязки данных В своем простейшем виде привязка данных — это отношение, которое указывает WPF извлечь некоторую информацию из объекта-источника и использовать ее для установки свойства целевого объекта. Целевое свойство всегда является свойством зависимостей и обычно принадлежит элементу WPF. В конце концов, конечная цель привязки данных WPF — отобразить некоторую информацию в пользовательском интерфейсе. Однако исходный объект может быть почти любым, начиная с другого элемента WPF и заканчивая объектом данных ADO.NET (вроде DataTable и DataRow) либо объектом данных вашей собственной разработки. Эту главу мы начнем с разбора привязки данных на примере самого простейшего подхода (привязка типа “элемент к элементу”), а затем рассмотрим, как использовать привязку данных с объектами других типов.
Book_Pro_WPF-2.indb 471
19.05.2008 18:10:40
472
Глава 16
Привязка к свойству элемента Простейший сценарий привязки данных состоит в том, что ваш исходный объект является элементом WPF, а исходное свойство — свойством зависимостей. Это объясняется тем, что свойства зависимостей имеют встроенную поддержку уведомлений об изменениях, как описано в главе 6. В результате, когда вы меняете значение свойства зависимостей исходного объекта, привязанное свойство в целевом объекте обновляется автоматически. Это именно то, что вам нужно, и для этого не требуется строить никакой дополнительной инфраструктуры. На заметку! Хотя и замечательно знать, что привязка данных “элемент к элементу” — простейший подход, большинство разработчиков заинтересовано в том, чтобы узнать, какой подход наиболее распространен в реальном мире. Прежде всего, основная часть работы по привязке данных будет сосредоточена на привязке элементов к объектам данных. Это позволит отображать информацию, извлекаемую из внешнего источника (такого как база данных или файл). Однако привязка “элемент к элементу” также часто бывает полезной. Например, вы можете использовать такую привязку для автоматизации способа взаимодействия элементов так, что когда пользователь модифицирует элемент управления, то другой элемент обновится автоматически. Это существенное преимущество, которое позволит вам сэкономить время на написание рутинного кода (и такая техника была невозможна в приложениях Windows Forms предыдущего поколения). Чтобы понять, как можно привязать один элемент к другому, рассмотрим простое окно, представленное на рис. 16.1. Оно содержит два элемента управления — Slider и TextBlock с единственной строкой текста. Если перетащить бегунок вправо, размер шрифта текста немедленно увеличится. Если сдвинуть его влево, размер шрифта уменьшится.
Рис. 16.1. Элементы управления, связанные привязкой данных Ясно, что было бы совсем нетрудно запрограммировать такое поведение в коде. Для этого вам всего лишь понадобилось бы реагировать на событие ValueChanged и копировать текущее значение бегунка в TextBlock. Однако привязка данных позволяет сделать это еще проще. Совет. Привязка данных обладает еще одним преимуществом — она позволяет создавать простые страницы XAML, которые можно запускать в браузере без компиляции их в приложения. (Как вы знаете из главы 1, если файл XAML имеет соответствующий файл отделенного кода, то он не может быть открыт в браузере.)
Book_Pro_WPF-2.indb 472
19.05.2008 18:10:40
Привязка данных
473
При использовании привязки данных вам не нужно вносить никаких изменений в исходный объект (в данном случае — Slider). Просто сконфигурируйте его, чтобы он принимал правильный диапазон значений, как это обычно делается:
Привязка задается в элементе TextBlock. Вместо установки FontSize с применением литерального значения вы используете выражение привязки, как показано ниже:
Выражения привязки данных используют расширение разметки XAML (отсюда и фигурные скобки). Выражение начинается со слова Binding, поскольку вы создаете экземпляр класса System.Windows.Data.Binding. Хотя вы можете сконфигурировать объект Binding несколькими способами, в данной ситуации нужно установить всего два свойства: ElementName, указывающее элемент-источник, и Path, указывающее привязываемое свойство элемента-источника. Совет. Имя Path используется вместо Property, поскольку Path может указывать на свойство свойства (например, FontFamily.Source) или индексатор, применяемый свойством (например, Content.Children[0]). Вы можете строить путь из множества компонентов, разделенных точками, углубляясь в свойство свойства свойства, и т.д. Если вы хотите сослаться на прикрепленное (attached) свойство (т.е. свойство, определенное в другом классе, но примененное к привязываемому элементу), то в этом случае должны поместить имя свойства в скобки. Например, если вы привязываете элемент, помещенный в Grid, то путь (Grid.Row) извлечет номер строки, в которую вы его поместили. Одним из замечательных возможностей привязки данных является то, что ваш целевой объект обновляется автоматически, независимо от того, как модифицируется источник. В данном примере источник может модифицироваться единственным способом — взаимодействием пользователя с бегунком. Однако рассмотрим слегка измененную версию этого примера, в который добавлено несколько кнопок, каждая из которых применяет заранее установленное значение бегунка. На рис. 16.2 показано новое окно. Когда вы щелкаете на кнопке Set to Large (Установить крупным), запускается следующий код: private void cmd_SetLarge(object sender, RoutedEventArgs e) { sliderFontSize.Value = 30; }
Этот код устанавливает значение бегунка, что, в свою очередь, вызывает изменение размера шрифта текста через привязку данных. Это то же самое, как если бы вы переместили бегунок вручную. Тем не менее, следующий код так не работает: private void cmd_SetLarge(object sender, RoutedEventArgs e) { lblSampleText.FontSize = 30; }
Book_Pro_WPF-2.indb 473
19.05.2008 18:10:41
474
Глава 16
Рис. 16.2. Программная модификация источника привязки данных
Ошибки привязки WPF не возбуждает исключений, чтобы известить вас о проблемах привязки данных. Если вы специфицируете несуществующий элемент или свойство, то не получите никакого указания на такую ошибку — вместо этого данные просто не появятся в целевом свойстве. На первый взгляд может показаться, что это чревато кошмаром при отладке. К счастью, WPF выводит трассировочную информацию, в которой детализирована работа средств привязки. Эта информация появляется в окне Output в Visual Studio при отладке приложения. Например, если вы попытаетесь привязать несуществующее свойство, то увидите примерно следующее сообщение к окне Output: System.Windows.Data Error: 35 : BindingExpression path error: 'Tex' property not found on 'object' ''TextBox' (Name='txtFontSize')'. BindingExpression:Path=Tex; DataItem='TextBox' (Name='txtFontSize'); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
WPF также игнорирует любые исключения, которые генерируются при попытке прочесть свойство-источник, и молча “проглатывает” исключения, которые происходят, когда данные не могут быть приведены к типу данных целевого свойства. Однако есть еще один способ справиться с этими проблемами — вы можете указать WPF, чтобы при возникновении ошибок изменялся внешний вид элемента-источника, тем самым сигнализируя о проблеме. Например, это позволит вам пометить некорректный ввод пиктограммой с восклицательным знаком или подчеркнуть его красным. Ниже, в разделе “Проверка достоверности” настоящей главы мы продемонстрируем эту технику. Последний код устанавливает шрифт текста напрямую. В результате позиция бегунка не будет обновлена, чтобы соответствовать новому значению шрифта. Хуже того, это разрушит вашу привязку размера шрифта, заменив ее литеральным значением. Если после этого вы передвинете бегунок, то текстовый блок вообще больше не изменится. Интересно то, что все-таки существует способ передачи значения в обоих направлениях: от источника к цели и от цели к источнику. Трюк заключается в установке свойства Mode объекта Binding. Ниже приведен пример объявления двунаправленной привязки данных, которая позволит применять изменения как к источнику, так и к цели и получать автоматическое эквивалентное обновление противоположной стороны:
Book_Pro_WPF-2.indb 474
19.05.2008 18:10:41
Привязка данных
475
В данном примере нет причин использовать двунаправленную привязку (которая требует больше накладных расходов), поскольку вы можете решить ту же проблему правильным кодом. Тем не менее, мы рассмотрим вариант данного примера, включающий текстовое поле, в котором пользователь может точно указывать размер шрифта. Текстовое поле нуждается в двунаправленной привязке, чтобы оно и применяло пользовательские изменения, и отображало значение размера шрифта, когда пользователь изменит его с другого конца. Этот пример вы увидите в следующем разделе.
Создание привязки в коде При построении окна обычно лучше объявлять ваше выражение привязки в коде разметки XAML, используя расширение Binding. Однако привязку можно также создать и в программном коде. Вот как можно создать привязку TextBlock, показанную в предыдущем примере: Binding binding = new Binding(); binding.Source = sliderFontSize; binding.Path = new PropertyPath("Value"); binding.Mode = BindingMode.TwoWay; lblSampleText.SetBinding(TextBlock.TextProperty, binding);
Вы можете также удалить привязку в коде программы, используя два статических метода класса BindingOperations. Метод ClearBinding() принимает ссылку на свойство зависимостей, которое имеет привязку, подлежащую удалению, в то время как ClearAllBindings() удаляет все привязки данных элемента: BindingOperations.ClearAllBindings(lblSampleText);
И ClearBinding(), и ClearAllBindings() используют метод ClearValue(), который каждый элемент наследует от базового класса DependencyObject . Метод ClearValue() просто удаляет локальное значение свойства (которое в данном случае является выражением привязки). Привязка на основе разметки намного более распространена, чем программная привязка, потому что она проще и требует меньше работы. В этой главе все примеры используют разметку для создания привязок. Однако в некоторых специализированных сценариях (описанных ниже) вам понадобится использовать программный код для создания привязки.
• Создание динамической привязки. Если вы хотите “подогнать” привязку на основе другой информации времени выполнения или создать другую привязку в зависимости от обстоятельств, часто имеет смысл создать ее в коде. (Альтернативно вы можете определить каждую привязку, которая может вам понадобиться, в коллекции Resources вашего окна и просто добавить код, вызывающий SetBinding() с соответствующим объектом привязки.)
• Удаление привязки. Если вы хотите удалить привязку, чтобы устанавливать значение свойства обычным способом, вам понадобится помощь методов ClearBinding() и ClearAllBindings(). Недостаточно просто применить новое значение свойства — если вы используете двунаправленную привязку, установленное значение распространится на привязанный объект, и оба свойства останутся синхронизированными. На заметку! Вы можете удалить любую привязку с помощью методов ClearBinding() и ClearAllBindings(). При этом не имеет значения, была привязка создана программно или в разметке XAML.
Book_Pro_WPF-2.indb 475
19.05.2008 18:10:41
476
Глава 16
• Создание пользовательских элементов управления. Чтобы облегчить другим людям задачу модификации внешнего вида разработанного вами пользовательского элемента управления, вам нужно переместить некоторые детали (такие как обработчики событий и выражения привязки данных) в ваш программный код из кода разметки. В главе 24 приведен пример пользовательского элемента управления для указания цвета, использующего программный код для создания привязок.
Множественные привязки Хотя предыдущий пример включает только одну привязку, вы не должны на этом останавливаться. При желании вы можете установить TextBlock, чтобы он дублировал содержимое текстового поля, текущий цвет переднего плана и фона из отдельного списка цветов, и так далее. Ниже показан пример.
На рис. 16.3 демонстрируется трижды привязанный TextBlock.
Рис. 16.3. TextBlock, привязанный к трем элементам Привязки данных можно связывать в цепочку. Например, вы можете создать выражение привязки для свойства TextBox.Text, которое связано со свойством TextBlock. FontSize, содержащим выражение привязки к свойству Slider.Value. В этом случае, когда пользователь перетащит бегунок в новое положение, значение из Slider будет передано в TextBlock, а затем — из TextBlock в TextBox. Хотя все это работает гладко, более ясный подход заключается в привязке ваших элементов как можно ближе к данным, используемым ими. В описанном примере вам стоит рассмотреть возможность привязки и TextBlock, и TextBox непосредственно к свойству Slider.Value. Все становится еще интереснее, если вы хотите, чтобы целевое свойство зависело от более чем одного источника, например, если требуется иметь две равнозначных привязки для установки свойства. На первый взгляд, это кажется невозможным. Однако это ограничение можно обойти несколькими путями. Простейший подход — изменить режим привязки данных. Как вы узнали из предыдущего раздела, свойство Mode позволяет изменять способ работы привязки — так, чтобы значения не просто выталкивались из источника к цели, но также и наоборот — от цели к источнику. Используя эту технику, вы можете создать множественные выраже-
Book_Pro_WPF-2.indb 476
19.05.2008 18:10:41
Привязка данных
477
ния привязки, устанавливающие одно и то же свойство. При этом действительным является значение, установленное последним свойством. Чтобы понять, как это работает, рассмотрим вариант примера с бегунком, добавив в него текстовое поле, в котором можно указывать размер шрифта, какой вы хотите. В этом примере (показанном на рис. 16.4) вы можете установить свойство TextBlock. FontSize двумя способами — перетаскивая бегунок или вводя размер шрифта в текстовом поле. Все элементы управления синхронизированы, так что если вы вводите новую цифру в текстовое поле, то размер шрифта текста-примера изменяется и бегунок перемещается в соответствующую позицию.
Рис. 16.4. Привязка двух свойств к размеру шрифта Как вы знаете, к свойству TextBlock.FontSize можно применить только одну привязку данных. Имеет смысл оставить его привязанным непосредственно к бегунку:
Хотя вы не можете добавить другую привязку к свойству FontSize, вы можете привязать новый элемент управления — TextBox — к свойству TextBlock.FontSize. Вот необходимый для этого код разметки:
Теперь всякий раз при изменении TextBlock.FontSize текущее значение будет вставляться в текстовое поле. Более того, вы можете редактировать значение в текстовом поле, указывая нужный размер. Отметим, что для того, чтобы этот пример работал, свойство TextBox.Text должно использовать двунаправленную привязку, чтобы данные передавались в обоих направлениях. В противном случае текстовое поле сможет отображать значение TextBlock.FontSize, но не сможет изменять его. С этим примером связано несколько особенностей.
• Поскольку свойство Slider.Value имеет тип double , вы получите от него дробное значение размера шрифта при перетаскивании бегунка. Можно ограничить бегунок только целочисленными значениями, установив его свойство TickFrequency в 1 (или некоторый другой числовой интервал) и установив свойство IsSnapToTickEnabled в true.
• Текстовое поле допускает ввод букв и других нецифровых символов. Если вы введете любой из них, то значение текстового поля нельзя будет интерпретировать как число. В результате привязка данных молча потерпит неудачу, и размер шрифта будет установлен в 0. Другой подход заключается в обработке нажатий клавиш в
Book_Pro_WPF-2.indb 477
19.05.2008 18:10:41
478
Глава 16 текстовом поле, чтобы предотвратить вообще неправильный ввод, либо использовать проверку достоверности привязки данных, о которой мы поговорим ниже.
• Изменения, проведенные в текстовом поле, не применяются до тех пор, пока оно не утратит фокус (например, когда вы нажмете клавишу для перехода к другому элементу управления). Если вам не подходит такое поведение, можете инициировать непрерывное обновление, используя для этого свойство UpdateSourceTrigger объекта Binding, как будет описано ниже в разделе “Обновления привязки”. Интересно, что показанное здесь решение — не единственный способ подключения текстового поля. Также можно сконфигурировать текстовое поле, чтобы оно изменяло свойство Slider.Value вместо TextBlock.FontSize:
Теперь изменение текста инициирует изменение положения бегунка, которое затем применит новый размер шрифта к тексту. Такой подход работает, только если вы используете двунаправленную привязку. И последнее: вы можете поменять ролями бегунок и текстовое поле — так, чтобы бегунок был привязан к текстовому полю. Чтобы сделать это, нужно создать несвязанный TextBox и присвоить ему имя:
Затем вы можете привязать свойство Slider.Property, как показано ниже:
Теперь бегунок находится под контролем. Когда окно отображается впервые, оно извлекает свойство TextBox.Text и использует его для установки его свойства Value. Когда пользователь перетаскивает бегунок в новую позицию, применяется привязка для обновления текстового поля. Или же пользователь может обновить значение бегунка (и размер шрифта текста примера), введя его в текстовом поле. На заметку! Если вы привязываете свойство Slider.Value, то текстовое поле ведет себя слегка иначе, чем в предыдущих двух примерах. Любые изменения, которые вы вносите в текстовое поле, применяются немедленно, вместо того, чтобы ожидать, пока текстовое поле потеряет фокус. Подробнее об управлении обновлениями вы узнаете в разделе “Обновления привязки”. Как демонстрирует этот пример, двунаправленные привязки обеспечивают замечательную гибкость. Вы можете использовать их для применения изменений от источника к цели и от цели к источнику. Вы можете также применять их в комбинации, создавая необыкновенно сложные окна, лишенные кода. Обычно решение о том, куда поместить выражение привязки, диктуется логикой вашей модели кодирования. В предыдущем примере, возможно, имело бы больше смысла поместить привязку в свойство TextBox.Text вместо Slider.Value, поскольку текстовое поле — это необязательное дополнение к завершенному примеру, а не центральный ингредиент, на который полагается бегунок. Также имело бы больше смысла привязать текстовое поле непосредственно к свойству TextBlock.FontSize, а не к Slider.Value. (Концептуально вы заинтересованы в сообщении текущего размера шрифта, а бегу-
Book_Pro_WPF-2.indb 478
19.05.2008 18:10:41
Привязка данных
479
нок — лишь один из способов его установки. Даже несмотря на совпадение положения бегунка с размером шрифта, нет необходимости в дополнительных деталях, если вы пытаетесь написать как можно более ясный код разметки.) Конечно, все эти решения субъективны и зависят от стиля кодирования. Наиболее важный урок из всего этого — что все три подхода дают в конечном итоге одно и то же поведение. В следующих разделах мы рассмотрим еще две детали, на которые полагается этот пример. Для начала мы обоснуем решения относительно направления привязки. Затем покажем, как вы можете явно указать WPF, когда она должна обновлять исходное свойство в двунаправленной привязке.
Направление привязки До сих пор вы познакомились с однонаправленной и двунаправленной привязками. На самом деле WPF позволяет при установке свойства Binding.Mode применять одно из пяти значений перечисления System.Windows.Data.BindingMode. В табл. 16.1 приведен полный список.
Таблица 16.1. Значения из перечисления BindingMode Имя
Описание
OneWay
Целевое свойство обновляется при изменении свойства-источника.
TwoWay
Целевое свойство обновляется при изменении свойства-источника, и свойство-источник обновляется при изменении целевого свойства.
OneTime
Целевое свойство устанавливается изначально на основе значения свойства-источника. Однако с этого момента изменения игнорируются (если только привязка не установлена на совершенно другой объект, или же вы вызываете BindingExpression.UpdateTarget(), как будет описано ниже в этой главе). Обычно вы используете этот метод для сокращения накладных расходов, если знаете, что исходное свойство не будет изменяться.
OneWaySource
Подобно OneWay, только наоборот. Исходное свойство обновляется при изменении целевого свойства (что может показаться несколько “задом наперед”), но целевое свойство никогда не обновляется.
Default
Тип привязки зависит от целевого свойства. Оно будет либо TwoWay (для устанавливаемых пользователем свойств, таких как TextBox.Text), либо OneWay (для всех прочих). Все привязки используют этот подход, если только вы не укажете другой.
На рис. 16.5 продемонстрирована разница. Вы уже видели OneWay и TwoWay. Значение OneTime достаточно очевидно. Два других выбора требуют некоторого исследования.
Целевой объект
Объект\источник
OneWay Свойство
OneWayToSource
Свойство зависимостей (установлено для привязки)
TwoWay
Рис. 16.5. Разные способы привязки двух свойств
Book_Pro_WPF-2.indb 479
19.05.2008 18:10:41
480
Глава 16
OneWayToSource Вы можете удивиться, зачем нужны два свойства — и OneWay, и OneWayToSource? В конце концов, оба значения создают однонаправленную привязку, которая работает одинаково. Единственное отличие состоит в том, где помещается выражение привязки. По сути, OneWayToSource позволяет поменять местами источник и цель, поместив выражение в тот объект, который обычно будет служить источником привязки. Наиболее распространенная причина для применения такого трюка — установка свойства, не являющегося свойством зависимостей. Как вы видели в начале этой главы, выражения привязки могут применяться только для установки свойств зависимостей. Но, используя OneWayToSource, вы можете преодолеть это ограничение, если свойство, применяющее значение, само является свойством зависимостей. Эта техника не слишком часто применяется при выполнении привязки “элемент к элементу”, поскольку почти все свойства элементов являются свойствами зависимостей. Единственным исключением является установка внутристрочных (inline) элементов, которые вы можете использовать для построения документов (как будет показано в главе 19). Например, рассмотрим следующий код разметки, создающий FlowDocument — идеальный выбор для отображения симпатично форматированных регионов статического содержимого: This is a paragraph one. This is paragraph two.
FlowDocument помещается внутри прокручиваемого содержимого (который является лишь одним из нескольких возможный контейнеров, которые вы можете использовать), и предоставляет два маленьких параграфа с небольшим объемом текста. Теперь посмотрим, что случится, если вы хотите привязать некоторый текст в параграфе к другому свойству. Первый шаг — поместить текст, который хотите изменить, в оболочку объекта Run, представляющего любую небольшую единицу текста внутри FlowDocument. Следующий шаг — можно попытаться установить текст Run, используя выражение привязки: This is a paragraph one.
В этом примере попытки Run пытается извлечь свой текст из текстового поля по имени txtParagraph. К сожалению, этот код не будет работать, потому что Run.Text не является свойством зависимостей, так что оно не знает, что делать с выражением привязки. Решение состоит в удалении выражения привязки из Run и помещении его вместо этого в текстовое поле: Content for second paragraph:
Book_Pro_WPF-2.indb 480
19.05.2008 18:10:42
Привязка данных
481
Теперь текст автоматически копируется из текстового поля в Run. Конечно, вы можете также использовать двунаправленную привязку в текстовом поле, но это повлечет за собой некоторый объем дополнительных накладных расходов. Это может оказаться лучшим выбором, если есть некоторый начальный текст в Run, и вы хотите, чтобы он сначала появлялся в привязанном текстовом поле.
Default Изначально кажется логичным предположить, что все привязки являются однонаправленными, если только вы явно не укажете другое. (В конце концов, именно так работает простой пример с бегунком.) Однако на самом деле это не так. Чтобы продемонстрировать этот факт, вернемся к примеру с привязанным текстовым полем, который позволяет редактировать текущий размер шрифта. Если вы удалите настройку Mode=TwoWay, этот пример будет работать как раньше. Это объясняется тем, что WPF использует разные установки по умолчанию для Mode, в зависимости от привязываемого свойства. (Технически для этого есть маленький фрагмент метаданных в каждом свойстве зависимостей — флаг FrameworkPropertyMetadata.BindsTwoWayByDefault, который указывает, должно ли свойство использовать однонаправленную или двунаправленную привязку.) Часто установки по умолчанию — именно то, что вам нужно. Однако вы можете представить себе пример с текстовым полем, доступным только для чтения, который пользователь не может изменить. В этом случае вы можете слегка сократить накладные расходы, установив режим однонаправленной привязки. В качестве общего эмпирического правила — никогда не помещает явно установить режим. Даже в случае текстового поля стоит подчеркнуть, что вам нужна двунаправленная привязка, включив свойство Mode.
Обновления привязки В примере, приведенном на рис. 16.4 (который привязывает TextBox.Text к TextBlock.FontSize), есть еще одна причуда. При изменении размера шрифта отображаемого текста вводом его в текстовое поле ничего не происходит. И не происходит до тех пор, пока вы не перейдете в другой элемент управления, применяющий изменения. Это поведение отличается от того поведения, которое вы наблюдаете у элемента управления — бегунка. Там новый размер шрифта немедленно применяется при перемещении этого бегунка. Нет необходимости переносить фокус. Чтобы понять, в чем разница, нужно присмотреться к выражениям привязки, используемым этими двумя элементами управления. Когда вы применяете привязку OneWay или TwoWay, измененное значение распространяется от источника к цели немедленно. В случае бегунка присутствует выражение однонаправленной привязки в TextBlock. Таким образом, изменения свойства Slider.Value немедленно применяются к свойству TextBlock.FontSize. То же поведение имеет место в примере с текстовым полем — изменения в источнике (TextBlock.FontSize) немедленно затрагивают цель (TextBox.Text). Однако изменения в обратном направлении — от цели к источнику — не обязательно происходят немедленно. Вместо этого их поведением управляет свойство Binding. UpdateSourceTrigger (которое принимает одно из значений, перечисленных в табл. 16.2). Когда текст берется из текстового поля и используется для обновления свойства TextBlock.FontSize, вы наблюдаете пример обновления “цель-источник”, которое использует поведение UpdateSourceTrigger.LostFocus.
Book_Pro_WPF-2.indb 481
19.05.2008 18:10:42
482
Глава 16
Таблица 16.2. Значения из перечисления UpdateSourceTrigger Имя
Описание
PropertyChanged
Источник обновляется немедленно после изменения целевого свойства.
LostFocus
Источник обновляется, когда изменяется целевое свойство, а целевой элемент теряет фокус.
Explicit
Источник не обновляется, пока не будет вызван метод BindingExpression.UpdateSource().
Default
Поведение обновления определяется метаданными целевого свойства (технически — его свойства FrameworkPropertyMetadata. DefaultUpdateSourceTrigger). Для большинства свойств поведением по умолчанию является PropertyChanged, хотя свойство TextBox.Text имеет поведение по умолчанию LostFocus.
Напомним, что значения в табл. 16.2 не оказывают влияния на то, как обновляется цель. Они просто управляют тем, как обновляется источник в привязке TwoWay или OneWayToSource. Зная это, вы можете усовершенствовать пример с текстовым полем, чтобы изменения применялись к размеру шрифта, когда пользователь осуществляет ввод в текстовом поле. Вот как это сделать:
Совет. Поведением по умолчанию свойства TextBox.Text является LostFocus, просто потому что текст в текстовом поле будет непрерывно изменяться во время ввода пользователя, требуя множественных обновлений. В зависимости от того, как элемент управления — источник обновляет себя, режим обновления PropertyChanged может создать впечатление “замедленного” поведения приложения. Вдобавок это может вызвать обновление объекта-источника до завершения редактирования, что может вызвать проблемы при проверке достоверности ввода. Поведение UpdateSourceTrigger.Explicit часто представляет собой приемлемый компромисс, хотя требует написания определенной порции кода. Например, в примере с текстовым полем вы можете добавить кнопку Apply (Применить), щелчок на которой обновит размер шрифта. Затем вы можете воспользоваться методом BindingExpression.UpdateSource() для инициации немедленного обновления. Разумеется, это порождает два замечательных вопроса, а именно: что собой представляет объект BindingExpression и как его получить? BindingExpression — это просто небольшой пакет, который содержит в себе две вещи: уже знакомый вам объект Binding (представлен свойством BindingExpression. ParentBinding) и объект, исходящий от источника (BindingExpression.DataItem). Вдобавок объект BindingExpression предоставляет два метода для инициирования немедленного обновления одной части привязки: UpdateSource() и UpdateTarget(). Для получения объекта BindingExpression вы используете метод GetBinding Expression(), который наследует каждый элемент от класса FrameworkElement, и передаете целевое свойство данной привязки. Ниже приведен пример, изменяющий размер шрифта в TextBlock на основе текущего содержимого текстового поля: // Получить привязку, примененную к текстовому полю. BindingExpression binding = txtFontSize.GetBindingExpression(TextBox.TextProperty); // Обновить связанный источник (TextBlock). binding.UpdateSource();
Book_Pro_WPF-2.indb 482
19.05.2008 18:10:42
Привязка данных
483
Привязка объектов, не являющихся элементами До сих пор мы говорили о добавлении выражений привязки, связывающей два элемента. Но в управляемых данными приложениях намного чаще приходится создавать выражения привязки, которые извлекают данные из невизуального объекта. Единственное требование состоит в том, чтобы информация, которую вы хотите отобразить, хранилась в общедоступных свойствах. Инфраструктура привязки данных WPF не может обращаться к приватной информации объектов или непосредственно к общедоступным полям. Привязываясь к объекту, не являющемуся элементом, вы должны отказаться от свойства Binding.ElementName и использовать вместо него одно из описанных ниже свойств.
• Source. Это — ссылка, указывающая на объект-источник. Другими словами — на объект, поставляющий данные.
• RelativeSource. Указывает на объект-источник, используя объект RelativeSource, позволяющий базировать вашу ссылку на текущем элементе. Это специализированный инструмент, удобный для написания шаблонов элементов управления и шаблонов данных.
• DataContext. Если вы не специфицируете источник через свойство Source или RelativeSource, то WPF выполняет поиск по дереву элементов, начиная с текущего элемента. При этом проверяется свойство DataContext каждого элемента и используется первый элемент, у которого оно не равно null. Свойство DataContext чрезвычайно удобно, если вам нужно привязать несколько свойств одного и того же объекта к разным элементам, поскольку вы можете установить свойство DataContext у более высокоуровневого объекта-контейнера вместо целевого элемента. В следующих разделах мы поговорим подробнее об этих трех свойствах.
Source Свойство Source достаточно просто. Единственный нюанс состоит в том, что ваш объект данных должен быть доступным, чтобы можно было привязать его. Как вы увидите, для получения объекта данных можно использовать несколько подходов. Вы можете извлечь его из ресурса, сгенерировать программно или же получить его с помощью поставщика данных. Простейший вариант — установить Source на некоторый статический объект, который постоянно доступен. Например, вы можете создать статический объект в своем коде и использовать его. Или же вы можете применить ингредиент библиотеки классов .NET, как показано ниже:
Выражение привязки получает объект FontFamily, представленный статическим свойством SystemFonts.IconFontFamily. (Обратите внимание, что вам понадобится помощь статического расширения разметки, чтобы установить свойство Binding.Source.) Затем оно устанавливает свойство Binding.Path в свойство FontFamily.Source, что даст имя семейства шрифтов. Результатом является единственная строка текста. В Windows Vista появляется имя шрифта Segoe UI. Другой вариант — привязать объект, который вы заранее создали в виде ресурса.
Book_Pro_WPF-2.indb 483
19.05.2008 18:10:42
484
Глава 16
Например, следующая разметка создает объект FontFamily , указывающий на шрифт Calibri: Calibri
А вот TextBlock, привязывающийся к этому ресурсу:
Теперь вы увидите текст, представленный шрифтом Calibri.
RelativeSource Свойство RelativeSource позволяет указать на объект-источник на основе его отношения к целевому объекту. Например, вы можете использовать свойство RelativeSource для привязки элемента к самому себе или к родительскому элементу, находящемуся на неизвестное число шагов вверх по дереву элементов. Для установки свойства Binding.RelativeSource вы используете объект RelativeSource . Это делает синтаксис несколько более изощренным, потому что вам нужно создавать объект Binding и создавать внутри него вложенный объект RelativeSource. Один из вариантов предусматривает применение синтаксиса установки свойства вместо расширения разметки Binding. Например, следующий код создает объект Binding для TextBlock. Объект Binding использует RelativeSource, который ищет родительское окно и отображает его заголовок.
Объект RelativeSource использует режим FindAncestor, который заставляет его искать вверх по дереву элементов, пока не будет найден тип элемента, определенный свойством AncestorType. Более распространенный способ записи этой привязки заключается в комбинации ее в одну строку с использованием расширений разметки Binding и RelativeSource, как показано ниже:
Режим FindAncestor — лишь один из четырех возможных при создании объекта RelativeSource. Все четыре режима перечислены в табл. 16.3. На первый взгляд свойство RelativeSource выглядит как излишнее усложнение разметки. В конце концов, почему бы ни привязаться непосредственно к нужному источнику, используя свойство Source или ElementName? Однако это не всегда возможно — обычно потому, что разметка объекта-источника и целевого объекта — это разные фрагменты разметки. Такое случается, когда вы создаете шаблоны элементов управления и шаблоны данных. Например, если вы строите шаблон данных, который изменяет способ представления элементов в списке, вам может понадобиться доступ к объекту
Book_Pro_WPF-2.indb 484
19.05.2008 18:10:42
Привязка данных
485
высшего уровня ListBox, чтобы прочитать свойство. Вы увидите несколько примеров использования привязки RelativeSource в главах 17 и 18.
Таблица 16.3. Значения перечисления RelativeSourceMode Имя
Описание
Self
Выражение привязывает к другому свойству того же элемента. (Пример этой техники вы видели в главе 10, где она использовалась для отображения текста, ассоциированного с командой в элементе управления, инициирующего команду.)
FindAncestor
Выражение привязывает к родительскому элементу. WPF будет производить поиск вверх по дереву элементов, пока не найдет нужного родителя. Чтобы специфицировать родителя, вы должны также установить свойство AncestorType для указания типа родительского элемента, который нужно найти. Необязательно вы можете использовать свойство AncestorLevel, чтобы пропустить определенное число вхождений указанного элемента. Например, если вы хотите привязать третий элемент типа ListBoxItem при проходе вверх по дереву, то должны установить AncestorType={x:Type ListBoxItem} и AncestorLevel=3, таким образом, пропуская первые два ListBoxItem. По умолчанию AncestorLevel равно 1, и поиск прекращается на первом подходящем элементе.
PreviousData
Выражение привязывает к предыдущему элементу данных в привязанном к данным списке. Вы должны использовать это в элементе списка.
TemplatedParent
Выражение привязывает к элементу, к которому применен шаблон. Этот режим работает, только если ваша привязка расположена внутри шаблона элемента управления или шаблона данных.
DataContext В некоторых случаях у вас будет несколько элементов, привязанных к одному объекту. Например, рассмотрим следующую группу элементов TextBlock, каждый из которых использует похожее выражение привязки для извлечения различных деталей о шрифте пиктограммы по умолчанию, включая промежуток между строками, стиль и вес первой его гарнитуры (и то, и другое будет просто Regular). Вы можете использовать свойство Source для каждого такого элемента, но это приведет к довольно длинной разметке:
В этой ситуации яснее и более гибко будет определить источник привязки, используя свойство FrameworkElement.DataContext. В данном примере имеет смысл установить свойство DataContext объекта StackPanel, содержащего элементы TextBlock. (Вы также должны установить свойство DataContext на еще более высоком уровне — например, на уровне всего окна. Однако имеет смысл определить его насколько возможно уже, дабы прояснить свои намерения.)
Book_Pro_WPF-2.indb 485
19.05.2008 18:10:42
486
Глава 16
Вы можете установить свойство DataContext элемента таким же образом, как вы устанавливаете свойство Binding.Source. Другими словами, вы можете просто применить свой объект встроенным образом, извлекая его из статического свойства или из ресурса, как показано ниже:
Теперь вы можете упростить выражения привязки, исключив информацию об источнике:
Когда информация об источнике пропущена в выражении привязки, WPF проверяет свойство DataContext этого элемента. Если оно равно null, то WPF выполняет поиск по дереву элементов, пытаясь найти первый контекст данных, который не равен null. (Изначально свойство DataContext всех элементов равно null.) Если контекст данных найден, он используется для привязки. Если нет, то выражение привязки не применяет никакого значения к целевому свойству. На заметку! Если вы создаете привязку, явно специфицирующую источник через свойство Source, то ваш элемент использует этот источник вместо любого контекста данных, который может быть доступен. Этот пример демонстрирует, как можно создать базовую привязку к объекту, не являющемуся элементом. Однако чтобы использовать эту технику в реальном приложении, вам понадобится немного более высокая квалификация. В следующем разделе вы узнаете, как отображать информацию, полученную из базы данных, базируясь на этой технике привязки данных.
Привязка пользовательских объектов к базе данных Когда разработчики слышат термин привязка данных, то они часто думают об одном специфическом ее приложении — получении информации из базы данных и отображении на экране с минимальным объемом кода либо вообще без него. Как вы уже видели, привязка данных в WPF — намного более широкое понятие. Даже если ваше приложение никогда не вступает в контакт с базой данных, все же сохраняется вероятность применения привязки данных для автоматизации взаимодействия элементов между собой или трансляции объектной модели в наглядное представление. Однако вы должны знать много подробностей о привязке объектов, для чего мы рассмотрим традиционный пример, запрашивающий и обновляющий таблицу базы данных. Прежде чем обратиться к этому, вам следует узнать о специальном компоненте доступа к данным и объекте данных, используемом в этом примере.
Построение компонента доступа к данным В профессиональных приложениях код работы с базой данных встроен в класс отделенного кода окна, но инкапсулирован в выделенном классе. Для еще лучшего разбиения на компоненты эти классы доступа к данным могут быть вообще исключены из вашего приложения и скомпилированы в отдельный компонент DLL. Это справедливо, в частности, при написании кода доступа к базе данных (поскольку этот код имеет тенденцию быть чрезвычайно чувствительным к производительности), да и вообще это — признак хорошего дизайна, независимо от местонахождения данных.
Book_Pro_WPF-2.indb 486
19.05.2008 18:10:43
Привязка данных
487
Проектирование компонентов доступа к данным Независимо от того, как вы планируете использовать привязку данных (или даже вообще не планируете), ваш код доступа к данным всегда должен находиться в отдельных классах. Такой подход — единственный путь обеспечения возможности эффективного сопровождения, оптимизации, диагностики проблем и (необязательно) повторного использования вашего кода работы с данными. При создании класса доступа к данным вы должны следовать нескольким базовым правилам, которые описаны ниже. • Открывать и закрывать соединения быстро. Открывайте соединение с базой данных при каждом вызове метода и закрывайте перед завершением метода. Таким образом, соединения не останутся открытыми по неосторожности. Один из способов гарантировать закрытие соединения в надлежащее время — использовать блок using. • Реализовать обработку ошибок. Используйте обработку ошибок, чтобы гарантировать закрытие соединений даже в случае возникновения исключений. • Следовать практике не поддерживающего состояние дизайна. Передавайте всю необходимую для метода информацию в его параметрах и возвращайте все извлеченные данные через значение возврата. Это позволит избежать усложнения во многих сценариях (например, если вы хотите создать многопоточное приложение или расположить ваш компонент базы данных на сервере). • Хранить строку подключения в одном месте. В идеале таким местом должен быть конфигурационный файл вашего приложения. Компонент базы данных, показанный в следующем примере, извлекает табличную информацию о продуктах из базы данных Store — базы данных примеров, описывающей фиктивный магазин IBuySpy, которая поставляется вместе с рядом программных продуктов Microsoft. Сценарий установки этой базы данных входит в состав загружаемых примеров для этой главы. На рис. 16.6 показаны две таблицы из базы данных Store и их схемы.
Рис. 16.6. Часть базы данных Store Класс доступа к данным весьма прост — он предоставляет только один метод, позволяющий извлечь одну запись о продукте. Ниже представлен базовый эскиз. public class StoreDB { // Получить строку подключения из текущего конфигурационного файла. private string connectionString = Properties.Settings.Default.Store; public Product GetProduct(int ID) { ... } }
Book_Pro_WPF-2.indb 487
19.05.2008 18:10:43
488
Глава 16
Запрос выполняется хранимой процедурой по имени GetProduct. Строка соединения не кодируется жестко — вместо этого она извлекается из конфигурационного файла .config приложения. (Чтобы просмотреть или установить настройки приложения, выполните двойной щелчок на узле Properties (Свойства) в Solution Explorer, затем перейдите на вкладку Settings (Настройка).) Когда другие окна нуждаются в данных, они вызывают метод StoreDB.GetProduct() для извлечения объекта Product. Объект Product — это специальный объект, имеющий единственное назначение — предоставлять информацию из единственной строки таблицы Products. Мы рассмотрим его в следующем разделе. Есть несколько способов сделать экземпляр StoreDB доступным окну вашего приложения.
• Окно может создавать экземпляр StoreDB всякий раз, когда ему понадобится доступ к базе данных.
• Вы можете изменить методы класса StoreDB, сделав их статическими. • Вы можете создать единственный экземпляр StoreDB и сделать его доступным через статическое свойство другого класса (следуя шаблону “фабрики”). Первые два варианта вполне уместны, но оба они ограничивают гибкость. Первый вариант исключает кэширование объектов данных для использования в нескольких окнах. Даже если вы не хотите применять кэширование, все же стоит так проектировать приложение, чтобы можно было легко реализовать кэширование в будущем. Аналогично, второй подход предполагает, что у вас не будет никаких специфичных для экземпляра состояний, которые можно было бы удерживать в классе StoreDB. Хотя это хороший принцип проектирования, все же вы можете запоминать некоторые детали (такие как строка подключения). Если вы преобразуете класс StoreDB для использования статических методов, становится сложнее обращаться к разным экземплярам базы данных Store в разных хранилищах данных. В конечном итоге третий вариант оказывается наиболее гибким. Он предотвращает дизайн “коммутатора”, заставляя все окна работать через единственное свойство. Ниже приведен пример, который открывает доступ к экземпляру StoreDB всему классу Application: public partial class App : System.Windows.Application { private static StoreDB storeDB = new StoreDB(); public static StoreDB StoreDB { get { return storeDB; } } }
В настоящей книге нас в первую очередь интересует, каким образом объекты данных могут быть привязаны к элементам WPF. Действительный процесс, имеющий дело с созданием и наполнением этих объектов данных (наряду с другими деталями реализации, такими как то, кэширует ли StoreDB данные между несколькими вызовами метода, использует ли хранимые процедуры вместо встроенных запросов, извлекает ли данные из локального XML-файла при отсутствии связи с базой, и т.д.), не является предметом нашего внимания. Однако просто чтобы получить представление о том, что происходит, ниже показан полный код. public class StoreDB { private string connectionString = Properties.Settings.Default.StoreDatabase;
Book_Pro_WPF-2.indb 488
19.05.2008 18:10:43
489
Привязка данных public Product GetProduct(int ID) { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetProductByID", con); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@ProductID", ID); try { con.Open(); SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow); if (reader.Read()) { // Создать объект Product, помещающий // в оболочку текущую запись. Product product = new Product((string)reader["ModelNumber"], (string)reader["ModelName"], (decimal)reader["UnitCost"], (string)reader["Description"], (string)reader["ProductImage"]); return(product); } else { return null; } } finally { con.Close(); } } }
На заметку! В таком виде метод GetProduct() не включает никакого кода обработки исключений, так что все они “всплывают” в вызывающем коде. Это разумное дизайнерское решение, но вы можете решить перехватывать исключения в GetProduct(), выполнять необходимую очистку или протоколирование и затем повторно возбуждать исключение, чтобы известить вызывающий код о возникновении проблемы. Такой шаблон дизайна называется caller inform (информирование вызывающего).
Построение объекта данных Объект данных — это пакет информации, который вы собираетесь отобразить в своем пользовательском интерфейсе. Любой класс работает только при условии наличия у него общедоступных свойств (поля и приватные свойства не поддерживаются). Вдобавок, если вы хотите применить этот объект для внесения изменений (через двунаправленную привязку), то такие свойства не могут быть доступными только для чтения. Ниже приведен код объекта Product, используемый StoreDB. public class Product { private string modelNumber; public string ModelNumber { get { return modelNumber; } set { modelNumber = value; } }
Book_Pro_WPF-2.indb 489
19.05.2008 18:10:43
490
Глава 16 private string modelName; public string ModelName { get { return modelName; } set { modelName = value; } } private decimal unitCost; public decimal UnitCost { get { return unitCost; } set { unitCost = value; } } private string description; public string Description { get { return description; } set { description = value; } } public Product(string modelNumber, string modelName, decimal unitCost, string description) { ModelNumber = modelNumber; ModelName = modelName; UnitCost = unitCost; Description = description; }
}
Отображение привязанного объекта Финальный шаг — создание объекта Product с последующей привязкой к элементам управления. Хотя вы можете создать объект Product и сохранить его в ресурсах или статическом свойстве, оба подхода не имеют особого смысла. Вместо этого вы должны использовать StoreDB для создания соответствующего объекта во время выполнения с последующей привязкой к вашему окну. На заметку! Хотя декларативный подход без кода кажется более элегантным, существует немало веских причин добавлять немного кода к вашим окнам, привязанным к данным. Например, если вы запрашиваете базу данных, то, вероятно, хотите поддерживать в коде подключение к ней, чтобы решать, как обрабатывать исключения и информировать пользователя о проблемах. Рассмотрим простое окно, показанное на рис. 16.7. Оно позволяет пользователю указывать код продукта, и затем отображает соответствующий продукт в Grid (в нижней части окна). Во время проектирования этого окна вы не имеете доступа к объекту Product, который поставит данные во время выполнения. Однако вы можете создавать привязки без указания источника данных. Вам нужно просто указать свойство класса Product, которое будет использовать каждый элемент. Ниже приведен код разметки для отображения объекта Product.
Book_Pro_WPF-2.indb 490
19.05.2008 18:10:43
Привязка данных
491
Model Number: Model Name: Unit Cost: Description:
Рис. 16.7. Запрос продукта Обратите внимание, что Grid, служащий оболочкой для всех этих деталей, имеет имя, так что вы можете манипулировать им в коде и завершить привязку данных. При первом запуске приложения никакая информация не отображается. Даже если вы определите необходимые привязки, никакой объект-источник не доступен. Когда пользователь щелкает на кнопке во время выполнения, вы используете класс StoreDB для получения соответствующих данных о продукте. Хотя вы можете создать каждую привязку программно, это не имеет особого смысла (и не сэкономит много кода по сравнению с заполнением элементов управления вручную). Однако свойство DataContext предлагает блестящую альтернативу. Если вы установите его для Grid, содержащего все ваши выражения привязки данных, то все они используют его для заполнения себя данными. Далее приведен код обработки событий, реагирующий на щелчок кнопки пользователем. private void cmdGetProduct_Click(object sender, RoutedEventArgs e) { int ID;
Book_Pro_WPF-2.indb 491
19.05.2008 18:10:43
492
Глава 16 if (Int32.TryParse(txtID.Text, out ID)) { try { gridProductDetails.DataContext = App.StoreDB.GetProduct(ID); } catch { MessageBox.Show("Ошибка подключения к базе данных."); } } else { MessageBox.Show("Неверный ID."); }
}
Обновление базы данных Вам не нужно ничего делать дополнительно для того, чтобы включить обновления базы данных в этом примере. Как вы узнали раньше, свойство TextBox.Text использует двустороннюю привязку по умолчанию. В результате объект Product модифицируется, когда вы редактируете содержимое текстовых полей. (С технической точки зрения, каждое свойство обновляется, когда вы переходите к новому полю, поскольку в качестве режима обновления источника по умолчанию для свойства TextBox.Text установлен LostFocus.) Вы можете зафиксировать изменения в базе данных в любой момент. Все, что нужно для этого — добавить метод UpdateProduct() в класс StoreDB и кнопку Update (Обновить) в окно. При щелчке на ней ваш код может получить текущий объект Product из контекста данных и использовать его для фиксации обновления: private void cmdUpdateProduct_Click(object sender, RoutedEventArgs e) { Product product = (Product)gridProductDetails.DataContext; try { App.StoreDB.UpdateProduct(product); } catch { MessageBox.Show("Ошибка подключения к базе данных."); } }
С этим примером связана одна потенциальная загвоздка. Когда вы щелкаете на кнопке Update, фокус переходит к этой кнопке и все незафиксированные изменения в текстовых полях применяются к объекту Product. Однако если вы сделаете кнопку Update кнопкой по умолчанию (установив свойство IsDefault в true), появится другая возможность. Пользователь может внести изменения в одно из полей и нажать клавишу , чтобы запустить процесс обновления без фиксации последнего изменения. Во избежание такой ситуации вы можете явно передать фокус прежде, чем выполнить любой код базы данных: FocusManager.SetFocusedElement(this, (Button)sender);
Book_Pro_WPF-2.indb 492
19.05.2008 18:10:43
Привязка данных
493
Уведомление об изменениях Пример с привязкой Product работает так хорошо потому, что каждый объект Product, по сути, фиксирован — он никогда не изменяется (за исключением ситуации, когда пользователь редактирует текст в одном из привязанных текстовых полей). Для простых сценариев, когда вы заинтересованы в первую очередь в отображении содержимого и разрешении пользователю редактировать его, такое поведение совершенно приемлемо. Однако несложно представить другую ситуацию, когда привязанный объект Product может модифицироваться где-то в другом месте вашего кода. Например, кнопка Increase Price (Увеличить цену) могла бы выполнять следующий код: product.UnitCost *= 1.1M;
На заметку! Хотя вы можете извлечь объект Product из контекста данных, в этом примере предполагается, что вы также сохраняете Product как переменную-член в вашем классе окна, что упрощает код и требует меньшее количество приведений типов. Запустив этот код, вы обнаружите, что даже несмотря на то, что объект Product изменен, в текстовом поле остается старое значение. Это происходит потому, что текстовое поле не имеет никакой возможности узнать о том, что вы изменили значение. Для решения этой проблемы применяются три подхода, которые описаны ниже.
• Можно сделать каждое свойство класса Product свойством зависимостей, используя синтаксис, который вы видели в главе 6. (В данном случае ваш класс должен наследоваться от DependencyObject.) Хотя такой подход заставляет WPF выполнять работу для вас (что прекрасно), он имеет больше смысла в элементах — классах, имеющих визуальное представление в окне. Это не слишком естественный подход к классам данных вроде Product.
• Вы можете инициировать событие для каждого свойства. В данном случае событие должно иметь имя ИмяСвойстваChanged (например, UnitCostChanged). Генерирование события при изменении свойства — сугубо ваша забота.
• Вы можете реализовать интерфейс System.ComponentModel.INotifyPropertyChanged, что потребует единственного события по имени PropertyChanged. Вы должны затем инициировать это событие всякий раз, когда свойство изменяется, и указывать, какое именно свойство изменилось, передавая его имя в строке. При этом также генерирование события при изменении свойств возлагается на ваши плечи, но уже не надо определять отдельное событие для каждого свойства. Первый подход полагается на инфраструктуру свойств зависимостей WPF, в то время как второй и третий строятся на событиях. Обычно при создании объекта данных вы будете использовать третий подход. Это простейший вариант для не-элементных классов. На заметку! На самом деле вы можете воспользоваться еще одним подходом. Если вы ожидаете изменений в привязанном объекте, и этот объект не поддерживает уведомления об изменениях любым другим допустимым способом, вы можете извлечь объект BindingExpression (применив метод FrameworkElement.GetBindingExpression() ) и вызвать BindingExpression.UpdateTarget() для активизации обновления. Очевидно, что это наиболее “корявое” решение — оно похоже на применение скотча, чтобы залатать дырку. Ниже приведено определение измененного класса Product, использующего интерфейс INotifyPropertyChanged, с кодом реализации события PropertyChanged:
Book_Pro_WPF-2.indb 493
19.05.2008 18:10:43
494
Глава 16
public class Product : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null) PropertyChanged(this, e); } }
Теперь вам просто нужно инициировать событие PropertyChanged во всех установщиках значений свойств: private decimal unitCost; public decimal UnitCost { get { return unitCost; } set { unitCost = value; OnPropertyChanged(new PropertyChangedEventArgs("UnitCost")); } }
Если вы используете эту версию класса Product в предыдущем примере, то получите то поведение, которого ожидаете. При изменении текущего объекта Product новая информация появится в текстовом поле немедленно. Совет. Если изменится несколько значений, вы можете вызвать OnPropertyChanged(), передав ему пустую строку. Это сообщит WPF о необходимости заново обработать выражения привязки, привязанные к любому свойству вашего класса.
Привязка к коллекции объектов Привязка к единственному объекту довольно проста. Но все становится намного интереснее, когда вам нужно привязать некоторую коллекцию объектов — например, все продукты в таблице. Хотя каждое свойство зависимостей поддерживает привязку одного значения, которую вы видели до сих пор, привязка коллекций требует несколько более интеллектуального элемента. В WPF все классы, унаследованные от ItemsControl, способны отображать целый список элементов. Возможностью привязки данных обладают ListBox, ComboBox и ListView (а также Menu и TreeView — для иерархических данных). Совет. Хотя может показаться, что WPF предлагает ограниченный перечень списочных элементов управления, тем не менее, эти элементы предоставляют вам почти неограниченные возможности по отображению данных. Это связано с тем, что списочные элементы управления поддерживают шаблоны данных, которые позволяют непосредственно управлять отображением элементов списка. Подробнее о шаблонах данных будет рассказано в главе 17. Чтобы поддержать привязку коллекций, класс ItemsControl определяет три ключевых свойства, перечисленные в табл. 16.4.
Book_Pro_WPF-2.indb 494
19.05.2008 18:10:44
Привязка данных
495
Таблица 16.4. Свойства класса ItemsControl для привязки данных Имя
Описание
ItemsSource
Указывает на коллекцию, содержащую все объекты, которые будут показаны в списке.
DisplayMemberPath
Идентифицирует свойство, которое будет применяться для создания отображаемого текста каждого элемента коллекции.
ItemTemplate
Принимает шаблон данных, который будет использован для создания визуального представления каждого элемента. Это свойство намного мощнее, чем DisplayMemberPath, и вы узнаете о его применении в главе 17.
Здесь вы, вероятно, недоумеваете — какой именно тип коллекции можно поместить в свойство ItemSource? К счастью, практически любой. Все, что вам понадобится — это поддержка интерфейса IEnumerable, которую обеспечивают массивы, все типы коллекций и многие другие специализированные объекты, служащие оболочками для групп элементов. Однако поддержка, которую вы получаете от базового интерфейса IEnumerable, ограничена привязкой только для чтения. Если вы хотите редактировать коллекцию (например, разрешить пользователям вставку и удаление), то, как вы вскоре увидите, вам понадобится немного более сложная инфраструктура.
Отображение и редактирование элементов коллекции Рассмотрим окно, показанное на рис. 16.8, которое отображает список продуктов. Когда вы выбираете продукт, информация о нем появляется в нижней части окна, где вы можете редактировать ее. (В данном примере GridSplitter позволяет подкорректировать место, выделенное для верхней и нижней части окна.)
Рис. 16.8. Список продуктов
Book_Pro_WPF-2.indb 495
19.05.2008 18:10:44
496
Глава 16
Чтобы создать этот пример, вам нужно начать с построения логики доступа к данным. В данном случае метод StoreDB.GetProducts() извлекает список всех продуктов базы из данных, используя хранимую процедуру GetProducts. Объект Product создается для каждой записи и добавляется в обобщенную коллекцию List. (Здесь вы можете использовать любую коллекцию — например, массив или слабо типизированный ArrayList будут работать одинаково.) Ниже приведен код метода GetProducts(). public List GetProducts() { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetProducts", con); cmd.CommandType = CommandType.StoredProcedure; List products = new List(); try { con.Open(); SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { // Создать объект Product, являющийся // оболочкой для текущей записи. Product product = new Product((string)reader["ModelNumber"], (string)reader["ModelName"], (decimal)reader["UnitCost"], (string)reader["Description"], (string)reader["CategoryName"], (string)reader["ProductImage"]); // Добавить в коллекцию. products.Add(product); } } finally { con.Close(); } return products; }
Когда выполняется щелчок на кнопке Get Products (Извлечь продукты), код обработки событий вызывает метод GetProducts() и применяет его в качестве ItemsSource для списка. Коллекция также сохраняется как переменная-член класса окна для облегчения доступа из любой части вашего кода. private List products; private void cmdGetProducts_Click(object sender, RoutedEventArgs e) { products = App.StoreDB.GetProducts(); lstProducts.ItemsSource = products; }
Это успешно наполняет список объектами Product. Однако список не знает, как отображать объект продукта, поэтому он просто вызывает метод ToString(). Поскольку этот метод не был переопределен в классе Product, это даст не впечатляющий результат — отображение полного квалифицированного имени класса для каждого элемента списка (рис. 16.9). Эту проблему можно решить тремя способами.
• Установить свойство DisplayMemberPath списка. Например, установить его в ModelName, чтобы получить результат, показанный на рис. 16.9.
Book_Pro_WPF-2.indb 496
19.05.2008 18:10:44
Привязка данных
497
• Переопределить метод ToString() для возврата более полезной информации. Например, вы можете вернуть строку с номером модели и наименованием модели каждого продукта. Такой подход обеспечивает возможность отображения более одного свойства в списке (например, он замечательно подходит для комбинирования имени и фамилии в классе Customer). Однако и в этом случае вы по-прежнему слабо контролируете представление данных.
• Применить шаблон данных. Таким образом, вы можете отобразить любое размещение значений свойств (наряду с фиксированным текстом). Использование этого трюка описано в главе 17. Решив, как вы будете отображать информацию в списке, вы готовы принять следующий вызов: отображение подробностей текущего выбранного элемента списка в сетке, которая появляется под этим списком. С этой задачей можно справиться, реагируя на событие SelectionChanged и вручную изменяя контекст данных сетки, но есть более быстрый способ, который вообще не требует никакого кода. Вы просто должны установить выражение привязки для свойства Grid.DataContent, которое извлечет выбранный в списке объект Product, как показано ниже: ...
Когда появляется окно, в списке ничего не выбрано. Свойство ListBox.SelectedItem равно null, и потому Grid.DataContext также равно null, так что никакой информации не появляется. Как только вы выберете элемент в списке, контекст данных устанавливается в соответствующий объект и вся информация тут же появляется. Если вы попробуете этот пример, то удивитесь, что он уже полностью функционален. Вы можете редактировать продукты, перемещаться по списку и затем, вернувшись к предыдущей записи, убедиться, что ваши изменения были успешно зафиксированы.
Рис. 16.9. Бесполезный привязанный список
Book_Pro_WPF-2.indb 497
19.05.2008 18:10:44
498
Глава 16
Фактически, вы даже можете изменить значение, которое затрагивает отображаемый в списке текст. Если вы модифицируете наименование модели и перейдете на другой элемент управления, то соответствующая позиция в списке автоматически обновится. (Опытные разработчики оценят это преимущество, которого так не хватает приложениям Windows Forms.) Совет. Чтобы предотвратить возможность редактирования поля, установите свойство IsLocked текстового поля в true, или же, что лучше — используйте элемент управления, разрешающий только чтение (наподобие TextBlock).
Формы “главная-подробности” Как вы уже видели, можно привязать другие элементы к свойству SelectedItem вашего списка, чтобы отобразить больше деталей о выбранном элементе. Интересно то, что вы можете использовать подобную технику для построения форм “главная-подробности” отображения ваших данных. Например, вы можете создать окно, которое показывает список категорий и список продуктов. Когда пользователь выбирает категорию в первом списке, вы можете отображать только относящиеся к этой категории продукты во втором. Чтобы добиться этого, вам нужно иметь родительский объект данных, который представляет коллекцию связанных дочерних объектов данных через некоторое свойство. Например, вы можете построить объект Category, предоставляющий свойство по имени Category.Products с продуктами, относящимися к этой категории. (Фактически, вы можете найти пример класса Category с похожим дизайном в главе 18.) Затем вы можете создать форму “главная-подробности” из двух списков. Заполните первый из них объектами Category. Чтобы показать связанные продукты, привяжите второй список, отображающий продукты, к свойству SelectedItem. Products первого списка. Это сообщит второму списку о необходимости взять текущий объект Category, извлечь его коллекцию связанных объектов Product и отобразить их. Пример применения взаимосвязанных данных представлен в главе 18, с TreeView, отображающим категоризированный список продуктов. Вы увидите там две версии примера — один, использующий объекты Category и Product, и другой — ADO.NET-объект DataRelation. Конечно, чтобы завершить этот пример с точки зрения приложения, вам еще нужно добавить некоторый код. Например, вам может понадобиться метод UpdateProducts(), принимающий коллекцию продуктов и выполняющий соответствующие операторы. Поскольку обычный объект .NET не предоставляет никаких возможностей отслеживания изменений, в этой ситуации вы можете подумать о применении DataSet из ADO. NET (что будет описано чуть позже в этой главе). Альтернативно вы можете пожелать заставить пользователей обновлять записи по одной за раз. (Одним из вариантов может быть отключение списка при модификации текстового поля и принуждения пользователя отменять изменение щелчком на кнопке Cancel (Отмена), либо немедленно подтверждать изменения щелчком по кнопке Update (Обновить).)
Вставка и удаление элементов коллекций Одно из ограничений предыдущего примера заключается в том, что он не может показать изменения, которые вы внесли в коллекцию. Он замечает измененные объекты Products, но не может обновить список, если вы добавляете новый элемент или удаляете его в коде. Например, предположим, что вы добавили кнопку Delete (Удалить), которая выполняет следующий код:
Book_Pro_WPF-2.indb 498
19.05.2008 18:10:44
Привязка данных
499
private void cmdDeleteProduct_Click(object sender, RoutedEventArgs e) { products.Remove((Product)lstProducts.SelectedItem); }
Удаленный элемент исключается из коллекции, но упорно остается видимым в привязанном списке. Чтобы включить отслеживание изменений коллекции, вы должны использовать коллекцию, реализующую интерфейс INotifyCollectionChanged. Большинство обобщенных коллекций этого не делают, включая List, использованную в данном примере. Фактически WPF включает единственную коллекцию, реализующую INotifyCollection Changed — это класс ObservableCollection. На заметку! Если у вас имеется объектная модель, которую вы перенесли из мира Windows Forms, то вы можете использовать Windows Forms — эквивалент ObservableCollection, которым является BindingList. Коллекция BindingList реализует IBindingList вместо INotifyCollectionChanged, который включает событие ListChanged, играющее ту же роль, что и событие INotifyCollectionChanged.CollectionChanged. Вдобавок вы можете наследоваться от BindingList, чтобы получить дополнительные средства для сортировки и создания элементов в элементе управления Windows Forms по имени DataGridView. Вы можете унаследовать собственную коллекцию от ObservableCollection, чтобы настроить по своему усмотрению ее работу, хотя это и не обязательно. В данном примере достаточно заменить объект List на ObservableCollection, как показано ниже: public List GetProducts() { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetProducts", con); cmd.CommandType = CommandType.StoredProcedure; ObservableCollection products = new ObservableCollection(); ...
Типом возврата может быть оставлен List , потому что класс ObservableCollection порожден от класса List. Чтобы сделать этот пример немного более универсальным, вы можете применить в качестве типа возврата
ICollection, так как интерфейс ICollection включает в себя все необходимые члены. Теперь, если вы удалите или добавите программно элемент в коллекцию, список будет соответствующим образом обновлен. Конечно, вам придется по-прежнему создавать код доступа к данным, который происходит перед модификацией коллекции — например, код, удаляющий запись о продукте из лежащей в основе базы данных.
Привязка объектов ADO.NET Все средства, которые вы изучили для специальных объектов, также работают с отключенными объектами данных ADO.NET. Например, вы можете создать такой же пользовательский интерфейс, как показанный на рис. 16.9, но использовать DataSet, DataTable и DataRow на заднем плане вместо специального класса Product и ObservableCollection. Чтобы попробовать это, начнем с рассмотрения версии, где метод GetProducts() извлекает те же данные, но упакованные в DataTable:
Book_Pro_WPF-2.indb 499
19.05.2008 18:10:44
500
Глава 16
public DataTable GetProducts() { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetProducts", con); cmd.CommandType = CommandType.StoredProcedure; SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataSet ds = new DataSet(); adapter.Fill(ds, "Products"); return ds.Tables[0]; }
Вы можете извлечь этот объект DataTable и привязать его к списку почти так же, как делали это с ObservableCollection. Единственное отличие состоит в том, что вы не можете привязаться к самому DataTable. Вместо этого нужно пройти через посредника — DataView. Хотя DataView можно создать вручную, но каждый DataTable имеет готовый к применению объект, доступный через свойство DataTable.DefaultView. На заметку! В этом ограничении нет ничего нового. Даже в приложении Windows Forms все привязки DataTable проходят через DataView. Разница в том, что в мире Windows Forms этот факт может быть скрыт. Это позволяет писать код, который выглядит так, будто привязка осуществляется непосредственно к DataTable, когда на самом деле используется DataView, взятый из свойства DataTable.DefaultView. Вот такой код вам понадобится: private DataTable products; private void cmdGetProducts_Click(object sender, RoutedEventArgs e) { products = App.StoreDB.GetProducts(); lstProducts.ItemsSource = products.DefaultView; }
Теперь список создаст отдельное вхождение для каждого объекта DataRow из коллекции DataTable.Rows. Чтобы определить, какое содержимое показано в списке, вам нужно установить свойство DisplayMemberPath с именем поля, которое вы хотите показать, или использовать шаблон данных (как описано в главе 17). Замечательный аспект этого примера заключается в том, что, однажды изменив код, извлекающий данные, вам не нужно вносить никаких дополнительных модификаций. Когда элемент выбирается в списке, то лежащий в основе Grid получает этот выбранный элемент для своего контекста данных. Код разметки, использованный с коллекцией ProductList, по-прежнему работает, поскольку имена свойств класса Product соответствуют именам полей DataRow. Другая замечательная особенность этого примера состоит в том, что вам не нужно предпринимать никаких дополнительных шагов для реализации уведомлений об изменениях. Дело в том, что класс DataView реализует интерфейс IBindingList, что позволяет ему извещать инфраструктуру WPF о добавлении новых DataRow или удалении существующих. Однако вам следует быть немного внимательнее при удалении объекта DataRow. Может показаться, что для удаления текущей выбранной записи подойдет код вроде следующего: products.Rows.Remove((DataRow)lstProducts.SelectedItem);
Такой код неверен по двум причинам. Во-первых, выбранный элемент в списке не является объектом DataRow. Это тонкая оболочка для DataRow, представленная DataView. Во-вторых, вероятно, вы не хотите удалять DataRow из коллекции строк таб-
Book_Pro_WPF-2.indb 500
19.05.2008 18:10:45
Привязка данных
501
лицы. Вместо этого, возможно, вы пожелаете просто пометить его как удаленный, чтобы соответствующая запись в таблице базы данных была удалена только при фиксации изменений. Ниже приведен правильный код, который получает выбранный DataRowView, использует свойство Row для нахождения соответствующего объекта DataRow и вызывает метод Delete() для пометки строки с целью последующего удаления: ((DataRowView)lstProducts.SelectedItem).Row.Delete();
В этой точке запланированный к удалению DataRow исчезает из списка, даже несмотря на то, что технически он остается в коллекции DataTable.Rows. Так происходит потому, что установки фильтрации по умолчанию в DataView скрывают все удаленные строки. В главе 17 вы узнаете больше о механизме фильтрации.
Привязка к выражению LINQ Одной из причин перехода от .NET 3.0 к .NET 3.5 является поддержка языка интегрированных запросов (Language Integrated Query — LINQ), предлагающий синтаксис запросов общего назначения, который работает с широким разнообразием источников данных и тесно интегрирован в язык C#. LINQ работает с любым источником, для которого есть соответствующий поставщик LINQ. Используя поддержку, включенную в .NET 3.5, вы можете использовать одинаково структурированные запросы LINQ для извлечения данных из коллекции, находящейся в памяти, из файла XML или из базы данных SQL Server. Как и другие языки запросов, LINQ позволяет фильтровать, сортировать и трансформировать извлекаемые данные. Хотя LINQ выходит за рамки тем, рассматриваемых в нашей книге, вы можете многое узнать о нем из простого примера. Например, предположим, что у вас есть коллекция объектов Product по имени products, и вы хотите создать вторую коллекцию, содержащую только те продукты, цена которых превышает $100. Используя процедурный код, вы можете написать что-то вроде следующего: // Получить полный список продуктов. List products = App.StoreDB.GetProducts(); // Создать вторую коллекцию с нужными продуктами. List matches = new List(); foreach (Product product in products) { if (product.UnitCost >= 100) { matches.Add(product); } }
Используя LINQ, вы можете написать следующее, намного более удобное выражение: // Получить полный список продуктов. List products = App.StoreDB.GetProducts(); // Создать вторую коллекцию с подходящими продуктами. IEnumerable matches = from product in products where product.UnitCost >= 100 select product;
Этот пример использует API-интерфейс LINQ to Collections, т.е. применяет выражение LINQ для выполнения запроса данных из находящейся в памяти коллекции. Выражения LINQ используют набор новых ключевых слов языка, включая from, in и select. Эти ключевые слова LINQ составляют неотъемлемую часть языка C#.
Book_Pro_WPF-2.indb 501
19.05.2008 18:10:45
502
Глава 16
На заметку! Полное обсуждение LINQ не входит в перечень тем нашей книги. За более детальной информацией обращайтесь в центр разработчиков LINQ по адресу http://msdn. microsoft.com/data/ref/linq или в огромный каталог примеров LINQ на http:// msdn2.microsoft.com/en-us/vcsharp/aa336746.aspx. LINQ вращается вокруг интерфейса IEnumerable. Независимо от того, какие источники данных вы используете, каждое выражение LINQ возвращает объект, реализующий IEnumerable. Поскольку интерфейс IEnumerable расширяет IEnumerable, вы можете привязать его к окну WPF, как делаете это с обычной коллекцией: lstProducts.ItemsSource = matches;
Таким образом, нам нужно рассмотреть лишь несколько нюансов. Детали вы найдете в следующих разделах.
Преобразование IEnumerable в обычную коллекцию В отличие от классов ObservableCollection и DataTable, интерфейс IEnumerable не предусматривает способов добавления или удаления элементов. Если вам нужна такая возможность, сначала придется преобразовать ваш объект IEnumerable в массив или коллекцию List, используя для этого метод ToArray() или ToList(). Ниже приведен пример использования ToList() для преобразования результатов запроса LINQ (показанного выше) в строго типизированную коллекцию List объектов Product: List productMatches = matches.ToList();
На заметку! ToList() — расширяющий метод, а это означает, что он определен в другом классе, чем тот с которым применяется. Технически ToList() определен во вспомогательном классе System.Linq.Enumerable и доступен всем объектам IEnumerable. Однако он не будет доступен, если в вашем текущем контексте недоступен класс Enumerable, т.е. показанный выше код не сможет работать, если не импортирует пространство имен System.Linq. Метод ToList() вызывает немедленное выполнение выражения LINQ. В результате получается обычная коллекция, с которой вы можете работать привычным способом. Например, вы можете поместить ее в оболочку ObservableCollection, чтобы получать события уведомлений, так что любые изменения в ней будут немедленно отражаться в связанных элементах управления: ObservableCollection productMatchesTracked = new ObservableCollection(productMatches);
Вы можете затем привязать коллекцию productMatchesTracked к элементу управления в вашем окне.
Отложенное выполнение LINQ использует отложенное выполнение. В противоположность тому, что вы можете ожидать, результатом выражения LINQ (такого как в случае нахождения соответствующих объектов из предыдущего примера) не является непосредственно коллекция. Вместо этого вы получаете специализированный объект LINQ, обладающий способностью извлекать данные по мере необходимости, а не в тот момент, когда создается выражение LINQ. В этом примере соответствующим объектом является экземпляр WhereIterator — приватного класса, вложенного внутрь класса System.Linq.Enumerable:
Book_Pro_WPF-2.indb 502
19.05.2008 18:10:45
Привязка данных
503
matches = from product in products where product.UnitCost >= 100 select product;
В зависимости от используемого вами специфического запроса выражение LINQ может возвращать разные объекты. Например, выражение объединения, комбинирующее данные из двух разных коллекций, будет возвращать экземпляр приватного класса UnionIterator. Или же, если вы упростите запрос, исключив конструкцию where, то получите простой итератор SelectIterator. На самом деле вам даже не нужно знать конкретный класс итератора, используемый вашим кодом, потому что вы взаимодействуете с его результатами через интерфейс IEnumerable. (Но если вы любопытны, то можете определить тип объекта во время выполнения, рассмотрев соответствующую переменную в отладчике Visual Studio в режиме останова выполнения кода.) Объекты итераторов LINQ добавляют дополнительный слой между определением выражения LINQ и его выполнением. Как только вы начинаете выполнять итерацию по объекту WhereIterator, он извлекает необходимые ему данные. Например, если вы пишете блок foreach, который проходит по коллекции соответствующих запросу объектов, это действие вызывает оценку выражения LINQ. То же самое происходит, когда вы привязываете объект IEnumerable к окну WPF — в этом случае инфраструктура привязки данных WPF выполняет итерацию по его содержимому. На заметку! Нет никаких технических причин, которые бы требовали от LINQ использования отложенного выполнения, но есть масса аргументов в пользу такого подхода. Во многих ситуациях это позволяет LINQ применять технику оптимизации производительности, которая была бы невозможна в противном случае. Например, когда вы используете отношения базы данных с LINQ to SQL, то можете избежать загрузки зависимых данных, которые вам не нужных на самом деле. Отложенное выполнение также допускает оптимизацию, когда вы создаете запросы LINQ, базирующиеся на других запросах LINQ.
Отложенное выполнение и LINQ to SQL Концепцию отложенного выполнения важно понимать, когда вы используете источник данных, который может быть не доступным. В примерах, которые вы видели до сих пор, выражения LINQ работали с находящейся в памяти коллекцией, отсюда важность (по крайней мере, для разработчика приложения) знания о том, когда именно вычисляется выражение. Однако это не касается случая, когда вы применяете LINQ to SQL для выполнения оперативного (just-in-time) запроса, адресованного базе данных. В такой ситуации перечисление результатов объекта IEnumerable заставит .NET установить соединение с базой данных и выполнить запрос. Это очевидно рискованное действие — если сервер базы данных недоступен или не может ответить, то исключение возникнет тогда, когда вы менее всего этого ожидаете. По этой причине принято использовать выражения LINQ двумя более ограниченными способами. • После того, как вы извлечете данные (используя обычный код доступа к данным), применяйте LINQ to Collections для фильтрации результатов. Это удобно, если вам нужно получить несколько разных представлений одних и тех же результатов. Именно этот подход демонстрируется в данном разделе. • Использовать LINQ to SQL для получения нужных данных. Это избавит вас от написания низкоуровневого кода доступа к данным. Применяйте метод ToList() для принудительного немедленного выполнения запроса и возврата обычной коллекции. Обычно создание компонента базы данных, использующего LINQ to SQL и возвращающего результирующий объект IEnumerable — не самая удачная идея. Если вы допускаете подобное, то теряете контроль над тем, когда именно будет выполнен запрос, и как будут обработаны потенциальные ошибки. (Также вы утрачиваете контроль над тем, сколько раз будет выполнен
Book_Pro_WPF-2.indb 503
19.05.2008 18:10:45
504
Глава 16
запрос, потому что выражение LINQ будет повторно вычисляться при каждой итерации по коллекции или привязке элемента управления. Привяжите одни и те же данные к нескольким разным элементам управления — и вы обеспечите лишней работой ваш сервер базы данных.) LINQ to SQL — сама по себе обширная тема. Этот API предлагает гибкий, свободный от SQL способ извлечения информации из базы данных и помещения их в спроектированные вами объекты (за счет изучения синтаксиса LINQ и применения еще одной модели данных.) В настоящее время LINQ to SQL поддерживает только SQL Server. Если вы хотите попробовать это, начните с детального обзора по адресу http://msdn2.microsoft.com/en-us/library/bb425822.aspx или же обратитесь к специальной книге по LINQ, такой как LINQ: язык интегрированных запросов в C# 2008 для профессионалов (ИД “Вильямс”, 2008 г.).
Преобразование данных При обычной привязке информация путешествует от источника к цели без каких-либо изменений. Это кажется логичным, но такое поведение не всегда подходит. Часто ваш источник данных может применять некоторое низкоуровневое представление, которое не нужно отображать непосредственно в пользовательском интерфейсе. Например, вы можете пожелать, чтобы числовые коды заменялись читабельными для человека строками, числа представлялись в укороченном виде, даты отображались в длинном формате и т.д. Если так, то нужен какой-то способ преобразования этих значений в корректную отображаемую форму. И если вы применяете двунаправленную привязку, вам также понадобится обратная операция — взять введенные пользователем данные и преобразовать в представление, подходящее для хранения в соответствующем объекте данных. К счастью, WPF позволяет делать то и другое посредством создания (и использования) класса конвертера значений. Такой конвертер значений отвечает за преобразование исходных данных непосредственно перед их отображением в целевом элементе и (в случае двунаправленной привязки) преобразования нового целевого значения непосредственно перед его применением к источнику. На заметку! Такой подход к преобразованию подобен способу работы привязки данных в мире Windows Forms с событиями привязки Format и Parse. Отличие заключается в том, что в приложении Windows Forms вы можете кодировать эту логику где угодно — вам просто нужно присоединить оба события к привязке. В WPF эта логика должна быть инкапсулирована в классе конвертера значений, что облегчает ее повторное использование. Конвертеры значений — исключительно удобная часть инфраструктуры привязки данных WPF. Их можно использовать несколькими удобными способами, которые перечислены ниже.
• Для форматирования данных к строчному представлению. Например, вы можете преобразовывать число в строку валюты. Это — наиболее очевидное применение конвертеров значений, но, конечно же, не единственное.
• Для создания специфических типов объектов WPF. Например, вы можете прочитать блок двоичных данных и создать объект BitmapImage, который можно привязать к элементу Image.
• Для условного изменения свойства элемента на основе привязанных данных. Например, вы можете создать конвертер значений, который изменяет цвет фона элемента для выделения значений из определенного диапазона. В следующих разделах мы рассмотрим пример каждого из перечисленных подходов.
Book_Pro_WPF-2.indb 504
19.05.2008 18:10:45
505
Привязка данных
Форматирование строк конвертером значений Конвертеры значений — замечательный инструмент для форматирования чисел, которые нужно отображать в виде тексте. Например, рассмотрим свойство Product. UnitCost из предыдущего примера. Оно сохраняется в виде десятичного числа, и в результате при отображении в текстовом поле вы видите значение вроде 3.9900. Такой формат не только отображает больше десятичных знаков, чем вам нужно, но также он пропускает символ валюты. Более интуитивно понятное представление должно выглядеть как $3.99, что показано на рис. 16.10.
Рис. 16.10. Отображение форматированных денежных величин Для создания конвертера значений потребуется выполнить четыре шага. 1. Создать класс, реализующий IValueConverter. 2. Добавить атрибут ValueConversion в объявление класса и специфицировать исходный и целевой типы данных. 3. Реализовать метод Convert(), преобразующий данные из исходного формата в отображаемый формат. 4. Реализовать метод ConvertBack(), выполняющий обратное преобразование значения из отображаемого формата в его “родной” формат. На рис. 16.11 можно видеть это в работе. В случае преобразования десятичного значения в денежное вы можете использовать метод Decimal.ToString(), чтобы получить нужное отформатированное строковое представление.
Объект\источник (Объект данных)
Конвертер значений Convert()
Свойство ConvertBack()
Целевой объект (Отображаемый элемент)
Свойство зависимостей (Установленное привязкой)
Рис. 16.11. Преобразование привязанных данных
Book_Pro_WPF-2.indb 505
19.05.2008 18:10:45
506
Глава 16
Для этого вам достаточно специфицировать строку денежного формата "C", как показано ниже: string currencyText = decimalPrice.ToString("C");
Этот код использует установки культуры, примененные в текущем потоке. Компьютер, настроенный для региона English (United States), работает с установкой локали en-US и отображает валюту знаком доллара ($). Компьютер, сконфигурированный для другой локали, может отображать другой символ валюты. Если это не то, что вам нужно (например, необходимо, чтобы появлялся знак числа), то вы можете специфицировать культуру, используя перегрузку метода ToString(), показанную ниже: CultureInfo culture = new CultureInfo("en-US"); string currencyText = decimalPrice.ToString("C", culture);
Подробнее обо всех доступных строках формата можно прочитать в справочной системе Visual Studio. В табл. 16.5 и 16.6 перечислены наиболее часто используемые опции, которые вы будете применять для числовых данных и дат соответственно.
Таблица 16.5. Строки формата для числовых данных Тип
Строка формата
Валюта
C
Пример
$1,234.50 Отрицательные значения представляются в скобках: ($1,234.50). Знак валюты специфичен для локали.
Научный (экспоненциальный)
E
1.234.50E+004
Процентный
P
45.6%
Фиксированный десятичный
F?
Зависит от количества установленных десятичных разрядов. F3 форматирует значения в виде 123.400. F0 форматирует значения как 123.
Таблица 16.6. Строки формата для времени и дат Тип
Строка формата
Формат
Короткая дата
d
M/d/yyyy. Например: 10/30/2007
Длинная дата
D
dddd, MMMM dd, yyyy. Например: Monday, January 30, 2008
Длинная дата и короткое время
f
dddd, MMMM dd, yyyy HH:mm aa. Например: Monday, January 30, 2008 10:00 AM
Длинная дата и длинное время
F
dddd, MMMM dd, yyyy HH:mm:ss aa. Например: Monday, January 30, 2008 10:00:23 AM
Сортируемый стандарт ISO
s
yyyy-MM-dd HH:mm:ss. Например: 2008-01-30 10:00:23
Месяц и день
M
MMMM dd. Например: January 30
Общий
G
M/d/yyyy HH:mm:ss aa (зависит от локальных установок). Например: 10/30/2008 10:00:23 AM
Book_Pro_WPF-2.indb 506
19.05.2008 18:10:45
Привязка данных
507
Обратное преобразование от отображаемого формата к числовому немного сложнее. Методы Parse() и TryParse() типа Decimal — логичный способ выполнить эту работу, но обычно они не могут справиться со строками, включающими символы валюты. Решение заключается в применении перегруженных версий методов Parse() и TryParse(), которые принимают значение System.Globalization.NumberStyles. Если вы примените NumberStyles.Any, то сможете успешно избавиться от символа валюты, если он присутствует. Ниже приведен полный код конвертера значений, который имеет дело со значениями цены, хранимыми в свойстве Product.UnitCost. [ValueConversion(typeof(decimal), typeof(string))] public class PriceConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { decimal price = (decimal)value; return price.ToString("C", culture); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { string price = value.ToString(culture); decimal result; if (Decimal.TryParse(price, NumberStyles.Any, culture, out result)) { return result; } return value; } }
Чтобы ввести в действие этот конвертер, надо начать с отображения пространства имен вашего проекта на префикс пространства имен XML, который можно применять в коде разметки. Приведем пример, в котором используется префикс пространства имен и предполагается, что конвертер значений находится в пространстве имен DataBinding: xmlns:local="clr-namespace:DataBinding"
Обычно вы будете добавлять этот атрибут к дескриптору , который содержит всю вашу разметку. Теперь вам нужно просто создать экземпляр класса PriceConverter и присвоить его свойству Converter вашей привязки. Чтобы сделать это, понадобится несколько более многословный синтаксис, приведенный ниже: Unit Cost:
Во многих случаях один и тот же конвертер используется для множества привязок. В этом случае не имеет смысла создавать по экземпляру конвертера для каждой при-
Book_Pro_WPF-2.indb 507
19.05.2008 18:10:46
508
Глава 16
вязки. Вместо этого создайте один объект конвертера в коллекции Resources, как показано далее:
Затем вы можете указывать на него в вашей привязке, используя ссылку
StaticResource, как было описано в главе 11: " + minimumPrice.ToString(); } }
Обратите внимание, что в этом примере применяется обходной путь с преобразованием текста в текстовом поле txtMinPrice в десятичное значение и затем обратно в строку, которая и должна использоваться для фильтрации. Это требует приложения чуть большего количества усилий, но исключает вероятность атак и ошибок с недействительными символами. В случае создания строки фильтра путем просто конкатенации текста из поля txtMinPrice, она могла бы содержать операции фильтра (=, ) и ключевые слова (AND, OR), приводящие к выполнению фильтрации совершенно отличным от ожидаемого образом. Подобное могло бы произойти в результате как намеренной атаки, так и из-за ошибки пользователя.
Сортировка Представление также можно использовать и для реализации сортировки. Самым простым подходом является сортировка на основании значения одного или более свойств в каждом элементе данных. Применяемые поля указываются с помощью объектов System.ComponentModel.SortDescription. Каждый объект SortDescription указывает на поле, которое должно использоваться для сортировки, и направление, в котором она должна выполняться (по возрастанию или по убыванию). Добавляются эти объекты SortDescription в том порядке, в котором они должны применяться. Например, можно сделать так, чтобы сортировка сначала осуществлялась по категории, а затем — по названию модели. Ниже приведен пример применения простой сортировки по названию модели в порядке возрастания: ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource); view.SortDescriptions.Add( new SortDescription("ModelName", ListSortDirection.Ascending));
Поскольку в этом коде используется не специальный класс представления, а интерфейс ICollectionView, он работает одинаково хорошо, каким бы ни был тип привязываемого источника данных. В случае BindingListCollectionView (при привязке объекта DataTable), объекты SortDescription используются для создания сортировочной строки, которая применяется к лежащему в основе свойству DataView.Sort. На заметку! При наличии более одного объекта BindingListCollectionView, работающего с одним и тем же объектом DataView, оба будут использовать одинаковые параметры фильтрации и сортировки, потому что эти детали хранятся в DataView, а не в BindingListCollec tionView. Те, кого такое поведение не устраивает, могут создать для упаковки одного и того же объекта DataTable несколько представлений DataView.
Book_Pro_WPF-2.indb 551
19.05.2008 18:10:52
552
Глава 17
Как и следовало ожидать, при сортировке строк значения упорядочиваются по алфавиту, а числа — в порядке нумерации. Чтобы применить другой порядок сортировки, сначала нужно очистить существующую коллекцию SortDescription. Также еще возможно выполнение специальной сортировки, но только в случае использования ListCollectionView (а не BindingListCollectionView). Класс ListCollectionView предоставляет свойство CustomSort, которое принимает объект IComparer, сравнивающий два элемента данных и указывающий, какой из них больше. Такой подход удобен, если требуется разработать процедуру сортировки, комбинирующую свойства для получения ключа сортировки. В нем также есть смысл при наличии нестандартных правил сортировки. Например, может быть нужно, чтобы несколько первых символов в коде продукта игнорировались, чтобы вычисление выполнялось по цене, чтобы поле перед сортировкой преобразовывалось в другой тип данных или другое представление, и т.д. Ниже показан пример, в котором сначала подсчитывается количество букв в названии модели, а затем полученное значение используется для определения порядка сортировки: public class SortByModelNameLength : IComparer { public int Compare(object x, object y) { Product productX = (Product)x; Product productY = (Product)y; return productX.ModelName.Length.CompareTo(productY.ModelName.Length); } }
Вот код, подключающий IComparer к представлению: ListCollectionView view = (ListCollectionView) CollectionViewSource.GetDefaultView(lstProducts.ItemsSource); view.CustomSort = new SortByModelNameLength();
В этом примере объект IComparer разработан так, чтобы он вписывался в конкретный сценарий. Если необходимо иметь возможность многократного использования объекта IComparer с похожими данными, но в разных местах, его можно обобщить. Например, класс SortByModelNameLength можно было бы заменить классом SortByTextLength. При создании экземпляра SortByTextLength код тогда должен был бы предоставлять имя используемого свойства (в виде строки), а метод Compare() мог бы с помощью рефлексии отыскивать его в объекте данных (как было в примере с SingleCriteriaHighlightTemplateSelector, который приводился ранее в этой главе).
Группирование Представления (во многом похожим на сортировку образом) позволяют применять и группирование. Как и сортировка, группирование может выполняться простым путем (на основании значения какого-то одного свойства) и сложным (с помощью специального обратного вызова). Для выполнения группирования, прежде всего, нужно добавить объекты System. ComponentModel.PropertyGroupDescription в коллекцию CollectionView. GroupDescriptions. Ниже показан пример группирования продуктов по названию категории: ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource); view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryName"));
Book_Pro_WPF-2.indb 552
19.05.2008 18:10:52
Шаблоны данных, представления данных и поставщики данных
553
На заметку! В этом примере предполагается, что в классе Product присутствует свойство с именем CategoryName. Однако более вероятно, что в нем будет присутствовать свойство с именем Category (возвращающее указанный объект Category) или свойство с именем CategoryID (указывающее на категорию с помощью уникального идентификационного номера). И в том и в другом случае все равно можно будет использовать группирование, но только добавив конвертер значений, анализирующий группируемую информацию (т.е. объект Category или свойство CategoryID) и возвращающий правильный текст категории для использования с группой. Как это можно сделать, будет продемонстрировано в следующем примере. С этим примером связана одна проблема. Элементы будут упорядочены в отдельные группы на основании их категорий, но того, что к списку было применено какое-то группирование, будет не видно. На самом деле список будет выглядеть точно так же, как и если бы в нем была просто выполнена сортировка по названию категории. В действительности же происходит нечто большее, просто увидеть это при параметрах по умолчанию невозможно. Когда используется группирование, для каждой группы создается отдельный объект GroupItem, и все эти объекты GroupItem добавляются в список. GroupItem представляет собой элемент управления содержимым, поэтому в каждом объекте GroupItem находится соответствующий контейнер (наподобие ListBoxItem) с фактическими данными. Сделать так, чтобы группы было видно, можно просто отформатировав элемент GroupItem так, чтобы он выделялся (т.е. отличался от остальных элементов). Для этого можно использовать стиль, применяющий форматирование ко всем объектам GroupItem в списке. Однако наверняка просто форматирования будет недостаточно — например, может также возникнуть желание сделать так, чтобы для каждой группы отображался еще и заголовок, что требует помощи шаблона. К счастью, класс ItemsControl делает выполнение обеих этих задач простым, благодаря свойству ItemsControl.GroupStyle, которое предоставляет коллекцию объектов GroupStyle. Несмотря на имя, класс GroupStyle стилем не является. Он представляет собой просто удобный пакет, упаковывающий несколько полезных параметров для конфигурирования объектов GroupItem. Свойства класса GroupStyle перечислены в табл. 17.1.
Таблица 17.1. Свойства класса GroupStyle Имя
Описание
ContainerStyle
Устанавливает стиль, который должен применяться к генерируемому для каждой группы элементу GroupItem.
ContainerStyleSelector
Это свойство можно использовать вместо свойства ContainerStyle для предоставления класса, выбирающего подходящий для использования стиль на основании группы.
HeaderTemplate
Позволяет создавать шаблон для отображения содержимого в начале каждой группы.
HeaderTemplateSelector
Это свойство можно использовать вместо свойства HeaderTemplate для предоставления класса, выбирающего подходящий для использования шаблон заголовка на основании группы.
Panel
Позволяет изменять шаблон, используемый для удержания групп. Например, WrapPanel можно применять вместо стандартного StackPanel для создания списка, предусматривающего мозаичное размещение групп слева направо и вниз.
Book_Pro_WPF-2.indb 553
19.05.2008 18:10:52
554
Глава 17
В этом примере нужно всего лишь, чтобы перед каждой группой отображался заголовок. И тогда можно будет получить эффект, показанный на рис. 17.12.
Рис. 17.12. Группирование списка продуктов Чтобы добавить заголовок для группы, нужно установить свойство GroupStyle. HeaderTemplate. Это свойство можно заполнить обычным шаблоном данных, подобным тем, что показывались ранее в этой главе, и использовать внутри него любую комбинацию элементов и выражений привязки данных. Однако есть один секрет. При написании выражения привязки привязку нужно выполнить не в отношении объекта данных из списка (каковым в данном случае является объект Product), а в отношении предназначенного для этой группы объекта PropertyGroupDescription. Это означает, что при желании отобразить значения поля для этой группы (как показано на рис. 17.12), необходимо привязать свойство PropertyGroupDescription.Name, а не свойство Product.CategoryName. Ниже показан весь шаблон.
Совет. Свойство ListBox.GroupStyle фактически представляет собой коллекцию объектов GroupStyle. Это позволяет добавить множество уровней группирования. Чтобы сделать это, необходимо добавить более одного объекта PropertyGroupDescription (причем в том порядке, в котором должно применяться группирование и подгруппирование), а затем еще добавить и соответствующий объект GroupStyle для форматирования каждого уровня.
Book_Pro_WPF-2.indb 554
19.05.2008 18:10:52
Шаблоны данных, представления данных и поставщики данных
555
Группирование часто используется вместе с сортировкой. Для сортировки групп нужно всего лишь сделать так, чтобы первый используемый объект SortDescription осуществлял сортировку на основании поля группировки. Ниже показан код, в котором сначала в алфавитном порядке по названию категории сортируются категории, а затем в алфавитном порядке по имени модели сортируется уже каждый продукт в этой категории: view.SortDescriptions.Add(new SortDescription("CategoryName", ListSortDirection.Ascending)); view.SortDescriptions.Add(new SortDescription("ModelName", ListSortDirection.Ascending));
Одно из ограничений демонстрируемого здесь подхода с простым группированием заключается в том, что для выполнения группирования он требует наличия поля с дублированными значениями. Предыдущий пример будет работать потому, что многие продукты относятся к одной и той же категории и, соответственно, имеют дублированные значения в свойстве CategoryName. Однако при попытке выполнить группирование по какому-то другому фрагменту информации, например, по полю UnitCost, этот подход уже не будет работать столь же хорошо. В таком случае он приведет к созданию отдельной группы для каждого продукта. Для этой проблемы существует решение. Можно создать класс, анализирующий какой-то фрагмент информации и помещающий его в концептуальную группу с целью отображения. Такой прием очень часто используется для группирования объектов данных с числами или датами, разбиваемыми на определенные диапазоны. Например, в текущем примере можно было бы одну группу создать для продуктов, цена которых составляет меньше $50, другую — для продуктов, цена которых находится в диапазоне от $50 до $100, и т.д., как показано на рис. 17.13.
Рис. 17.13. Группирование по диапазонам
Book_Pro_WPF-2.indb 555
19.05.2008 18:10:52
556
Глава 17
Для реализации такого решения потребуется предоставить конвертер значений, анализирующий поле в источнике данных (или множество полей, если реализовать конвертер типа IMultiValueConverter) и возвращающий заголовок группы. При условии использования одинакового заголовка группы для множества объектов данных, эти объекты будут помещаться в одну и ту же логическую группу. Следующий код иллюстрирует конвертер, создающий диапазоны цен, которые были показаны на рис. 17.13. Он спроектирован с определенной долей гибкости — в частности, в нем можно задавать размер диапазонов группирования. (На рис. 17.13 размер диапазона составляет 50 единиц.) public class PriceRangeProductGrouper : IValueConverter { public int GroupInterval { get; set; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { decimal price = (decimal)value; if (price < GroupInterval) { return String.Format(culture, "Less than {0:C}", GroupInterval); } else { int interval = (int)price / GroupInterval; int lowerLimit = interval * GroupInterval; int upperLimit = (interval + 1) * GroupInterval; return String.Format(culture, "{0:C} to {1:C}", lowerLimit, upperLimit); } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException("This converter is for grouping only."); } }
Чтобы сделать этот класс даже еще более гибким, так чтобы он мог использоваться с другими полями, в него можно было бы добавить другие свойства, которые бы позволяли устанавливать фиксированную часть текста заголовка и строку формата, который должен применяться при преобразовании числовых значений в текст заголовка. (В текущем коде предполагается, что числа должны интерпретироваться как денежные единицы, поэтому число 50 в заголовке превращается в $50.00.) Ниже показан код, в котором данный конвертер используется для применения группирования по диапазонам. Обратите внимание на то, что продукты должны сначала сортироваться по цене, иначе группироваться они будут на основании того, в каком месте списка они находятся. ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource); view.SortDescriptions.Add(new SortDescription("UnitCost", ListSortDirection.Ascending)); PriceRangeProductGrouper grouper = new PriceRangeProductGrouper(); grouper.GroupInterval = 50; view.GroupDescriptions.Add(new PropertyGroupDescription("UnitCost", grouper));
Book_Pro_WPF-2.indb 556
19.05.2008 18:10:53
557
Шаблоны данных, представления данных и поставщики данных
В этом примере вся работа выполняется в коде, но конвертер и представление также можно создавать и декларативным образом, размещая их в коллекции Resources окна. О том, как именно это делается речь пойдет уже в следующем разделе.
Создание представлений декларативным образом Пока что все приведенные примеры работали одинаково. Они предполагали извлечение нужного представления с помощью кода и последующее его изменение программным путем. Однако существует и другой вариант: можно создавать класс CollectionViewSource декларативно в XAML-разметке и затем связывать этот класс CollectionViewSource со своими элементами управления (например, списком). На заметку! С технической точки зрения CollectionViewSource — это не представление, а вспомогательный класс, который позволяет извлекать представление (с помощью метода GetDefaultView(), который уже демонстрировался в предыдущих примерах), и фабрика, которая может создавать представление, когда в нем возникает необходимость (как будет показываться в этом разделе). Двумя наиболее важными свойствами класса CollectionViewSource являются свойство View, которое упаковывает объект представления, и свойство Source, которое упаковывает источник данных. У него также имеются и дополнительные свойства SortDescriptions и GroupDescriptions, которые в точности повторяют имеющие такие же имена свойства представления, о которых уже рассказывалось. Когда класс CollectionViewSource создает представление, он просто передает значения этих свойств ему. Класс CollectionViewSource также включает событие Filter, которое можно использовать для выполнения фильтрации. Эта фильтрация работает точно таким же образом, как обратный вызов Filter, предоставляемый объектом представления, с тем лишь исключением, что она определяется в виде события, благодаря чему в XAML можно легко присоединять нужный обработчик событий. Например, возьмем предыдущий пример, в котором продукты разбивались на группы с помощью диапазонов цен. Ниже показано, как нужно было бы определить необходимые для этого примера конвертер и класс CollectionViewSource декларативным образом:
Обратите внимание, что класс SortDescription не относится ни к одному из пространств имен WPF. Чтобы его использовать, необходимо добавить следующий псевдоним пространства имен: xmlns:component="clr-namespace:System.ComponentModel;assembly=WindowsBase"
Создав объект CollectionViewSource можно сразу же привязаться к нему в списке:
Book_Pro_WPF-2.indb 557
19.05.2008 18:10:53
558
Глава 17
На первый взгляд это выглядит несколько странно. Кажется, будто бы элемент управления ListBox привязывается непосредственно к объекту CollectionViewSource, а не к предлагаемому им представлению (которое хранится в свойстве CollectionViewSource.View). Однако в модели привязки данных WPF объект CollectionViewSource является особым исключением. Когда он используется в выражении привязки, WPF просит его создать свое представление и затем привязывает это представление к соответствующему элементу. Декларативный подход на самом деле не экономит никаких усилий. Все равно нужно писать код для извлечения данных во время выполнения. Разница состоит лишь в том, что при таком подходе данные этом коде нужно передавать в объект CollectionViewSource, а не прямо в список: ICollection products = App.StoreDB.GetProducts(); CollectionViewSource viewSource = (CollectionViewSource) this.FindResource("GroupByRangeView"); viewSource.Source = products;
Альтернативный вариант — создать коллекцию продуктов как ресурс с помощью XAML-разметки, а затем привязать объект CollectionViewSource к этой коллекции декларативным образом. Однако при таком подходе тоже все равно придется писать код для заполнения коллекции продуктов. На заметку! Для создания привязок данных без кода некоторые пользуются сомнительными приемами. Иногда они определяют и заполняют коллекцию данных в XAML-разметке (с помощью жестко закодированных значений), а иногда просто скрывают код для заполнения объекта данных в конструкторе этого объекта. Оба этих подхода являются крайне непрактичными. Здесь о них было упомянуто исключительно из-за того, что их часто применяют для создания быстрых, импровизированных примеров привязки данных. Теперь, после рассмотрения подхода с использованием кода и подхода с разметкой для конфигурирования представления, вам наверняка интересно узнать, какой из них лучше с точки зрения проектирования. Оба являются вполне достойными. Выбор следует делать на основании того, где должны быть сосредоточены детали представления данных. Однако при желании использовать множество представлений, выбор подхода начинает играть более серьезную роль. В таком случае рекомендуется определять все представления в разметке, а для переключения на подходящее представление использовать код. Совет. Создавать множество представлений имеет смысл только в том случае, если эти представления сильно отличаются друг от друга (например, предполагают группирование по совершенно разным критериям). Во многих случаях гораздо проще будет просто изменить информацию о сортировке или группировании для текущего представления. Например, при желании расширить окно, показанное на рис. 17.13, так, чтобы иметь возможность создать большие и меньшие по размеру группы, наиболее эффективным подходом будет динамически изменить свойство PriceRangeProductGrouper.GroupInterval.
Навигация в представлении Одной из самых простых вещей, которые можно делать с объектом представления, является определение количества элементов в списке (осуществляемое с помощью свойства Count) и извлечение ссылки на текущий объект данных (CurrentItem) или индекс текущей позиции (CurrentPosition). Также еще доступен и ряд методов, позволяющих перемещаться от одной записи к другой, среди которых MoveCurrentToFirst(),
Book_Pro_WPF-2.indb 558
19.05.2008 18:10:53
Шаблоны данных, представления данных и поставщики данных
559
MoveCurrentToLast() , MoveCurrentToNext() , MoveCurrentToPrevious() и MoveCurrentToPosition(). Пока что эти детали были не нужны, поскольку во всех приводившихся примерах для предоставления пользователю возможности переходить от одной записи к следующей применялся список. Однако при желании создать приложение для просмотра записей, может потребоваться предоставить свои собственные навигационные кнопки. На рис. 17.4 показан пример такого приложения.
Рис. 17.14. Приложение для просмотра записей Привязанные текстовые поля, отображающие данные для привязанного продукта, остаются в том же виде. Нужно только, чтобы они указывали на подходящее свойство, как показано ниже: Model Number:
В этом примере, однако, никакого элемента управления списком нет, поэтому о возможностях навигации разработчик должен позаботиться сам. Чтобы упростить себе жизнь, он может сохранить ссылку на представление в виде переменной экземпляра в классе окна: private ListCollectionView view;
В таком случае код будет приводить представление к соответствующему типу (ListCollectionView), а не использовать интерфейс ICollectionView. Интерфейс ICollectionView предлагает многие те же самые функциональные возможности, но не предоставляет свойство Count, которое подсчитывает общее количество элементов в коллекции. При первой загрузке окна можно извлечь данные, разместить их в элементе DataContext окна и сохранить ссылку на представление: ICollection products = App.StoreDB.GetProducts(); this.DataContext = products; view = (ListCollectionView)CollectionViewSource.GetDefaultView(this.DataContext); view.CurrentChanged += new EventHandler(view_CurrentChanged);
Все необходимое для отображения коллекции элементов в окне делается во второй строке. В частности, в ней вся коллекция объектов Product размещается в объекте DataContext. Привязанные элементы управления в форме будут выполнять поиск
Book_Pro_WPF-2.indb 559
19.05.2008 18:10:53
560
Глава 17
вверх по дереву элементов до тех пор, пока не найдут этот объект. Конечно, нужно, чтобы выражения привязки привязывались к текущему элементу в коллекции, а не к самой коллекции, но WPF способна догадаться об этом самостоятельно и потому будет предоставлять их с текущим элементом автоматически, так что добавлять лишний код не потребуется. В предыдущем примере есть один дополнительный оператор. Он соединяет обработчик событий с событием CurrentChanged представления. При срабатывании этого события могут выполняться несколько полезных действий, например, включаться или отключаться кнопки перехода назад и вперед в зависимости от текущей позиции и отображаться информация о текущей позиции в TextBlock в нижней части окна. private void view_CurrentChanged(object sender, EventArgs e) { lblPosition.Text = "Record " + (view.CurrentPosition + 1).ToString() + " of " + view.Count.ToString(); cmdPrev.IsEnabled = view.CurrentPosition > 0; cmdNext.IsEnabled = view.CurrentPosition < view.Count - 1; }
Этот код кажется кандидатом на привязку данных и использование триггеров. Однако логика просто является слишком сложной (частично из-за необходимости добавлять в индекс 1 для получения номера позиции подлежащей отображению записи). Последний шаг — написать логику для кнопок перехода к предыдущей и следующей записи. Поскольку эти кнопки автоматически отключаются, когда они не применимы, волноваться о перемещении перед первым элементом или после последнего элемента не нужно. private void cmdNext_Click(object sender, RoutedEventArgs e) { view.MoveCurrentToNext(); } private void cmdPrev_Click(object sender, RoutedEventArgs e) { view.MoveCurrentToPrevious(); }
Для увеличения привлекательности можно добавить в эту форму элемент управления списком, так чтобы у пользователя была возможность как переходить по записям по очереди с помощью кнопок, так и перепрыгивать сразу на конкретный элемент с помощью списка (рис. 17.15). В данном случае необходим элемент ComboBox, использующий свойство ItemsSource (для извлечения полного списка продуктов) и применяющий привязку на свойстве Text (для отображения правильного элемента):
При первом извлечении коллекции продуктов привязывается список: lstProducts.ItemsSource = products;
Это может и не дать ожидаемого эффекта. По умолчанию элемент, выбираемый в
ItemsControl, не синхронизируется с текущим элементом в представлении. Это означает, что при выполнении нового выбора в списке пользователь будет не направляться к новой записи, а изменять свойство ModelName текущей записи. К счастью, существуют два простых способа решить эту проблему.
Book_Pro_WPF-2.indb 560
19.05.2008 18:10:53
561
Шаблоны данных, представления данных и поставщики данных
Рис. 17.15. Приложение для просмотра записей с выпадающим списком Первым способ является грубым и предполагает просто перемещение к новой записи при каждом выборе какого-либо элемента в списке. Необходимый для этого код выглядит так: private void lstProducts_SelectionChanged(object sender, RoutedEventArgs e) { view.MoveCurrentTo(lstProducts.SelectedItem); }
Более простой способ — установить для свойства ItemsControl.IsSynchronized WithCurrentItem значение true. В таком случае выбранный в текущий момент элемент будет автоматически синхронизироваться в соответствии с текущей позицией представления, без всякого кода.
Использование списка поиска для редактирования Элемент управления ComboBox предлагает удобный способ для редактирования значений записей. В текущем примере в этом нет особого смысла, ибо какой смысл может быть в присваивании одному продукту точно такого же имени, как и другому? Однако совсем не трудно представить другие сценарии, в которых элемент управления ComboBox будет прекрасным инструментом для редактирования. Например, в базе данных может присутствовать поле, принимающее одно из небольшого набора заданных значений. В таком случае будет очень удобно использовать элемент управления ComboBox, привязать его к соответствующему полю с помощью выражения привязки в свойстве Text и заполнить допустимыми значениями путем установки его свойства ItemsSource так, чтобы оно указывало на определенный список, а при желании, чтобы значения отображались в списке одним способом (например, в виде текста), но хранились другим (например, в виде числовых кодов) — просто добавить в привязку свойства Text соответствующий конвертер значений. Другим случаем, когда в списке поиска будет смысл, является наличие привязанных таблиц. Например, может возникнуть желание позволить пользователю выбирать категорию для продукта с помощью списка, отображающего все доступные категории. В таком случае, опять-таки, можно будет привязаться к подходящему полю с помощью свойства Text и заполнить список опциями с помощью свойства ItemsSource, а при необходимости, чтобы низкоуровневые уникальные идентификаторы преобразовывались в более понятные имена — использовать конвертер значений.
Book_Pro_WPF-2.indb 561
19.05.2008 18:10:53
562
Глава 17
Поставщики данных В большинстве рассмотренных до этого примерах высокоуровневый источник данных предоставлялся за счет программной установки свойства DataContext элемента или свойства ItemsSource спискового элемента управления. В принципе такой подход является наиболее гибким, особенно если объект данных создается другим классом (таким как StoreDB). Однако возможны и другие варианты. Как уже показывалось ранее в этой главе, объект данных может объявляться ресурсом окна (или какого-нибудь другого контейнера). Такой подход работает хорошо, если можно, чтобы объект создавался декларативным образом, но менее удобен, если требуется, чтобы во время выполнения устанавливалось соединение с каким-то внешним источником данных (например, базой данных). Однако некоторые разработчики все равно прибегают к нему (зачастую ради того, чтобы не писать код обработки событий). В целом он подразумевает просто создание объекта-упаковщика, извлекающего необходимые данные в конструкторе. Например, раздел ресурсов мог бы выглядеть так:
Здесь ProductListSource — это класс, который наследуется от Observable Collection. А это значит, что он может хранить список продуктов. Также у него в конструкторе имеется кое-какая базовая логика, вызывающая метод StoreDB. GetProducts(), который позволяет ему самостоятельно заполнять себя данными. Далее его можно было бы использовать в выражениях привязки других элементов: 1) { // Декодируем текст заметки. string base64Text = annotation.Cargos[1].Contents[0].InnerText; byte[] decoded = Convert.FromBase64String(base64Text); // Записываем декодированный текст в поток. MemoryStream m = new MemoryStream(decoded); // Используем поток StreamReader, преобразуем // байты текста в более полезную строку. StreamReader r = new StreamReader(m); string annotationXaml = r.ReadToEnd(); r.Close(); // Показываем содержимое аннотации. MessageBox.Show(annotationXaml); }
Этот код получает текстовую аннотацию, упакованную в элементе XAML . Дескриптор включает атрибуты, определяющие широкий диапазон типографических подробностей. Внутри элемента находятся множество элементов и .
Book_Pro_WPF-2.indb 655
19.05.2008 18:11:09
656
Глава 19
На заметку! Подобно текстовой аннотации, типографская (чернильная) аннотация тоже будет иметь коллекцию Cargos с более чем одним элементом. Однако в этом случае коллекция Cargos будет содержать типографские данные, а не декодируемый текст. Если предыдущий код использовать применительно к типографской аннотации, вы получите пустое окно сообщения. Таким образом, если ваш документ содержит текстовые и типографские аннотации, вам необходимо проверить свойство Annotation.AnnotationType, чтобы убедиться в том, что вы имеете дело с текстовой аннотацией, прежде чем использовать данный код. Если вы просто хотите получить текст без окружающего его кода XML, вы можете применить XamlReader, чтобы выполнить его преобразование (и не использовать StreamReader). XML можно преобразовать в объект Section посредством следующего кода: if (annotation.Cargos.Count > 1) { // Декодирование текста примечания. string base64Text = annotation.Cargos[1].Contents[0].InnerText; byte[] decoded = Convert.FromBase64String(base64Text); // Запись декодированного текста в поток. MemoryStream m = new MemoryStream(decoded); // Обратное преобразование XML в объект Section. Section section = XamlReader.Load(m) as Section; m.Close(); // Получение текста внутри объекта Section. TextRange range = new TextRange(section.ContentStart, section.ContentEnd); // Показать содержимое аннотации. MessageBox.Show(range.Text); }
Как показано в табл. 19.8, из аннотации можно извлечь не только сам текст. Можно без труда узнать, кто ее автор, когда она была создана и когда была изменена в последний раз. Вы можете также получить информацию о том, где именно в документе прикреплена аннотация. Однако для этой задачи коллекция Anchors не очень подходит, поскольку она предлагает низкоуровневую коллекцию объектов AnnotationResource, заключающих в себе дополнительные данные XML. Чтобы упросить задачу, .NET 3.5 предлагает метод GetAnchorInfo() в классе AnnotationHelper. Этот метод принимает аннотацию и возвращает объект, реализующий IAnchorInfo. IAnchorInfo anchorInfo = AnnotationHelper.GetAnchorInfo(service, annotation);
IAnchorInfo комбинирует AnnotationResource (свойство Anchor), аннотацию (Annotation) и объект, представляющий местоположение аннотации в дереве документа (ResolvedAnchor), что является более полезной информацией. Несмотря на то что свойство ResolvedAnchor имеет объектный тип, текстовые аннотации и выделения всегда возвращают TextAnchor. Объект TextAnchor описывает начальную точку прикрепленного текста (BoundingStart) и конечную точку (BoundingEnd). Ниже показано, как можно определить выделенный текст для аннотации посредством IAnchorInfo: IAnchorInfo anchorInfo = AnnotationHelper.GetAnchorInfo(service, annotation); TextAnchor resolvedAnchor = anchorInfo.ResolvedAnchor as TextAnchor; if (resolvedAnchor != null) { TextPointer startPointer = (TextPointer)resolvedAnchor.BoundingStart; TextPointer endPointer = (TextPointer)resolvedAnchor.BoundingEnd; TextRange range = new TextRange(startPointer, endPointer); MessageBox.Show(range.Text); }
Book_Pro_WPF-2.indb 656
19.05.2008 18:11:09
Документы
657
Вы можете также использовать объекты TextAnchor в качестве отправной точки для оставшегося дерева документа: // Прокручиваем документ, чтобы отобразить абзац с текстом аннотации. TextPointer textPointer = (TextPointer)resolvedAnchor.BoundingStart; textPointer.Paragraph.BringIntoView();
Среди примеров в данной главе есть один, в котором этот прием используется для создания списка аннотаций. Когда аннотация выделяется в списке, она автоматически отображается в документе. В каждом из этих случаев метод AnnotationHelper.GetAnchorInfo() позволяет переходить от аннотации к ее тексту, подобно тому как метод AnnotationStore. GetAnnotation() позволяет переходить от содержимого документа к аннотациям. Несмотря на относительную простоту проверки существующих аннотаций, средство аннотаций в WPF оказывается слабо пригодным, когда дело доходит до манипулирования этими аннотациями. Пользователю совсем несложно открыть клейкий листок, перетащить его в новое место, изменить текст и т.п., однако программным образом эти задачи решить непросто. В действительности, все свойства объекта Annotation доступны только для чтения. Не существует готовых и доступных способов изменения аннотации, поэтому процесс редактирования аннотации включает в себя удаление и повторное создание аннотации. Вы можете сделать это посредством методов AnnotationStore или AnnotationHelper (если аннотация прикреплена к выделенному в данный момент времени тексту). Тем не менее, оба подхода потребуют некоторых усилий с вашей стороны. Если вы используете AnnotationStore, вам необходимо создать объект Annotation вручную. Если вы используете AnnotationHelper, вам необходимо явным образом задать выбор текста, чтобы включить соответствующую порцию текста, прежде чем создавать аннотацию. Оба подхода являются утомительными и подвержены ошибкам.
Реагирование на изменения в аннотациях Вы уже знаете, как AnnotationStore позволяет извлекать аннотации в документе (посредством метода GetAnnotations()) и манипулировать ими (посредством методов DeleteAnnotation() и AddAnnotation()). AnnotationStore предлагает дополнительную возможность — он генерирует события, информирующие об изменениях в аннотациях. AnnotationStore предлагает четыре события: AnchorChanged (возникает при перемещении аннотации), AuthorChanged (возникает при изменении информации об авторе аннотации), CargoChanged (возникает, когда изменяются данные аннотации, включая ее текст) и StoreContentChanged (возникает, когда при создании, удалении или какомлибо изменении аннотации). Примеры для этой главы, доступные в оперативном режиме, содержат пример отслеживания аннотации. Обработчик события StoreContentChanged начинает работать, если производятся изменения в аннотации. Он получает всю информацию об аннотации (посредством метода GetAnnotations()), а затем отображает текст аннотации в списке. На заметку! События аннотации возникают после того, как в ней было произведено изменение. Это означает, что вы не сможете подключить специальную логику, расширяющую действие аннотации. Например, вам не удастся добавить оперативную информацию в аннотацию или выборочно отменить попытку пользователя отредактировать или удалить аннотацию.
Book_Pro_WPF-2.indb 657
19.05.2008 18:11:09
658
Глава 19
Сохранение аннотаций в фиксированном документе В предыдущих примерах использовались аннотации в потоковом документе. В этом сценарии аннотации можно сохранять для работы с ними в будущем, однако их нужно хранить отдельно — например, в отдельном файле XML. Для фиксированного документа вы можете применять тот же подход, но при этом у вас будет дополнительный вариант — хранить аннотации прямо в файле документа XPS. По сути, вы могли бы даже хранить множество наборов разных аннотаций, и все это в одном и том же документе. Вам нужно всего лишь использовать поддержку пакетов в пространстве имен System.IO.Packaging. Как было сказано ранее, каждый документ XPS на самом деле является архивом ZIP, который включает несколько файлов. Когда вы храните аннотации в документе XPS, то на самом деле вы создаете другой файл внутри архива ZIP. Первым делом необходимо выбрать URI для идентификации ваших аннотаций. Ниже показан пример, в котором используется имя AnnotationStream: Uri annotationUri = PackUriHelper.CreatePartUri( new Uri("AnnotationStream", UriKind.Relative));
Теперь нужно получить пакет Package для документа XPS с помощью статического метода PackageStore.GetPackage(): Package package = PackageStore.GetPackage(doc.Uri);
После этого вы можете создать часть пакета, в которой будут храниться ваши аннотации внутри документа XPS. Однако потребуется проверить, существует ли уже часть пакета аннотаций (если да, то нужно будет загрузить документ заранее и добавить в него аннотации). Если ее нет, можете создать ее сейчас: PackagePart annotationPart = null; if (package.PartExists(annotationUri)) { annotationPart = package.GetPart(annotationUri); } else { annotationPart = package.CreatePart(annotationUri, "Annotations/Stream"); }
На последнем этапе необходимо создать AnnotationStore , который будет содержать часть пакета аннотации, а затем уже привычным способом разрешить AnnotationService: AnnotationStore store = new XmlStreamStore(annotationPart.GetStream()); service = new AnnotationService(docViewer); service.Enable(store);
Чтобы эта технология могла работать, вы должны открыть файл XPS в режиме
FileMode.ReadWrite, а не в режиме FileMode.Read, чтобы аннотации можно было записывать в файл XPS. По той же причине вам нужно хранить документ XPS открытым, когда будет работать служба аннотирования. Вы можете закрыть документ XPS, когда будет закрыто окно (или открыть новый документ).
Настройка внешнего вида клейких листков Окна заметок, которые появляются при создании текстовой заметки или заметки, написанной от руки, являются экземплярами класса StickyNotControl, который оп-
Book_Pro_WPF-2.indb 658
19.05.2008 18:11:09
659
Документы
ределен в пространстве имен System.Windows.Controls. Как и со всеми элементами управления WPF, вы можете настраивать внешний вид StickyNoteControl с помощью средств настройки стилей или применяя новый шаблон элемента управления. Например, вы можете без труда создать стиль, который будет применяться ко всем экземплярам StickyNoteControl с помощью свойства Style.TargetType. Ниже показан пример, который дает каждому экземпляру StickyNoteControl новый цвет фона:
Чтобы сделать более динамичной версию StickyNoteControl, вы можете написать триггер стиля, который будет реагировать на свойство StickyNoteControl.IsActive, имеющее значение true, если клейкий листок находится в фокусе. Чтобы получить дополнительные возможности для управления, вы можете использовать совершенно другой шаблон элемента управления для StickyNoteControl. Единственная хитрость заключается в том, что шаблон StickyNoteControl варьируется, в зависимости от того, что он хранит: текстовую заметку или заметку, написанную от руки. Если вы разрешите пользователю создавать оба типа заметок, вам понадобится триггер, который будет выбирать один из двух шаблонов. Заметки, написанные от руки, должны включать InkCanvas, а текстовые заметки должны содержать RichTextBox. В обоих случаях этот элемент должен иметь имя PART_ContentControl. Ниже показан стиль, который применяет простейший шаблон элемента управления для заметок обоих типов. Он задает размерности окна заметок и выбирает подходящий шаблон на основе типа содержимого заметки.
Book_Pro_WPF-2.indb 659
19.05.2008 18:11:09
660
Глава 19
Резюме Многие разработчики уже знают о том, что WPF предлагает новую модель для рисования, компоновки и анимации. Однако ее богатые функции документов часто остаются незамеченными. В этой главе вы видели, как создаются потоковые документы, как компонуется текст внутри них самыми разными способами, и узнали, как осуществляется управление отображением текста в разных контейнерах. Мы говорили также о применении объектной модели FlowDocument для динамического изменения порций документа и рассмотрели элемент управления RichTextBox, который предлагает твердую основу для расширенных особенностей редактирования текста. В завершение главы была дана краткая информация о фиксированных документах и классе XpsDocument. Модель XPS предлагает основу для новой функции печати в WPF, которая является темой следующей главы.
Book_Pro_WPF-2.indb 660
19.05.2008 18:11:09
ГЛАВА
20
Печать В
озможности печати в WPF являются гораздо более мощными, чем были в Windows Forms. Задачи (вроде проверки очереди печати), которые невозможно было выполнять с помощью библиотек .NET и для которых требовалось использовать API-интерфейс Win32 или WMI, теперь полностью поддерживаются благодаря классам в новом пространстве имен System.Printing. Даже еще более замечательной является тщательно переделанная модель печати, в которой все кодирование организуется вокруг одного единственного компонента, а именно — класса PrintDialog в пространстве имен System.Windows.Controls. С помощью класса PrintDialog можно отображать диалоговое окно Print (Печать), позволяющее пользователю выбирать принтер и изменять его параметры настройки, а также отправлять элементы документы и низкоуровневые визуальные объекты прямо на принтер. В этой главе речь как раз и пойдет о том, как можно использовать класс PrintDialog для создания как следует масштабируемых и разбиваемых на страницы распечаток.
Основные сведения о печати Хотя WPF включает десятки связанных с печатью классов (большинство из которых находится в пространстве имен System.Printing), наилучшей отправной точкой, облегчающей жизнь разработчику, является класс PrintDialog. Класс PrintDialog упаковывает знакомое всем диалоговое окно Print, которое позволяет пользователю выбирать принтер и несколько других стандартных параметров печати, вроде количества копий (рис. 20.1). Однако класс PrintDialog — это больше чем просто симпатичное окно: у него также имеется встроенная возможность для активизации вывода данных на печать. Чтобы предоставить задание на печать с помощью класса PrintDialog, необходимо использовать один из описанных ниже методов.
• Метод PrintVisual(), который работает с любым из классов, унаследованных от System.Windows.Media.Visual, т.е. с любой рисуемой вручную графикой и любым размещаемым в окне элементом.
• Метод PrintDocument(), который работает с любым объектом DocumentPaginator, таким как DocumentPaginator , используемый для разбиения на страницы документа FlowDocument (или XpsDocument ), и любой специальный объект DocumentPaginator, создаваемый разработчиком для обработки его собственных данных. В следующих разделах будут рассматриваться различные стратегии, которыми можно пользоваться для создания выводимой на печать копии.
Book_Pro_WPF-2.indb 661
19.05.2008 18:11:09
662
Глава 20
Рис. 20.1. Окно PrintDialog в Windows Vista
Печать элемента Самый простой подход — воспользоваться моделью, которая уже применяется для экранной визуализации. С помощью метода PrintDialog.PrintVisual() любой элемент в окне (и все его дочерние элементы) можно отправлять прямо на принтер. Чтобы увидеть, как это работает, давайте рассмотрим окно, показанное на рис. 20.2. В нем содержится элемент управления Grid, в котором размещаются все остальные элементы. В самой верхней строке находится элемент Canvas, а в нем — рисунок, состоящий из элементов TextBlock и Path (который сам визуализируется в виде прямоугольника с отверстием посередине овальной формы).
Рис. 20.2. Простой рисунок Чтобы отправить объект Canvas на печать вместе со всеми содержащимися в нем элементами, можно использовать при выполнении щелчка на кнопке Print следующий фрагмент кода:
Book_Pro_WPF-2.indb 662
19.05.2008 18:11:09
Печать
663
PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { printDialog.PrintVisual(canvas, "A Simple Drawing"); }
Здесь сначала создается объект PrintDialog. Затем вызывается метод ShowDialog для отображения диалогового окна Print. Метод ShowDialog возвращает булевские значения true и false, а также значение null. Значение true означает, что пользователь щелкнул на кнопке OK, значение false — что он щелкнул на кнопке Cancel (Отмена), а null — что он закрыл диалоговое окно, так и не щелкнув ни на какой из этих кнопок. При вызове метода PrintVisual() передаются два аргумента. Первый — это название элемента, который требуется распечатать, а второй — строка, которая должна использоваться для идентификации задания на печать. Именно она и будет отображаться в очереди печати Windows (в столбце Document Name (Имя документа)). Когда печать выполняется подобным образом, возможности для управления выводом на печать являются довольно ограниченными. Элемент всегда выравнивается по левому верхнему углу страницы. Если он не включает ненулевых значений Margin, край содержимого может оказаться в непечатаемой области страницы, а это означает, что в распечатанном выводе его не будет. Отсутствие возможности управлять полями (Margin ) — это только первое ограничение, с которым доведется столкнуться в случае применения такого подхода. Он также не позволяет разбивать содержимое на страницы, если оно является слишком длинным, так что при наличии содержимого, занимающего больший объем, чем может уместиться на одной странице, какая-то его часть будет усечена внизу. И, наконец, он не предоставляет возможности для управления масштабированием, применяемым для визуализации задания на печать. Вместо этого WPF использует все ту же аппаратно-независимую систему визуализации на основе единиц, составляющих 1/96 дюйма. Например, если имеется прямоугольник шириной 96 единиц, он будет занимать один дюйм как на экране (при условии, что используется стандартный системный параметр экранного разрешения Windows, равный 96), так и на печатной странице. Зачастую это приводит к тому, что печатный вывод выглядит гораздо меньшим, чем требуется. На заметку! Очевидно, что WPF заполнит гораздо больше деталей на печатной странице, поскольку практически ни один принтер не поддерживает такого низкого разрешения, как 96 dpi (более распространенными разрешениями принтеров являются 600 и 1200 dpi). Однако размер содержимого в выводе на печать WPF все равно оставит таким же, как и на мониторе. На рис. 20.3 показано, как на распечатанной странице будет выглядеть элемент Canvas из окна, которое было показано на рис. 20.2. Рис. 20.3. Распечатанный элемент
Book_Pro_WPF-2.indb 663
19.05.2008 18:11:10
664
Глава 20
Особенности класса PrintDialog Класс PrintDialog является оболочкой для низкоуровневого внутреннего класса .NET по имени Win32PrintDialog, который, в свою очередь, упаковывает диалоговое окно Print, предоставляемое Win32 API. К сожалению, эти дополнительные уровни немного снижают степень гибкости. Одной из потенциальных проблем является способ, которым класс PrintDialog работает с модальными окнами. Где-то глубоко в недоступном коде Win32PrintDialog зарыта логика, которая всегда делает диалоговое окно Print модальным по отношению к главному окну приложения. Это приводит к возникновению необычной проблемы, когда модальное окно отображается из главного окна и затем из него вызывается метод PrintDialog.ShowDialog(). Диалоговое окно Print оказывается модальным по отношению не ко второму окну, как ожидается, а по отношению к главному окну, а это означает, что пользователь может возвращаться ко второму окну и взаимодействовать с ним (даже щелкать на кнопке Print (Печать) и тем самым отображать множество экземпляров диалогового окна Print)! Для решения этой проблемы существует одно несколько громоздкое решение — вручную изменить главное окно приложения на текущее окно перед вызовом метода PrintDialog.ShowDialog() и затем сразу же после этого переключить его обратно. Со способом, по которому работает класс PrintDialog, также связано еще одно ограничение. Поскольку распечатываемым содержимым владеет основной поток приложения, выполнение печати в фоновом потоке является невозможным. Это представляет проблему, если используется логика печати, занимающая определенное время. Доступно два решения. Можно создать визуальные объекты, которые должны распечатываться в фоновом потоке (а не извлекать их существующего окна), и тогда выполнение печати в фоновом потоке станет возможным. Однако более простым решением будет применение диалогового окна PrintDialog, позволяющего пользователю указывать параметры печати, а затем для фактической распечатки содержимого использование методов печати не класса PrintDialog, а класса XpsDocumentWriter. Класс XpsDocumentWriter включает возможность для отправки содержимого на принтер асинхронным образом, и более подробно будет описываться далее в этой главе, в разделе “Печать через XPS”.
Трансформирование распечатываемого вывода В главе 13 рассказывалось, что к свойству RenderTransform или LayoutTransform любого элемента можно присоединять объект Transform для изменения способа его визуализации. Объекты Transform могли бы решить проблему с негибким распечатываемым выводом, поскольку их можно было бы использовать для изменения размера элемента (ScaleTransform), его перемещения по странице (TranslateTransform) или и того и другого вместе (TransformGroup). К сожалению, визуальные объекты способны размещать себя только одним способом за раз. Это означает, что отмасштабировать элемент одним способом в окне и другим в распечатке невозможно — вместо этого все примененные объекты Transform будут изменять внешний вид элемента как на печатной странице, так и на экране. При отсутствии боязни перед не совсем аккуратным компромиссом можно обойти эту проблему несколькими способами. Главная идея состоит в применении объектов Transform непосредственно перед созданием распечатки и последующем их удалении. Чтобы предотвратить появление в окне измененного в размерах элемента, можно временно скрыть его. Может показаться, что скрыть элемент можно путем изменения соответствующим образом его свойства Visibility, но это приведет к сокрытию элемента как в окне, так и в распечатке, что однозначно не является подходящим вариантом. Одно из возможных решений — изменить свойство Visibility родительского элемента (каковым в данном случае является отвечающий за компоновку элемент управления Grid). Такой
Book_Pro_WPF-2.indb 664
19.05.2008 18:11:10
Печать
665
подход сработает, поскольку метод PrintVisual() принимает в расчет только указанный элемент и его потомков, но никак не детали родителя. Ниже приведен код, который иллюстрирует все выше сказанное и распечатывает элемент Canvas из рис. 20.2 в пять раз большим размером в ширину и высоту: PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { // Скрываем сетку (элемент управления Grid). grid.Visibility = Visibility.Hidden; // Увеличиваем масштаб вывода в 5 раз. canvas.LayoutTransform = new ScaleTransform(5, 5); // Распечатываем элемент. printDialog.PrintVisual(canvas, "A Scaled Drawing"); // Удаляем трансформацию и делаем элемент снова видимым. canvas.LayoutTransform = null; grid.Visibility = Visibility.Visible; }
В этом примере не хватает одной детали. Хотя элемент Canvas (и его содержимое) и растягивается, он все равно использует информацию о компоновке из содержащего его элемента управления Grid. Другими словами, элемент Canvas по-прежнему считает, что у него для работы имеется пространство в объеме, равном размеру ячейки Grid, в которой он находится. В данном примере подобное упущение не представляет проблемы, поскольку элемент Canvas не ограничивает себя доступным пространством (в отличие от ряда других контейнеров). Однако проблема обязательно возникнет в случае наличия текста и необходимости упаковать его так, чтобы он не выходил за границы печатной страницы, или в случае использования для элемента Canvas фона (который в текущем примере будет занимать в ячейке Grid меньше места, чем вся область позади элемента Canvas). Решение выглядит просто. Нужно всего лишь после установки LayoutTransform (но перед распечаткой элемента Canvas) вручную инициировать процесс компоновки c помощью методов Measure() и Arrange(), которые каждый элемент наследует от класса UIElement. Секрет заключается в том, что при вызове этих методов будет передаваться размер страницы, благодаря чему элемент Canvas сможет растягиваться так, чтобы не выходить за ее границы. (Кстати, по этой же причине устанавливать следует не свойство RenderTransform, а свойство LayoutTransform, поскольку нужно, чтобы новый расширенный размер учитывался в компоновке.) Извлечь размер страницы можно из свойств PrintableAreaWidth и PrintableAreaHeight. На заметку! Судя по именам свойств PrintableAreaWidth и PrintableAreaHeight, будет вполне логично предположить, что они отражают печатную область страницы — другими словами, ту часть страницы, на которой принтер действительно может печатать. (Многие принтеры не способы печатать до самых краев, обычно из-за того, что именно там ролики захватывают страницу.) Но на самом деле свойства PrintableAreaWidth и PrintableAreaHeight просто возвращают значения всей ширины и высоты страницы в аппаратно-независимых единицах. Для листа бумаги размером 8,5×11 дюймов эти значения будут выглядеть как 816 и 1056. (Попробуйте разделить эти числа на 96 dpi и вы получите полный размер бумаги.) Следующий пример демонстрирует, как следует использовать свойства
PrintableAreaWidth и PrintableAreaHeight . Для придания большей привлекательности в нем 10 единиц (т.е. около 0,1 дюйма) оставляется для границы по краям страницы.
Book_Pro_WPF-2.indb 665
19.05.2008 18:11:10
666
Глава 20
PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { // Скрываем сетку (элемент управления Grid). grid.Visibility = Visibility.Hidden; // Увеличиваем масштаб вывода в 5 раз. canvas.LayoutTransform = new ScaleTransform(5, 5); // Задаем поле. int pageMargin = 5; // Извлекаем размер страницы. Size pageSize = new Size(printDialog.PrintableAreaWidth — pageMargin * 2, printDialog.PrintableAreaHeight - 20); // Инициируем установку размеров элемента. canvas.Measure(pageSize); canvas.Arrange(new Rect(pageMargin, pageMargin, pageSize.Width, pageSize.Height)); // Распечатываем элемент. printDialog.PrintVisual(canvas, "A Scaled Drawing"); // Удаляем трансформацию и делаем элемент снова видимым. canvas.LayoutTransform = null; grid.Visibility = Visibility.Visible; }
В конечном итоге получился способ для распечатки любого элемента и его масштабирования в соответствии с существующими потребностями (полностраничная распечатка показана на рис. 20.4). Такой подход работает довольно хорошо, но держится на (несколько неаккуратном) связующем звене.
Рис. 20.4. Отмасштабированный распечатанный элемент
Book_Pro_WPF-2.indb 666
19.05.2008 18:11:10
Печать
667
Печать элементов без их отображения Из-за того, что способ, которым данные должны отображаться в приложении, и способ, которым они должны отображаться в распечатке, часто отличаются, иногда бывает удобнее создать визуальный объект программно (чем использовать тот, который появляется в существующем окне). Например, ниже показан код, который создает сохраняемый в памяти объект TextBlock, заполняет его текстом, устанавливает перенос для текста, задает его размер так, чтобы он не выходил за границы печатной страницы, и затем распечатывает его. PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { // Создаем текст. Run run = new Run("This is a test of the printing functionality " + "in the Windows Presentation Foundation."); // Упаковываем его в TextBlock. TextBlock visual = new TextBlock(); TextBlock.Inlines.Add(run); // Используем поле для получения границы страницы. visual.Margin = new Thickness(15); // Разрешаем перенос текста для соответствия ширине страницы. visual.TextWrapping = TextWrapping.Wrap; // Увеличиваем масштаб элемента TextBlock в обоих направлениях в 5 раз. // (В данном случае увеличение шрифта дало бы тот же самый эффект, // поскольку TextBlock является единственным элементом.) visual.LayoutTransform = new ScaleTransform(5, 5); // Устанавливаем размер элемента. Size pageSize = new Size(printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight); visual.Measure(pageSize); visual.Arrange(new Rect(0,0, pageSize.Width, pageSize.Height)); // Распечатываем элемент. printDialog.PrintVisual(visual, "A Scaled Drawing"); }
На рис. 20.5 показана печатная страница, которую создает этот код. Такой подход позволяет брать необходимое содержимое из окна, но настраивать его внешний вид при печати отдельно. Однако он абсолютно бесполезен при наличии содержимого, которое должно занимать более одной страницы (в случае чего придется использовать технологии печати, описываемые в следующих разделах).
Печать документа Метод PrintVisual() , пожалуй, является самым универсальным методом печати, но класс PrintDialog также включает и другую опцию. Для печати содержимого из потокового документа можно использовать метод PrintDocument(). Преимущество такого подхода состоит в том, что потоковый документ может иметь дело с огромными объемами слож-
Book_Pro_WPF-2.indb 667
Рис. 20.5. Перенос текста с помощью элемента TextBlock
19.05.2008 18:11:10
668
Глава 20
ного содержимого и разбивать это содержимое на множество страниц (точно так же, как и на экране). Возможно, вы думаете, что метод PrintDialog.PrintDocument() требует объекта FlowDocument, однако на самом деле он принимает не этот объект, а DocumentPaginator. Объект DocumentPaginator представляет собой специализированный класс, роль которого заключается в том, чтобы принимать содержимое, разбивать его на множество страниц и предоставлять эти страницы по запросу. Каждая страница имеет вид объекта DocumentPage, который в действительности является всего лишь упаковщиком одного, немного приукрашенного объекта Visual. В классе DocumentPage просто доступно еще три дополнительных свойства: Size, которое возвращает размер страницы, ContentBox, отображающее размер поля, в котором размещается содержимое на странице после добавления полей, и BleedBox, представляющее область, которая находится за пределами границ страницы и в которой появляются печатаемые на краях страницы блоки, метки совмещения и ограничительные метки. Это означает, что метод PrintDocument() работает во многом подобно методу PrintVisual(). Единственное отличие состоит в том, что он печатает несколько визуальных объектов — по одному для каждой страницы. На заметку! Хотя разбивать содержимое на отдельные страницы можно и без помощи DocumentPaginator, за счет выполнения повторных вызовов метода PrintVisual(), это не очень хороший подход, поскольку в таком случае каждая страница будет восприниматься как отдельное задание на печать. Так как же можно получить объект DocumentPaginator для FlowDocument? Необходимо привести FlowDocument к типу IDocumentPaginatorSource и затем использовать свойство DocumentPaginator. Ниже приводится соответствующий пример: PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { printDialog.PrintDocument( ((IDocumentPaginatorSource)docReader.Document).DocumentPaginator, "A Flow Document"); }
Этот код может дать, а может и не дать желаемый результат, что зависит от контейнера, в котором в текущий момент находится документ. Если документ находится в памяти (а не в окне) или если он хранится в элементе управления RichTextBox или FlowDocumentScrollViewer, этот код будет работать нормально и в конечном итоге даст многостраничную распечатку с двумя столбцами (на стандартном листе бумаги размером 8,5×11 дюймов с книжной ориентацией). Точно такого же результата можно добиться и с помощью команды ApplicationCommands.Print. На заметку! Как рассказывалось в главе 10, некоторые элементы управления включают встроенную поддержку команд. Контейнеры FlowDocument (вроде применяемого здесь FlowDocument ScrollViewer) как раз являются одним из примеров таких элементов. Для выполнения базовой распечатки они пользуются командой Command.Print. Этот жестко закодированный код печати похож на тот, что был приведен выше, но только в нем используется объект XpsDocumentWriter, о котором более подробно будет рассказываться далее в этой главе, в разделе “Печать через XPS”. Однако если документ хранится в элементе FlowDocumentPageViewer или FlowDocumentReader, результат уже не будет выглядеть так же хорошо. В таком случае документ будет разбиваться на страницы в соответствии с текущим представлением в
Book_Pro_WPF-2.indb 668
19.05.2008 18:11:11
Печать
669
контейнере. То есть, если для того, чтобы уместить содержимое в текущем окне, потребуется 24 страницы, распечатаны будут все 24 страницы, каждая с небольшим содержащим данные окном. Решение является несколько громоздким, но зато работает. (По сути, это то же самое решение, которым пользуется команда ApplicationCommands.Print.) Секрет заключается в принуждении объекта FlowDocument самостоятельно разбивать содержимое на страницы для печати на принтере. Заставить его делать это можно путем установки для его свойств FlowDocument.PageHeight и FlowDocument.PageWidth значений границ страницы вместо значений границ контейнера. (В контейнерах вроде FlowDocumentScrollViewer значения для этих свойств не устанавливаются, поскольку разбиение на страницы не применяется. Именно поэтому функция печати здесь и работает без сучка и задоринки — разбиение на страницы выполняется автоматически при создании вывода на печать.) FlowDocument doc = docReader.Document; doc.PageHeight = printDialog.PrintableAreaHeight; doc.PageWidth = printDialog.PrintableAreaWidth; printDialog.PrintDocument( ((IDocumentPaginatorSource)doc).DocumentPaginator, "A Flow Document");
Также не помешает установить подходящие значения для свойств ColumnWidth и ColumnGap, чтобы получить именно столько столбцов, сколько требуется. В противном случае количество столбцов будет таким же, как и в текущем окне. Единственная проблема такого подхода состоит в том, что после изменения этих свойств они будут применяться к контейнеру, который отображает документ. В результате получится несколько сжатая версия документа, возможно, даже слишком маленькая для прочтения в текущем окне. Правильным решением будет учесть это, сохранив эти значения, изменив и затем снова восстановив исходные значения. Ниже показан весь код, распечатывающий вывод с двумя столбцами и приличными полями (добавленными и через свойство FlowDocument.PagePadding). PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { FlowDocument doc = docReader.Document; // Сохраняем все существующие параметры. double pageHeight = doc.PageHeight; double pageWidth = doc.PageWidth; Thickness pagePadding = doc.PagePadding; double columnGap = doc.ColumnGap; double columnWidth = doc.ColumnWidth; // Делаем так, чтобы страница FlowDocument соответствовала печатной странице. doc.PageHeight = printDialog.PrintableAreaHeight; doc.PageWidth = printDialog.PrintableAreaWidth; doc.PagePadding = new Thickness(50); // Используем два столбца. doc.ColumnGap = 25; doc.ColumnWidth = (doc.PageWidth - doc.ColumnGap - doc.PagePadding.Left - doc.PagePadding.Right) / 2; printDialog.PrintDocument( ((IDocumentPaginatorSource)doc).DocumentPaginator, "A Flow Document"); // Снова применяем прежние параметры. doc.PageHeight = pageHeight; doc.PageWidth = pageWidth; doc.PagePadding = pagePadding; doc.ColumnGap = columnGap; doc.ColumnWidth = columnWidth; }
Book_Pro_WPF-2.indb 669
19.05.2008 18:11:11
670
Глава 20
С этим подходом связано несколько ограничений. Помимо возможности настраивать свойства, отвечающие за поля и количество столбцов, никаких особых возможностей для управления больше нет. Конечно, объект FlowDocument можно изменять программно (например, временно увеличивая значение его свойства FontSize), но корректировать распечатываемый вывод, добавляя в него детали вроде номеров страниц, нельзя. Один из способов, которым можно обойти это ограничение, будет рассматриваться в следующем разделе.
Печать аннотаций WPF включает два класса, которые наследуются от DocumentPaginator. Класс FlowDocumentPaginator разбивает на страницы потоковые документы — именно его получает разработчик при проверке свойства FlowDocument.DocumentPaginator. Подобным образом класс FixedDocumentPaginator разбивает на страницы документы XPS и автоматически используется классом XpsDocument. Однако оба эти класса являются внутренними и не доступны в создаваемом разработчиком коде. Вместо этого с ними можно взаимодействовать с помощью членов базового класса DocumentPaginator. WPF включает только один конкретный общедоступный класс типа Paginator, который называется AnnotationDocumentPaginator и применяется для печати документов вместе со связанными с ним аннотациями. (Об аннотациях рассказывалось в главе 19.) Класс AnnotationD ocumentPaginator является общедоступным, так что при необходимости его можно создать для инициирования распечатки аннотированного документа. Чтобы использовать AnnotationDocumentPaginator, потребуется упаковать существующий DocumentPaginator в новый объект AnnotationDocumentPaginator. Сделать это можно, просто создав AnnotationDocumentPaginator и передав две ссылки: одну, указывающую на исходный класс Paginator документа, и вторую, указывающую на хранилище, в котором содержатся все аннотации. Пример показан ниже:
// Извлекаем обычный класс Paginator. FlowDocument doc = ((IDocumentPaginatorSource)doc).DocumentPaginator; // Извлекаем (работающую в текущий момент) службу // аннотаций для конкретного контейнера документа. AnnotationService service = AnnotationService.GetService(docViewer); // Создаем класс AnnotationDocumentPaginator. AnnotationDocumentPaginator paginator = new AnnotationDocumentPaginator(doc, service.Store); Далее документ можно будет распечатать с наложенными поверх аннотациями (в их текущем свернутом или развернутом состоянии), вызвав метод PrintDialog.PrintDocument() и передав ему в качестве аргумента объект AnnotationDocumentPaginator.
Манипулирование страницами в распечатке документа Немного больше возможностей для управления способом распечатки документа
FlowDocument можно получить, создав свой собственный класс DocumentPaginator. По имени этого класса не трудно догадаться, что он разбивает содержимое документа на отдельные страницы для печати (или отображения в постраничном средстве просмотра документов типа FlowDocument). Он отвечает за возвращение общего количества страниц на основе установленного для страниц размера и предоставлении скомпонованного содержимого для каждой страницы в виде объекта DocumentPage. Создаваемый самостоятельно класс DocumentPaginator вовсе необязательно должен быть сложным — на самом деле, он может вообще просто упаковывать класс
Book_Pro_WPF-2.indb 670
19.05.2008 18:11:11
Печать
671
DocumentPaginator, предоставляемый FlowDocument, и позволять ему делать всю рутинную работу по разбиению текста на отдельные страницы. Однако его, в отличие от стандартного класса DocumentPaginator, можно использовать для внесения небольших изменений наподобие добавления верхнего и нижнего колонтитула. В целом секрет состоит в перехватывании каждого выполняемого PrintDialog запроса на страницу и изменении этой страницы перед ее передачей в место назначения. Первое, что нужно сделать при таком подходе — это создать класс HeaderedFlow DocumentPaginator , унаследованный от класса DocumentPaginator . Поскольку DocumentPaginator является абстрактным классом, класс HeaderedFlowDocument Paginator должен реализовать несколько методов. Однако он (т.е. HeaderedFlow DocumentPaginator) может и возложить большую часть работы на стандартный класс DocumentPaginator, предоставляемый FlowDocument. Ниже показано, как может выглядеть базовая структура класса HeaderedFlowDocument Paginator. public class HeaderedFlowDocumentPaginator : DocumentPaginator { // Реальный разделитель на страницы (который выполняет // всю работу по разбивке содержимого на страницы). private DocumentPaginator flowDocumentPaginator; // Сохраняем класс FlowDocumentPaginator данного документа. public HeaderedFlowDocumentPaginator(FlowDocument document) { flowDocumentPaginator = ((IDocumentPaginatorSource)document).DocumentPaginator; } public override bool IsPageCountValid { get { return flowDocumentPaginator.IsPageCountValid; } } public override int PageCount { get { return flowDocumentPaginator.PageCount; } } public override Size PageSize { get { return flowDocumentPaginator.PageSize; } set { flowDocumentPaginator.PageSize = value; } } public override IDocumentPaginatorSource Source { get { return flowDocumentPaginator.Source; } } public override DocumentPage GetPage(int pageNumber) { ... } }
Поскольку класс HeaderedFlowDocumentPaginator передает свою работу приватному классу DocumentPaginator, в этом коде не видно, как работают свойства PageSize, PageCount и IsPageCountValid. Свойство PageSize устанавливается потребителем DocumentPaginator (т.е. кодом, использующим DocumentPaginator). Это свойство указывает DocumentPaginator, сколько пространства доступно в каждой печатной (или отображаемой на экране) странице. Свойства PageCount и IsPageCountValid предоставляются потребителю DocumentPaginator и сообщают о результатах разбиения на страницы. При каждом изменении значения свойства PageSize , класс DocumentPaginator будет заново вычислять размер каждой страницы. (Чуть позже в
Book_Pro_WPF-2.indb 671
19.05.2008 18:11:11
672
Глава 20
этой главе будет представлена более полная версия DocumentPaginator, созданная с нуля и включающая детали реализации этих свойств.) Метод GetPage() представляет собой то место, с которого начинается действие. В частности, данный код сначала вызывает метод GetPage() реального DocumentPaginator и только после этого приступает к работе над страницей. Главное, что нужно сделать — это извлечь объект Visual из страницы и поместить его в новый объект ContainerVisual. Затем можно добавить в этот объект ContainetVisual любой необходимый текст и, наконец, создать новый класс DocumentPage, упаковывающий его и новый вставленный в него заголовок. На заметку! В этом коде используется программирование визуального слоя (о котором рассказывалось в главе 14). Именно поэтому и необходим какой-то способ для создания визуальных объектов, которые бы представляли распечатываемый вывод. Вдаваться во все связанные с этим накладные расходы вроде обработки событий, свойств зависимостей и других низкоуровневых деталей, не нужно. Для специальных процедур печати (о которых пойдет речь в следующем разделе) практически всегда будет достаточно только программирования на уровне визуальных объектов и классы ContainerVisual, DrawingVisual и DrawingContext. Ниже показан полный код. public override DocumentPage GetPage(int pageNumber) { // Извлекаем запрашиваемую страницу. DocumentPage page = flowDocumentPaginator.GetPage(pageNumber); // Упаковываем страницу в объекте Visual, что позволит затем применять // к ней различные трансформации и добавлять другие элементы. ContainerVisual newVisual = new ContainerVisual(); newVisual.Children.Add(page.Visual); // Создаем заголовок. DrawingVisual header = new DrawingVisual(); using (DrawingContext dc = header.RenderOpen()) { Typeface typeface = new Typeface("Times New Roman"); FormattedText text = new FormattedText("Page " + (pageNumber + 1).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, Brushes.Black); // Оставляем между краем страницы и этим текстом пространство в четверть дюйма. dc.DrawText(text, new Point(96*0.25, 96*0.25)); } // Добавляем к объекту Visual заголовок. newVisual.Children.Add(header); // Упаковываем объект Visual в новой странице. DocumentPage newPage = new DocumentPage(newVisual); return newPage; }
В этой реализации предполагается, что размер страницы не изменяется из-за добавления заголовка и что в поле (Margin), наоборот, имеется достаточно свободного пространства для его размещения. В случае использования данного кода с небольшим значением Margin, заголовок будет печататься поверх содержимого документа. Точно таким же образом заголовки работают и в программах вроде Microsoft Word. Заголовки не считаются частью основного документа и потому позиционируются отдельно от его содержимого. Здесь имеется одна небольшая проблема. Пока объект Visual для страницы отображается в окне, добавить его в контейнер ContainerVisual не получится. Обойти эту
Book_Pro_WPF-2.indb 672
19.05.2008 18:11:11
673
Печать
проблему можно, временно удалив его из контейнера, выполнив печать, а затем вернув его обратно. FlowDocument document = docReader.Document; docReader.Document = null; HeaderedFlowDocumentPaginator paginator = new HeaderedFlowDocumentPaginator(document); printDialog.PrintDocument(paginator, "A Headered Flow Document"); docReader.Document = document;
Класс HeaderedFlowDocumentPaginator применяется для выполнения печати, но к FlowDocument не присоединяется, так что влиять на способ отображения документа на экране он не будет.
Специальная печать К этому моменту вся основная “правда” о печати в WPF уже должна быть ясна. С помощью описанных в предыдущем разделе хоть и недостаточно изящных, но зато быстрых приемов можно отправлять содержимое из окна на принтер и даже немного настраивать его. Но при желании создать для приложения высококлассную функцию печати, ее придется разрабатывать самостоятельно.
Печать с помощью классов уровня визуальных объектов Наилучший способ создать специальную распечатку — это использовать классы уровня визуальных объектов. Особенно полезными из них являются два описанных ниже класса.
• Класс ContainerVisual, который представляет собой упрощенный визуальный объект, способный удерживать (в своей коллекции Children) один и более других объектов Visual.
• Класс DrawingVisual , который наследуется от класса ContainerVisual и дополнительно предлагает метод RenderOpen() и свойство Drawing . Метод RenderOpen() создает объект DrawingContext, который можно использовать для прорисовывания в визуальном объекте какого-нибудь содержимого (например, текста, фигур и тому подобного), а свойство Drawing позволяет извлекать конечный продукт в виде объекта DrawingGroup. Для тех, кто знает, как используются эти классы, процесс создания специальной распечатки покажется довольно простым. 1. Создать свой класс DrawingVisual . (Можно также создать и объект ContainerVisual, что делается реже и, как правило, при желании объединить на одной и той же странице более чем один отдельный, прорисовываемый объект DrawingVisual.) 2. Вызвать метод DrawingVisual.RenderOpen() , чтобы извлечь объект DrawingContext. 3. Использовать методы объекта DrawingContext для создания своего вывода. 4. Закрыть объект DrawingContext. (Если объект DrawingContext был упакован в блоке using, этот шаг будет выполнен автоматически.) 5. С помощью метода PrintDialog.PrintVisual() отправить свой визуальный объект на принтер.
Book_Pro_WPF-2.indb 673
19.05.2008 18:11:11
674
Глава 20
Этот подход не только обеспечивает большую гибкость, чем продемонстрированный до этого подход с печатью элемента, но также и снижает накладные расходы. Очевидно, что главным для выполнения этой работы является знание того, какие методы предлагает класс DrawingContext для создания вывода. Все методы, которые можно использовать, описаны в табл. 20.1. Методы PushXxx() особенно интересны, поскольку они применяют параметры, которые будут действовать в будущих операциях рисования. Метод Pop() можно применять для отмены самого последнего метода PushXxx(). Если вызывается несколько методов PushXxx(), с помощью последующих вызовов метода Pop() можно будет отменить их все по очереди.
Таблица 20.1. Методы класса DrawingContext Имя
Описание
DrawLine(), DrawRectangle(), Рисуют указанную фигуру в указанной точке с заданными заDrawRoundedRectangle() и ливкой и контуром. Эти методы позволяют рисовать фигуры, DrawEllipse() описывавшиеся в главе 13. DrawGeometry() и DrawDrawing()
Рисуют более сложные объекты Geometry и Drawing. Об этих объектах рассказывалось в главе 14.
DrawText()
Рисует текст в указанном месте. Текст, шрифт, заливка и другие детали указываются путем передачи этому методу объекта FormattedText. Этот метод можно использовать для рисования заворачиваемого текста при условии установки свойства FormattedText.MaxTextWidth.
DrawImage()
Рисует растровое изображение в указанной области (определенной классом Rect).
Pop()
Отменяет последний вызывавшийся метод PushXxx(). Метод PushXxx() применяется для временного применения одного или более эффектов, а метод Pop() — для их отмены.
PushClip()
Ограничивает рисование определенной областью отсечения. Содержимое, которое выходит за рамки этой области, не прорисовывается.
PushEffect()
Применяет BitmapEffect к последующим операциям рисования.
PushOpacity()
Применяет новый параметр непрозрачности для того, чтобы сделать последующие операции рисования частично прозрачными.
PushTransform()
Устанавливает объект Transform, который будет применяться к последующим операциям рисования. Объект Transform можно применять для масштабирования, перемещения, переворачивания и наклона содержимого.
И это все ингредиенты, которые необходимы для создания приличного распечатываемого вывода (конечно, помимо умеренного количества математических подсчетов, необходимых для вычисления оптимального места для размещения всего содержимого). Ниже приведен код, в котором данный подход применяется для размещения по центру страницы блока отформатированного текста и добавления вокруг этой страницы границы. PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { // Создаем для страницы объект Visual. DrawingVisual visual = new DrawingVisual();
20_Pro-WPF2.indd 674
20.05.2008 16:31:10
Печать
675
// Извлекаем контекст рисования. using (DrawingContext dc = visual.RenderOpen()) { // Определяем текст, который нужно напечатать. FormattedText text = new FormattedText(txtContent.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Calibri"), 20, Brushes.Black); // Выбираем максимальную ширину, чтобы использовать перенос текста. text.MaxTextWidth = printDialog.PrintableAreaWidth / 2; // Извлекаем требующийся для текста размер. Size textSize = new Size(text.Width, text.Height); // Отыскиваем левый верхний угол, где должен размещаться текст. double margin = 96*0.25; Point point = new Point( (printDialog.PrintableAreaWidth - textSize.Width) / 2 - margin, (printDialog.PrintableAreaHeight - textSize.Height) / 2 - margin); // Прорисовываем содержимое. dc.DrawText(text, point); // Добавляем границу (прямоугольник без фона). dc.DrawRectangle(null, new Pen(Brushes.Black, 1), new Rect(margin, margin, printDialog.PrintableAreaWidth - margin * 2, printDialog.PrintableAreaHeight - margin * 2)); } // Печатаем визуальный объект. printDialog.PrintVisual(visual, "A Custom-Printed Page"); }
Совет. Этот код можно улучшить, переместив логику рисования в отдельный класс (например, в класс документа, который упаковывает предназначенное для печати содержимое), а затем вызвав в этом классе метод для извлечения визуального объекта и передав этот объект методу PrintVisual() в той части кода окна, которая отвечает за обработку событий. На рис. 20.6 показано, как будет выглядеть вывод.
Специальная печать с использованием множества страниц Визуальный объект не может выполнять разбивку на страницы. При желании создать многостраничную распечатку необходимо использовать тот же самый класс, что и при печати документа FlowDocument — DocumentPaginator . Разница состоит лишь в том, что в этом случае его нужно создавать самостоятельно, с нуля, без приватного класса DocumentPaginator внутри, отвечающего за выполнение всей самой сложной работы. В реализации базового дизайна класса DocumentPaginator нет ничего сложного. Все, что потребуется — это добавить метод, разбивающий содержимое на страницы, и сохранить
Book_Pro_WPF-2.indb 675
Рис. 20.6. Специальная распечатка
19.05.2008 18:11:12
676
Глава 20
информацию об этих страницах где-нибудь внутри, а затем просто реагировать на метод GetPage(), предоставляя ту страницу, которая необходима PrintDialog. Каждая страница должна генерироваться классом DrawingVisual, но сам этот класс должен помещаться в оболочку класса DocumentPage. Наиболее сложной частью является разбиение содержимого на страницы. Здесь WPF не предлагает никаких чудесных функций — разработчик должен сам решать, как лучше разбить содержимое. Какое-то содержимое легко поддается разбиению (например, как длинная таблица, которая будет демонстрироваться в следующем примере), а какоето в этом плане является очень проблематичным. Например, если необходимо напечатать длинный текстовый документ, потребуется передвигать слово за словом по всему тексту, добавляя слова в строки, а строки в страницы, и замерять каждый отдельный фрагмент текста для того, чтобы узнать, уместится он в строке или нет. И это только для разбиения текста с обычным выравниванием по левому краю — при желании добиться оптимального выравнивания, подобного тому, что применяется для документов FlowDocument, лучше будет воспользоваться методом PrintDialog.PrintDocument(), как описывалось ранее, поскольку придется писать просто огромное количество кода и использовать некоторые очень специфические алгоритмы. В следующем примере демонстрируется типичное, не слишком сложное задание по разбиению на страницы. Содержимое DataTable распечатывается в виде таблицы с размещением каждой записи в отдельной строке. Строки разбиваются на страницы на основании того, какое количество строк умещается на странице при использовании выбранного шрифта. На рис. 20.7 показан конечный результат.
Рис. 20.7. Таблица данных, разбитая на две страницы В этом примере специальный класс DocumentPaginator содержит код для разбиения данных на страницы и код для распечатки каждой страницы в виде объекта Visual. Хотя здесь можно было бы применять и два класса (например, если нужно сделать так, чтобы одни и те же данные распечатывались одинаковым образом, а на страницы разбивались по-разному), обычно так все же не делают, поскольку код, требуемый для вычисления размера страницы, тесно связан с кодом, который на самом деле печатает эту страницу.
Book_Pro_WPF-2.indb 676
19.05.2008 18:11:12
Печать
677
Реализация данного специального класса DocumentPaginator является довольно длинной, поэтому рассматриваться будет по кусочкам. Для начала StoreDataSetPaginator сохраняет в приватных переменных несколько важных деталей, включая объект DataTable, который планируется распечатывать, а также выбранную гарнитуру шрифта, размер шрифта, размер страницы и размер поля: public class StoreDataSetPaginator : DocumentPaginator { private DataTable dt; private Typeface typeface; private double fontSize; private double margin; private Size pageSize; public override Size PageSize { get { return pageSize; } set { pageSize = value; PaginateData(); } } public StoreDataSetPaginator(DataTable dt, Typeface typeface, double fontSize, double margin, Size pageSize) { this.dt = dt; this.typeface = typeface; this.fontSize = fontSize; this.margin = margin; this.pageSize = pageSize; PaginateData(); } ...
Обратите внимание, что эти детали предоставляются в конструкторе и после этого изменяться не могут. Единственным исключением является свойство PageSize, которое представляет собой абстрактное свойство из класса DocumentPaginator. При желании разрешить коду изменять эти детали после создания класса DocumentPaginator, можно было бы создать свойства для упаковки других деталей. Для этого потребовалось бы просто сделать так, чтобы при изменении любой из этих деталей вызывался метод PaginateData(). Метод PaginateData() не является обязательным членом. Это просто удобное место для вычисления необходимого количества страниц. StoreDataSetPaginator разбивает данные на страницы сразу же, как только в конструкторе предоставляется объект DataTable. Когда выполняется метод PaginateData(), он определяет объем пространства, который необходим для строки текста, и сравнивает его с размером страницы, чтобы узнать, сколько строк сможет уместиться на каждой странице. Полученный результат сохраняется в поле с именем rowsPerPage. ... private int rowsPerPage; private int pageCount; private void PaginateData() { // Создаем тестовую строку в целях измерения. FormattedText text = GetFormattedText("A");
Book_Pro_WPF-2.indb 677
19.05.2008 18:11:12
678
Глава 20 // Подсчитываем количество умещающихся на странице строк. rowsPerPage = (int)((pageSize.Height-margin*2) / text.Height); // Оставляем одну строку под заголовки (headings). rowsPerPage -= 1;
pageCount = (int)Math.Ceiling((double)dt.Rows.Count / rowsPerPage); } ...
В этом коде предполагается, что заглавной буквы А достаточно для вычисления высоты строки. Однако это может быть так не для всех шрифтов, в случае чего нужно будет передать строку с полным перечнем всех символов, цифр и знаков пунктуации методу GetFormattedText(). На заметку! Для вычисления количества умещающихся на странице строк применяется свойство FormattedText.Height. Свойство FormattedText.LineHeight, которое по умолчанию имеет значение 0, для этого не используется. Оно предоставляется для обеспечения возможности перекрытия стандартного межстрочного интервала при прорисовывании блока с множеством строк текста. Однако если его не установить, класс FormattedText будет использовать свои собственные вычисления, которые подразумевают применение свойства Height. В некоторых случаях нужно будет выполнять немного больше работы и сохранять специальный объект для каждой страницы (например, массив строк с текстом для каждой строчки). Однако в примере StoreDataSetPaginator в этом нет никакой необходимости, поскольку все строки являются одинаковыми, и нет никакого текста, о переносе которого следовало бы беспокоиться. PaginateData() использует приватный вспомогательный метод по имени GetFormattedText(). Печатая текст, вы обнаружите, что требуется создавать огромное количество объектов FormattedText. Эти объекты FormattedText будут всегда совместно использовать одинаковые параметры культуры и компоновку текста “слева направо”. Во многих случаях они также будут использовать и одинаковую гарнитуру шрифта. Метод GetFormattedText() инкапсулирует эти детали и, следовательно, упрощает остальную часть кода. В StoreDataSetPaginator применяются две перегруженных версии метода GetFormattedText(), одна из которых в качестве параметра принимает другую подлежащую применению гарнитуру шрифта: ... private FormattedText GetFormattedText(string text) { return GetFormattedText(text, typeface); } private FormattedText GetFormattedText(string text, Typeface typeface) { return new FormattedText( text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, fontSize, Brushes.Black); } ...
Теперь, зная количество страниц, можно реализовать остальные обязательные свойства DocumentPaginator: ... // Всегда возвращает true, потому что при изменении размера страницы // значение количества страниц обновляется незамедлительно и синхронно. // Оно никогда не остается в неопределенном состоянии. public override bool IsPageCountValid {
Book_Pro_WPF-2.indb 678
19.05.2008 18:11:12
Печать
679
get { return true; } } public override int PageCount { get { return pageCount; } } public override IDocumentPaginatorSource Source { get { return null; } } ...
Фабричного класса, способного создать этот специальный класс DocumentPaginator, здесь нет, поэтому свойство Source возвращает null. Самая последняя деталь реализации также является и самой длинной. Метод GetPage() возвращает объект DocumentPage запрашиваемой страницы со всеми данными. Первый шаг — найти позицию, в которой будут начинаться два столбца. В данном примере размер столбцов устанавливается в соответствии с шириной одной заглавной буквы А, которая является очень удобным сокращением, когда не хочется выполнять более детализированные вычисления. ... public override DocumentPage GetPage(int pageNumber) { // Создаем тестовую строку в целях измерения. FormattedText text = GetFormattedText("A"); double col1_X = margin; double col2_X = col1_X + text.Width * 15; ...
Следующий шаг — найти смещения, идентифицирующие диапазон принадлежащих данной странице записей: ... // Вычисляем диапазон умещающихся на данной странице строк. int minRow = pageNumber * rowsPerPage; int maxRow = minRow + rowsPerPage; ...
Теперь можно переходить к операции печати. Печати подлежат три элемента: заголовки столбцов, разделительная линия и строки. Подчеркнутый заголовок рисуется с помощью методов DrawText() и DrawLine() из класса DrawingContext. Что касается строк, код прогоняет их по циклу от первой до последней, прорисовывая текст из соответствующего объекта DataRow в двух столбцах и затем увеличивая позицию координаты Y на количество, равное высоте строки текста. ... // Создаем для страницы визуальный объект. DrawingVisual visual = new DrawingVisual(); // Сначала устанавливаем позицию левого верхнего угла печатаемой области. Point point = new Point(margin, margin); using (DrawingContext dc = visual.RenderOpen()) { // Прорисовываем заголовок столбца. Typeface columnHeaderTypeface = new Typeface( typeface.FontFamily, FontStyles.Normal, FontWeights.Bold, FontStretches.Normal); point.X = col1_X; text = GetFormattedText("Model Number", columnHeaderTypeface);
Book_Pro_WPF-2.indb 679
19.05.2008 18:11:12
680
Глава 20 dc.DrawText(text, point); text = GetFormattedText("Model Name", columnHeaderTypeface); point.X = col2_X; dc.DrawText(text, point); // Рисуем линию внизу. dc.DrawLine(new Pen(Brushes.Black, 2), new Point(margin, margin + text.Height), new Point(pageSize.Width - margin, margin + text.Height)); point.Y += text.Height; // Прорисовываем значения столбца. for (int i = minRow; i < maxRow; i++) { // Проверяем конец последней (наполовину заполненной) страницы. if (i > (dt.Rows.Count - 1)) break; point.X = col1_X; text = GetFormattedText(dt.Rows[i]["ModelNumber"].ToString()); dc.DrawText(text, point); // Добавляем второй столбец. text = GetFormattedText(dt.Rows[i]["ModelName"].ToString()); point.X = col2_X; dc.DrawText(text, point); point.Y += text.Height; } } return new DocumentPage(visual);
}
Теперь, когда класс StoreDateSetDocumentPaginator готов, его можно использовать везде, где требуется, для печати содержимого объекта DataTable со списком продуктов, как показано ниже: PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { StoreDataSetPaginator paginator = new StoreDataSetPaginator(ds.Tables[0], new Typeface("Calibri"), 24, 96*0.75, new Size(printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight)); printDialog.PrintDocument(paginator, "Custom-Printed Pages"); }
Класс StoreDataSetPaginator обладает определенной гибкостью — например, он может работать с различными шрифтами, полями и размерами страниц — но не способен справляться с данными, имеющими другую схему. Очевидно, что в библиотеке WPF по-прежнему не хватает удобного класса, который бы мог принимать данные, определения столбцов и строк, верхние и нижние колонтитулы и т.д., и затем распечатывать разбитую надлежащим образом на страницы таблицу. В настоящее время ничего подобного в WPF пока нет, но вполне вероятно, что в скором времени сторонние производители заполнят этот пробел, разработав соответствующие компоненты.
Параметры печати и управление Пока что все внимание уделялось только двум методам класса PrintDialog : PrintVisual() и PrintDocument(). Их вполне достаточно для получения приличной распечатки, но при желании иметь возможность управлять параметрами принтера и заданиями на печать потребуется приложить дополнительные усилия. И снова отправной точкой будет класс PrintDialog.
Book_Pro_WPF-2.indb 680
19.05.2008 18:11:12
Печать
681
Сохранение параметров печати В предыдущих примерах уже показывалось, как класс PrintDialog позволяет выбирать принтер и его параметры. Однако те, кто попробовал сделать с помощью этих примеров более одной распечатки, наверняка заметили небольшую аномалию. При каждом возврате в диалоговое окно Print (Печать) в нем восстанавливаются параметры печати, используемые по умолчанию, из-за чего принтер и все остальные настройки приходится выбирать заново. Такие сложности не являются обязательными. У разработчика есть возможность сохранить всю эту информацию и использовать ее повторно. Один из неплохих подходов — сохранить PrintDialog в окне в виде переменной экземпляра. В таком случае создавать новый объект PrintDialog перед каждой новой операцией печати будет не нужно — вместо этого можно просто продолжить пользоваться уже существующим объектом. Такой подход будет работать потому, что PrintDialog инкапсулирует выбор принтера и параметры печати с помощью двух свойств: PrintQueue и PrintTicket. Свойство PrintTicket ссылается на объект System.Printing.PrintTicket, который определяет параметры для задания на печать. К числу таких параметров относятся детали, подобные разрешению и двусторонней печати. При желании параметры PrintTicket можно настраивать и программным путем. У класса PrintTicket даже имеются методы GetXmlStream() и SaveTo(), позволяющие сериализовать объект PrintTicket в поток, а также конструктор, который позволяет воссоздавать объект PrintTicket на основании потока. Эта возможность представляет интерес, если требуется сделать так, чтобы определенные параметры печати сохранялись между различными сеансами работы приложения. (Например, ею можно было бы воспользоваться для создания функция типа профиля печати.) До тех пор, пока эти свойства PrintQueue и PrintTicket будут оставаться согласованными, выбранный принтер и его параметры будут сохраняться одинаковыми при каждом отображении окна Print. Так что даже при необходимости в многократном создании окна PrintDialog простая установка этих свойств позволит сберечь выбираемые пользователем детали.
Печать диапазонов страниц В классе PrintDialog есть еще одна функциональная возможность, которую мы еще не рассматривали. Она позволяет разрешать пользователю выбирать для печати только какую-то определенную часть из всего доступного для печати вывода с помощью текстового поля Pages (Страницы) в разделе Page Range (Диапазон страниц). В текстовом поле Pages пользователь может указывать ряд страниц путем ввода номера начальной и конечной страницы (например, 4-6), или вообще выбирать только конкретную страницу (например, 4). Указывать несколько диапазонов страниц (например, 1-3, 5) в этом поле нельзя. По умолчанию текстовое поле Pages отключено. Чтобы включить его, нужно просто перед вызовом метода ShowDialog() установить для свойства PrintDialog. UserPageRangeEnabled значение true. Опции Selection (Выделенный фрагмент) и Current Page (Текущая страница) все равно останутся недоступными, поскольку они не поддерживаются классом PrintDialog. Но зато еще можно установить значения для свойств MaxPage и MinPage и ограничить количество доступных пользователю для выбора страниц. После отображения диалогового окна Print определить, ввел ли пользователь какойто диапазон страниц, можно путем проверки свойства PageRangeSelection. Если в нем содержится значение UserPages, значит, диапазон страниц присутствует. Свойство
Book_Pro_WPF-2.indb 681
19.05.2008 18:11:12
682
Глава 20
UserRange предоставляет свойство PageRange, которое, в свою очередь, предоставляет номера начальной (PageRange.PageFrom) и конечной страницы (PageRange.PageTo). Сделать так, чтобы эти значения учитывались, и выполнялась печать только запрашиваемых страниц, должен сам разработчик, написав соответствующий код печати.
Управление очередью на печать Обычно взаимодействие клиентского приложения с очередью на печать является довольно ограниченным. После отправки задания на печать, как правило, просто отображается информация о его состоянии или (что встречается гораздо реже) опция, позволяющая приостановить, возобновить или вообще отменить выполнение этого задания. Классы печати WPF предоставляют гораздо более мощные возможности в этом плане и позволяют создавать средства, способные управлять как локальными, так и удаленными очередями заданий на печать. За поддержку для управления очередями на печать отвечают классы из пространства имен System.Printing. Большую часть работы можно выполнять с помощью всего лишь нескольких ключевых классов, которые перечислены в табл. 20.2.
Таблица 20.2. Ключевые классы для управления печатью Имя
Описание
PrintServer и LocalPrintServer
Представляют компьютер, на котором установлены принтеры или другие соответствующие устройства. (“Другими устройствами” могут быть принтеры со встроенными возможностями для работы в сети или специальным оборудованием, позволяющим выступать и в роли сервера печати.) С помощью класса PrintServer можно извлекать коллекцию объектов для данного компьютера. Также еще можно использовать класс LocalPrintServer, который является производным от класса PrintServer и всегда представляет текущий компьютер. Он имеет дополнительное свойство DefaultPrintQueue, которое можно применять для извлечения (или установки) принтера по умолчанию, и статический метод GetDefaultPrintQueue(), который можно использовать без создания экземпляра класса LocalPrintServer.
PrintQueue
Представляет сконфигурированный принтер на сервере печати. Этот класс позволяет извлекать информацию о состоянии принтера и управлять очередью печати, а также извлекать коллекцию объектов PrintQueueJobInfo для данного принтера.
PrintSystemJobInfo
Представляет задание, которое было отправлено в очередь на печать. С помощью этого класса можно извлекать информацию о состоянии задания, а также изменять состояние задания или удалять его.
Используя эти базовые классы, легко создать программу, инициирующую печать вообще безо всякого вмешательства пользователя. PrintDialog dialog = new PrintDialog(); // Выбираем принтер по умолчанию. dialog.PrintQueue = LocalPrintServer.GetDefaultPrintQueue(); // Печатаем что-нибудь. dialog.PrintDocument(someContent, "Automatic Printout");
Также еще можно создать и применить к окну PrintDialog объект PrintTicket, чтобы сконфигурировать другие связанные с печатью параметры. Даже еще интереснее то, что можно заглянуть в классы PrintServer, PrintQueue и PrintSystemJobInfo поглубже и изучить, что там на самом деле происходит.
Book_Pro_WPF-2.indb 682
19.05.2008 18:11:13
683
Печать
На рис. 20.8 показана простая программа, которая позволяет просматривать очереди на печать на текущем компьютере и ожидающие выполнения задания в каждой из них. Кроме того, она еще также позволяет выполнять кое-какие базовые задачи по управлению вроде приостановки работы принтера (или задания на печать), возобновления работы принтера (или задания на печать) и отмены одного или всех заданий на печать в очереди. Проанализировав, как работает данное приложение, можно изучить основы модели управления печатью WPF.
Рис. 20.8. Просмотр очередей и заданий принтера В этом примере используется один единственный объект PrintServer, который создается в виде поля-члена в классе окна: private PrintServer printServer = new PrintServer();
Когда объект PrintServer создается без передачи каких-либо аргументов конструктору, он представляет текущий компьютер. Однако, передав соответствующий UNCпуть, можно указать и на какой-нибудь сервер печать в сети, например, так: private PrintServer printServer = new PrintServer(\\Warehouse\PrintServer);
С помощью объекта PrintServer код получает список очередей на печать, представляющих сконфигурированные на текущем компьютере принтеры. В этом шаге нет ничего сложного. Все, что требуется сделать — это вызвать при первой загрузке окна метод PrintServer.GetPrintQueues(): private void Window_Loaded(object sender, EventArgs e) { lstQueues.DisplayMemberPath = "FullName"; lstQueues.SelectedValuePath = "FullName"; lstQueues.ItemsSource = printServer.GetPrintQueues(); }
В этом фрагменте кода используется только свойство PrintQueue.FullName. Однако в классе PrintQueue имеется и много других заслуживающих внимания свойств: например, свойства вроде DefaultPriority, DefaultPrintTicket и т.д., которые позволяют извлекать параметры печати по умолчанию, свойства вроде QueueStatus и NumberOfJobs,
Book_Pro_WPF-2.indb 683
19.05.2008 18:11:13
684
Глава 20
которые позволяют извлекать информацию о состоянии и другие общие сведения, и булевские свойства IsXxx и HasXxx вроде IsManualFeedRequired, IsWarmingUp, IsPaperJammed, IsOutOfPaper, HasPaperProblem и NeedUserIntervention, которые позволяют изолировать определенные проблемы. В текущем примере при выборе принтера в списке сначала отображается информация о состоянии этого принтера, а затем осуществляется выборка всех отправленных в очередь заданий на печать. За выполнение этой задачи отвечает метод PrintQueue. GetPrintJobInfoCollection(). private void lstQueues_SelectionChanged(object sender, SelectionChangedEventArgs e) { try { PrintQueue queue = printServer.GetPrintQueue(lstQueues.SelectedValue.ToString()); lblQueueStatus.Text = "Queue Status: " + queue.QueueStatus.ToString(); lstJobs.DisplayMemberPath = "JobName"; lstJobs.SelectedValuePath = "JobIdentifier"; lstJobs.ItemsSource = queue.GetPrintJobInfoCollection(); } catch (Exception err) { MessageBox.Show(err.Message, "Error on " + lstQueues.SelectedValue.ToString()); } }
Каждое задание имеет вид объекта PrintSystemJobInfo. Когда в списке выбирается то или иное задание, следующий код отображает информацию о его состоянии: private void lstJobs_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (lstJobs.SelectedValue == null) { lblJobStatus.Text = ""; } else { PrintQueue queue = printServer.GetPrintQueue(lstQueues.SelectedValue.ToString()); PrintSystemJobInfo job = queue.GetJob((int)lstJobs.SelectedValue); lblJobStatus.Text = "Job Status: " + job.JobStatus.ToString(); } }
Теперь нерассмотренной осталась только одна деталь, а именно — обработчики событий, манипулирующие очередью или заданием при выполнении щелчка на одной из кнопок в окне. Этот код выглядит вообще чрезвычайно просто. Все, что здесь требуется сделать — это извлечь ссылку на соответствующую очередь или задание и затем вызвать соответствующий метод. Например, ниже показано, как приостанавливается PrintQueue: PrintQueue queue = printServer.GetPrintQueue(lstQueues.SelectedValue.ToString()); queue.Pause();
А вот как приостанавливается задание на печать: PrintQueue queue = printServer.GetPrintQueue(lstQueues.SelectedValue.ToString()); PrintSystemJobInfo job = queue.GetJob((int)lstJobs.SelectedValue); job.Pause();
Book_Pro_WPF-2.indb 684
19.05.2008 18:11:13
Печать
685
На заметку! Приостанавливать (и возобновлять) можно как работу всего принтера, так и выполнение только какого-нибудь одного задания. И то и другое делается с помощью доступного в окне панели управления значка Printers (Принтеры). Приостановить или возобновить очередь можно, щелкнув на принтере правой кнопкой мыши, а поработать с отдельными заданиями — дважды щелкнув на нем. Очевидно, что без кода обработки ошибок при выполнении подобных задач никак не обойтись, поскольку нет никакой гарантии, что они будут выполнены успешно. Например, при попытке отменить чье-то задание на печать может вмешаться система безопасности Windows, а при попытке выполнить печать на сетевом принтере после разрыва соединения — возникнуть ошибка. WPF включает довольно много функциональных возможностей, связанных с печатью. Тем, кто заинтересован в применении именно этих специализированных возможностей (например, потому, что разрабатывает какое-нибудь соответствующее средство или создает задачу, предназначенную для длительного выполнения на заднем фоне), стоит заглянуть в комплект .NET SDK и ознакомиться там с классами, доступными в пространстве имен System.Printing.
Печать через XPS Как рассказывалось в главе 19, WPF поддерживает два дополнительных типа документов: потоковые документы и документы XPS. Потоковые документы предназначены для гибкого содержимого, которое способно принимать форму, соответствующую любому указываемому размеру страницы. XPS-документы предназначены для хранения готового к печати содержимого с фиксированным размером страниц. Это содержимое просто “замораживается” на месте и остается в своем точном исходном состоянии. Нетрудно догадаться, что печать документов XpsDocument выполняется легко. Класс XpsDocument, точно так же как и класс FlowDocument, предоставляет класс DocumentPaginator. Однако этому классу DocumentPaginator практически ничего не нужно делать, поскольку содержимое уже скомпоновано в виде фиксированных, не изменяющихся страниц. Ниже приводится код, который можно использовать для загрузки XPS-файла в память, отображения его в DocumentViewer и последующей отправки на принтер: // Отображаем документ. XpsDocument doc = new XpsDocument("filename.xps", FileAccess.ReadWrite); docViewer.Document = doc.GetFixedDocumentSequence(); doc.Close(); // Печатаем документ. if (printDialog.ShowDialog() == true) { printDialog.PrintDocument(docViewer.Document.DocumentPaginator, "A Fixed Document"); }
Очевидно, что отображать фиксированный документ в DocumentViewer перед его распечаткой вовсе необязательно. В данном коде этот шаг присутствует потому, что такой подход является очень распространенным. Во многих сценариях требуется, чтобы документ XpsDocument сначала загружался для просмотра и распечатывался только после того, как пользователь щелкнет на соответствующей кнопке. Элемент DocumentViewer, как и аналогичные элементы, применяемые для объектов FlowDocument, тоже поддерживает команду ApplicationCommands.Print, а это означает, что XPS-документ можно отправлять из DocumentViewer на принтер безо всякого кода.
Book_Pro_WPF-2.indb 685
19.05.2008 18:11:13
686
Глава 20
Поддержка XPS в Windows XP Поддержка документов XPS — это встроенная часть .NET Framework 3.0. Однако возможность распечатывать XPS-содержимое напрямую является функциональной возможностью Windows Vista, поскольку требует наличия модели печати XPS. Более того, процесс печати XPS требует драйвера, поддерживающего XPS, а таковые практически недоступны для более старых моделей принтеров. К счастью, WPF включает уровень функциональной совместимости, обеспечивающий возможность выполнять печать XPS-содержимого на любой из операционных систем (Windows XP и Windows Vista) без особых отличий. Когда XPS-содержимое печатается в Windows Vista с поддерживающим XPS драйвером печати, WPF использует процесс печати XPS. Когда печать осуществляется без поддерживающего XPS драйвера в Windows XP, WPF выполняет кое-какие незаметные операции по преобразованию, в результате которых XPS-содержимое равномерно конвертируется в GDIмодель, используемую традиционными драйверами.
Создание документа XPS для предварительного просмотра перед печатью WPF также включает и всю необходимую поддержку для создания XPS-документов программным способом. Создание XPS с концептуальной точки зрения похоже на печать какого-то содержимого — после разработки XPS-документа выбирается фиксированный размер страниц и вся компоновка “замораживается”. Так зачем же тогда прилагать еще какие-то усилия и добавлять такой дополнительный шаг? На то существуют две веских причины.
• Предварительный просмотр перед печатью. Можно сделать так, чтобы сгенерированный XPS-документ отображался в DocumentViewer для предварительного просмотра, а дальше уже пользователь решал, распечатывать его или нет.
• Асинхронная печать. Класс XpsDocumentWriter включает не только метод Write() для синхронной печати, но и метод WriteAsync(), позволяющий отправлять содержимое на принтер асинхронным образом. Для длинной, сложной операции печати предпочтительнее использовать асинхронный вариант. В этом случае приложение будет лучше реагировать на действия пользователя. Единственным ограничением при создании XPS-документа является то, что его нужно записывать в файл. Просто создать XPS-документ в памяти нельзя. Для получения подходящего пути к временному файлу можно использовать метод вроде Path.GetTempFileName(). В целом, чтобы создать XPS-документ, нужно просто с помощью статического метода XpsDocument.CreateXpsDocumentWriter() создать объект XpsDocumentWriter, например: XpsDocument xpsDocument = new XpsDocument("filename.xps", FileAccess.ReadWrite); XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);
Объект XpsDocumentWriter представляет собой упрощенный класс: его функциональные возможности крутятся вокруг методов Write() и WriteAsync(), которые записывают содержимое в XPS-документ. Оба эти метода перегружаются множество раз, позволяя записывать различные типы содержимого, включая еще один XPS-документ, страницу, извлеченную из XPS-документа, визуальный объект (который позволяет записывать любой элемент) и DocumentPaginator. Последние две опции являются особенно интересными, поскольку дублируют опции, доступные при печати. Например, если
Book_Pro_WPF-2.indb 686
19.05.2008 18:11:13
687
Печать
вы создали DocumentPaginator для обеспечения специальной печати (как описывалось ранее в этой главе), вы можете также использовать его и для записи XPS-документа. Ниже приведен пример, в котором открывается существующий потоковый документ и выполняется запись этого документа в XpsDocumentWriter с помощью метода Write(). Далее получившийся новый документ XPS отображается в DocumentViewer, что дает возможность просматривать его перед печатью: using (FileStream fs = File.Open("FlowDocument1.xaml", FileMode.Open)) { FlowDocument flowDocument = (FlowDocument)XamlReader.Load(fs); writer.Write(((IDocumentPaginatorSource)flowDocument).DocumentPaginator); // Отображение нового документа XPS в окне предварительного просмотра. docViewer.Document = xpsDocument.GetFixedDocumentSequence(); xpsDocument.Close(); }
Извлекать классы типа Visual и Paginator в приложении WPF можно множеством разных способов. Поскольку класс XpsDocumentWriter поддерживает их, он позволяет записывать в XPS-документ любое WPF-содержимое.
Печать прямо на принтер через XPS Как уже говорилось в этой главе, поддержка печати в WPF основывается на процессе печати XPS. Если использовать класс PrintDialog, можно и не увидеть ни одного признака этой низкоуровневой реальности. Но если работать с классом XpsDocumentWriter, не заметить это просто не получится. Пока что вся печать осуществлялась через класс PrintDialog. Такой подход не является обязательным, потому что на самом деле класс PrintDialog делегирует всю сложную работу классу XpsDocumentWriter . То есть можно создать класс XpsDocumentWriter, упаковывающий не объект FileStream, а объект PrintQueue. Фактический код для написания распечатываемого вывода будет идентичным: доведется снова полагаться на методы Write() и WriteAsync(). Ниже показан фрагмент кода, который отображает диалоговое окно Print, извлекает информацию о том, какой принтер был выбран, и использует ее для создания класса XpsDocumentWriter, отправляющего задание на печать: string filePath = Path.Combine(appPath, "FlowDocument1.xaml"); if (printDialog.ShowDialog() == true) { PrintQueue queue = printDialog.PrintQueue; XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue); using (FileStream fs = File.Open(filePath, FileMode.Open)) { FlowDocument flowDocument = (FlowDocument)XamlReader.Load(fs); writer.Write(((IDocumentPaginatorSource)flowDocument).DocumentPaginator); }
Интересно то, что в этом примере все равно присутствует класс PrintDialog . Однако он служит только для отображения стандартного диалогового окна Print и предоставления пользователю возможности выбрать принтер. Сама печать выполняется уже непосредственно с помощью класса XpsDocumentWriter.
Асинхронная печать Класс XpsDocumentWriter делает выполнение асинхронной печати простой задачей. На самом деле предыдущий пример можно переделать так, чтобы в нем осущест-
Book_Pro_WPF-2.indb 687
19.05.2008 18:11:13
688
Глава 20
влялась асинхронная печать, просто заменив вызов метода Write() вызовом метода WriteAsync(). На заметку! В Windows все задания на печать распечатываются асинхронным образом. Однако процесс отправки задания выполняется синхронным образом, если используется метод Write(), и асинхронно, если применяется метод WriteAsync(). Во многих случаях время, затрачиваемое на отправку задания на печать, не имеет особого значения и потому эта функция не нужна. Однако когда речь идет о создании (и разбивке на страницы) содержимого, которое должно распечатываться асинхронно, дело обстоит совсем иначе, поскольку зачастую этот этап оказывается самым длительным в процессе печати, а раз так, подобная функция вовсе не помешает. При желании иметь ее, придется писать код, выполняющий логику печати в фоновом потоке. Чтобы сделать этот процесс относительно простым, можно воспользоваться приемами, описанными в главе 3 (вроде класса BackgroundWorker). Сигнатура метода WriteAsync() совпадает с сигнатурой метода Write(): другими словами, метод WriteAsync() принимает объект Paginator, объект Visual или объект одного из нескольких других типов. Вдобавок метод WriteAsynс() имеет перегрузки, принимающие второй необязательный параметр с информацией о состоянии. Эта информация о состоянии может иметь вид любого объекта, который должен использоваться для идентификации задания на печать. Этот объект предоставляется через объект WritingCompletedEventArgs при срабатывании события WritingCompleted, что позволяет инициировать сразу множество заданий на печать, обрабатывать событие WritingCompleted для каждого из них с помощью одного и того же обработчика событий и определять, какое из них было отправлено на печать при каждом срабатывании события. Когда асинхронное событие на печать уже находится в процессе обработки, его можно отменить вызовом метода CancelAsync(). Класс XpsDocumentWriter также включает небольшой набор событий, которые позволяют обеспечивать соответствующую реакцию при отправке задания на печать: WritingProgressChanged, WritingCompleted и WritingCancelled. Важно запомнить, что событие WritingCompleted срабатывает после записи задания в очередь на печать, но это вовсе не означает, что принтер уже его распечатал.
Резюме В этой главе речь шла о новой модели печати, которая появилась в WPF. Сначала была рассмотрена самая простая точка входа: универсальный класс PrintDialog, который позволяет пользователям конфигурировать параметры печати, а приложению — отправлять документ или визуальный объект на печать. После описания ряда различных способов для расширения класса PrintDialog и его использования с отображаемым на экране и динамически генерируемым содержимым мы перешли к исследованию находящейся на более низком уровне модели печати XPS и, в частности, класса XpsDocumentWriter, который поддерживает PrintDialog и может использоваться самостоятельно. Этот класс позволяет легко создавать средство для предварительного просмотра документов перед печатью (ведь в WPF нет никакого элемента управления с подобной функциональностью), а также отравлять задания на печать асинхронным образом.
Book_Pro_WPF-2.indb 688
19.05.2008 18:11:14
ГЛАВА
21
Анимация А
нимация позволяет создавать по-настоящему динамические пользовательские интерфейсы. Она часто применяется для создания различных эффектов, например, пиктограмм, которые увеличиваются при перемещении над ними курсора мыши, вращающихся логотипов, прокручивающегося текста и т.п. Иногда такие эффекты выглядят чрезмерными. Но при правильном применении анимация может во многих отношениях усовершенствовать приложение. Она может повысить реактивность приложения, сделать его более естественным и интуитивно понятным. (Например, кнопка, сдвигающаяся при щелчке на ней, выглядит более реальной, физической кнопкой, а не просто очередным серым прямоугольником.) Анимация также может привлечь внимание к наиболее важным элементам и служить своеобразным проводником для пользователя в его странствиях по новому содержимому. (Например, приложение может рекламировать только что загруженную информацию посредством подергивания, мерцания или помещения пиктограммы в панель состояния.) Анимация — центральная часть модели WPF. Это значит, что вам не нужно использовать таймеры и код обработки событий, чтобы привести ее в действие. Вместо этого вы создаете ее декларативно, конфигурируете несколько классов и запускаете в действие, не написав ни единой строчки кода C#. Также анимация совершенно незаметно сама интегрируется в обычные окна WPF и на страницы. Например, если вы анимируете кнопку таким образом, что она дрейфует по окну, тем не менее, она при этом продолжает вести себя как кнопка. Ее можно стилизовать, она принимает фокус и на ней можно щелкнуть, запустив обычный код обработчика события. Именно это отличает анимацию от традиционных медиафайлов, таких как видео. (В главе 22 вы прочтете о том, как поместить видео-окно в ваше приложение. Видео-окно — это полностью отдельная область вашего приложения; она может воспроизводить видео, но не может взаимодействовать с пользователем.) В этой главе вы ознакомитесь с богатым набором классов анимации, предлагаемым WPF. Вы увидите, как следует применять их в коде и (что делается чаще) как конструировать и управлять ими через XAML. Попутно вы увидите широкий набор примеров анимации, включая “затухающие” окна, вращающиеся кнопки и разворачивающиеся элементы.
Основы анимации WPF В предыдущих Windows-ориентированных платформах (вроде Windows Forms и MFC) разработчикам приходилось разрабатывать собственные системы анимации с нуля. Наиболее распространенный прием заключался в применении таймера в сочетании с некоторой специальной логикой рисования. WPF изменила правила игры, предложив новую систему анимации, основанную на свойствах (property-based). В следующих двух разделах мы поясним разницу.
Book_Pro_WPF-2.indb 689
19.05.2008 18:11:14
690
Глава 21
Анимация на основе таймера Предположим, что вам нужно заставить кусок текста вращаться в окне About приложения Windows Forms. Ниже представлен традиционный способ решения этой проблемы. 1. Создать таймер, который срабатывает периодически (скажем, каждые 50 миллисекунд). 2. Когда сработает таймер, использовать обработчик событий для вычисления некоторых деталей анимации, таких как новый угол поворота. Затем сделать недействительным все окно либо его часть. 3. Сразу после этого Windows попросит окно перерисовать свое содержимое, запустив ваш специальный код рисования. 4. В вашем коде отобразить текст, повернутый на текущее значение угла. Хотя такое решение на основе таймера реализовать не так трудно, интеграция его в обычное окно приложения неоправданно затруднена. Ниже описаны некоторые проблемы, возникающие при этом.
• Рисуются пиксели, а не элементы управления. Чтобы повернуть текст в Windows Forms, вам нужно обратиться к низкоуровневой поддержке рисования GDI+. Сделать это не сложно, однако это плохо сочетается с обычными элементами окна вроде кнопок, текстовых полей, меток и т.п. В результате вам придется отделять анимируемое содержимое от элементов управления, и вы не сможете включить в анимацию элементы, способные взаимодействовать с пользователем. Если потребуется реализовать вращающуюся кнопку — считайте, что вам не повезло.
• Подразумевается единственная анимация. Если вы пожелаете иметь две анимации, выполняющиеся одновременно, то вам придется переписать весь код анимации — и это существенно его усложнит. В этом отношении WPF намного мощнее, что позволяет строить более сложные анимации, состоящие из индивидуальных, более простых.
• Временные рамки анимации фиксированы. Они определяется таймером. И если вы измените период таймера, то вам может понадобиться изменить код анимации (в зависимости от того, как выполняются вычисления). Более того, выбранные вами фиксированные временные рамки не всегда подойдут для конкретной видеосистемы компьютера.
• Более сложная анимация требует экспоненциально более сложного кода. Пример с вращающимся текстом достаточно прост, но, к примеру, перемещение маленького вектора (стрелки) по определенному пути — задача несколько более сложная. В WPF даже самая замысловатая анимация может быть определена в XAML (и сгенерирована с применением визуальных инструментальных средств от независимых разработчиков). Даже без поддержки анимации WPF пример с вращающимся текстом можно упростить. Это связано с тем, что WPF предлагает более совершенную графическую модель, которая гарантирует, что окно будет автоматически перерисовано, если в нем произошли какие-то изменения. Это означает, что вам не нужно беспокоиться об объявлении недействительным области окна и его перерисовке.
Book_Pro_WPF-2.indb 690
19.05.2008 18:11:14
Анимация
691
Вместо этого достаточно выполнить перечисленные ниже шаги. 1. Создать периодически срабатывающий таймер. (Для этого WPF предоставляет System.Windows.Threading.DispatherTimer, работающий в потоке пользовательского интерфейса). 2. Когда срабатывает таймер, использовать обработчик событий для вычисления некоторых связанных с анимацией деталей вроде нового угла поворота. Затем модифицировать соответствующие элементы. 3. WPF заметит проведенные изменения в элементах вашего окна и перерисует (с кэшированием) новое содержимое окна. При таком решении вам не нужно возиться с низкоуровневыми классами для рисования, а также отделять анимируемое содержимое от обычных элементов того же окна. Хотя это, безусловно, усовершенствование, однако анимация на основе таймера все-таки имеет ряд недостатков: она приводит к появлению не слишком гибкого кода, серьезному усложнению его в случае, когда нужно получить более сложные эффекты, а также не обеспечивает максимально возможной производительности. Вместо этого WPF включает высокоуровневую модель, которая позволяет сосредоточиться на определении анимации, не беспокоясь о способе ее отображения. Эта модель основана на инфраструктуре свойств зависимостей, которую мы опишем в следующем разделе.
Анимация на основе свойств Часто анимацию воспринимают как серию фреймов. Чтобы выполнить анимацию, эти фреймы отображаются друг за другом, подобно покадровому видео. WPF использует совершенно другую модель. По сути, анимация WPF — это просто способ модифицировать значение свойства зависимостей через интервалы времени. Например, чтобы заставить кнопку растягиваться и сжиматься, вы можете в анимации модифицировать ее свойство Width. Чтобы заставить ее мерцать, вы можете изменять свойства LinearGradientBrush, используемого для ее фона. Секрет создания правильной анимации — в определении того, какие именно свойства нужно модифицировать. Если вам нужно внести изменения, которые не могут быть обеспечены модификацией свойств, то вам не повезло. Например, вы не можете добавлять или удалять элементы в процессе анимации. Аналогично вы не можете попросить WPF выполнить переход от начальной сцены к конечной (хотя некоторые обходные пути позволяют эмулировать такой эффект). И, наконец, вы можете применять анимацию только к свойствам зависимостей, поскольку только эти свойства использует система динамического разрешения свойств (описанная в главе 6), которая принимает во внимание анимацию. На первый взгляд сосредоточенная на свойствах природа анимации WPF кажется чрезвычайно ограниченной. Однако когда вы поработаете с WPF, то обнаружите, что она на самом деле очень удобна. Фактически вы можете реализовать широкий диапазон эффектов анимации, используя общие свойства, которые поддерживаются всеми элементами. Но следует признать, что существует немало случаев, когда система анимации на основе свойств не работает. В качестве эмпирического правила скажем, что анимация на базе свойств — отличный способ добавить динамические эффекты к обычным в других отношениях приложениям Windows. Например, если вы хотите соорудить привлекательный фасад для интерактивного инструмента электронной торговли, анимация на базе свойств будет работать замечательно. Однако если вы захотите использовать анимацию для обеспечения основной функциональности вашего приложения, и заставить ее ра-
Book_Pro_WPF-2.indb 691
19.05.2008 18:11:14
692
Глава 21
ботать на протяжении всего времени его существования, то для этого вам понадобится нечто более мощное и гибкое. Например, если вы создаете аркадную игру, требующую сложных вычислений для моделирования коллизий, то вам понадобится более высокая степень контроля, чем может обеспечить анимация. В таких ситуациях вам придется сделать большую часть работы самостоятельно, используя низкоуровневую поддержку визуализации фреймов WPF, которая будет описана в конце этой главы.
Базовая анимация Вы уже изучили основное правило анимации WPF — каждая анимация работает на основе отдельного свойства зависимостей. Однако есть и другое ограничение. Чтобы анимировать свойство (другими словами, изменять его значение с течением времени), вам понадобится класс анимации, поддерживающий его тип данных. Например, свойство Button.Width использует тип данных double. Чтобы анимировать его, вы применяете класс DoubleAnimation. Однако Button.Padding использует структуру Thickness, поэтому ему потребуется класс ThicknessAnimation. Это требование не так строго, как первое правило WPF-анимации, ограничивающее анимацию свойствами зависимостей. Это объясняется тем, что вы можете анимировать свойство зависимостей, которое не имеет соответствующего класса анимации, создав собственный класс анимации для этого типа данных. Однако в пространстве имен System.Windows.Animation вы найдете классы для большинства типов данных, которые захотите использовать. Многие типы данных не имеют соответствующих классов анимации, поскольку это непрактично. Примером могут служить перечисления. Например, вы можете контролировать то, как элемент помещается в панели компоновки, используя свойство HorizontalAlignment, которое берет свое значение из перечисления HorizontalAlignment. Однако перечисление HorizontalAlignment позволяет выбирать только между четырьмя значениями (Left, Right, Center и Stretch), что существенно ограничивает его применение в анимации. Хотя вы можете переключаться от одной ориентации к другой, все же вы не можете плавно перенести элемент от одного типа выравнивания к другому. По этой причине не предусмотрен класс анимации для типа данных HorizontalAlignment. Вы можете создать его самостоятельно, но все равно будете ограничены этими четырьмя значениями перечисления. Ссылочные типы обычно не анимируются. Однако их подсвойства — могут. Например, все элементы управления имеют свойство Background, что позволяет устанавливать объект Brush, используемый для рисования фона. Применять анимацию для переключения от одной кисти к другой не слишком эффективно, но вы можете использовать анимацию для изменения свойств кисти. Например, вы можете варьировать свойство Color объекта SolidColorBrush (применяя для этого класс ColorAnimation) или же свойство Offset объекта GradientStop в составе LinearGradient (с помощью класса DoubleAnimation). Это расширяет возможности анимации WPF, позволяя анимировать специальные аспекты внешнего вида элементов.
Классы анимации На основе упомянутых нами имен типов анимации — DoubleAnimation и ColorAnimation — вы можете предположить, что все классы анимации называются в стиле ИмяТипаAnimation. Это близко к истине, но не совсем так. На самом деле существуют два вида анимации — та, которая изменяет свойства последовательно от начального до конечного значения (этот процесс называется линейной интерполяцией), и та, что произвольно изменяет свойство от одного значения к
Book_Pro_WPF-2.indb 692
19.05.2008 18:11:14
Анимация
693
другому. DoubleAnimation и ColorAnimation — примеры из первой категории; они используют интерполяцию для гладкого изменения значения. Однако интерполяция не имеет смысла при изменении определенных типов данных, таких как строки и объекты ссылочных типов. Вместо применения интерполяции эти типы данных изменяются скачкообразно в определенный момент времени, используя для этого технику, называемую анимация ключевого кадра (key frame animation). Все классы анимации ключевого кадра носят имена в форме ИмяТипаAnimationUsingKeyFrames — например, StringAnimationUsingKeyFrames и ObjectAnimationUsingKeyFrames. Некоторые типы данных имеют класс анимации ключевого кадра, но не имеют класса анимации интерполяцией. Например, вы можете анимировать строку, используя ключевые кадры, но не можете анимировать ее интерполяцией. Однако каждый тип данных поддерживает анимацию ключевого кадра или же он не имеет поддержки анимации вообще. Другими словами, каждый тип данных, имеющий соответствующий ему класс анимации, использующий интерполяцию (наподобие DoubleAnimation и ColorAnimation), также имеет соответствующий тип анимации ключевого кадра (такой как DoubleAnimationUsingKeyFrames и ColorAnimationUsingKeyFrames). Правда, есть еще один тип анимации. Третий тип называется анимацией на основе пути (path-based animation), и этот тип более специализирован, чем анимация интерполяцией или анимация ключевого кадра. Анимация на основе пути модифицирует значение в соответствии с фигурой, описанной в объекте PathGeometry, и в первую очередь применяется для перемещения элемента по некоторому пути. Классы для анимации на базе пути имеют имена в стиле ИмяТипаAnimationUsingPath, вроде DoubleAnimationUsingPath или PointAnimationUsingPath. На заметку! Хотя в настоящее время WPF использует три подхода к анимации (линейная интерполяция, ключевые кадры и пути), ничто не помешает вам создавать классы анимации, которые модифицируют значения на основе совершенно другого подхода. Единственное требование — чтобы ваш класс анимации модифицировал значения с течением времени. В конечном итоге, в пространстве имен System.Windows.Media.Animation вы найдете:
• 17 классов ИмяТипаAnimation, использующих интерполяцию; • 22 класса ИмяТипаAnimationUsingKeyFrames, использующих анимацию ключевого кадра;
• 3 класса ИмяТипаAnimationUsingPath , использующих анимацию на основе пути. Все эти классы анимации унаследованы от абстрактного класса ИмяТипаAnimationBase, реализующего несколько основополагающих моментов. Он дает вам основу для создания ваших собственных классов анимации. Если тип данных поддерживает более одного типа анимации, то все его классы анимации наследуются от абстрактного базового класса. Например, DoubleAnimation и DoubleAnimationUsingKeyFrames — оба являются наследниками DoubleAnimationBase. На заметку! Этими 42 классами содержимое пространства имен System.Windows.Media. Animation не исчерпывается. Каждая анимация ключевого кадра также работает со своим собственным классом ключевого кадра и классом коллекции ключевых кадров. Так что в сумме пространство имен System.Windows.Media.Animation содержит более 100 классов. Вы можете легко определить, какие типы данных имеют готовую поддержку анимации, просмотрев эти 42 класса. Ниже представлен их полный список.
Book_Pro_WPF-2.indb 693
19.05.2008 18:11:14
694
Глава 21
BooleanAnimationUsingKeyFrames ByteAnimation ByteAnimationUsingKeyFrames CharAnimationUsingKeyFrames ColorAnimation ColorAnimationUsingKeyFrames DecimalAnimation DecimalAnimationUsingKeyFrames DoubleAnimation DoubleAnimationUsingKeyFrames DoubleAnimationUsingPath Int16Animation Int16AnimationUsingKeyFrames Int32Animation Int32AnimationUsingKeyFrames Int64Animation Int64AnimationUsingKeyFrames MatrixAnimationUsingKeyFrames MatrixAnimationUsingPath ObjectAnimationUsingKeyFrames PointAnimation
PointAnimationUsingKeyFrames PointAnimationUsingPath Point3DAnimation Point3DAnimationUsingKeyFrames QuarternionAnimation QuarternionAnimationUsingKeyFrames RectAnimation RectAnimationUsingKeyFrames Rotation3DAnimation Rotation3DAnimationUsingKeyFrames SingleAnimation SingleAnimationUsingKeyFrames SizeAnimation SizeAnimationUsingKeyFrames StringAnimationUsingKeyFrames ThicknessAnimation ThicknessAnimationUsingKeyFrames VectorAnimation VectorAnimationUsingKeyFrames Vector3DAnimation Vector3DAnimationUsingKeyFrames
Многие из этих типов самоочевидны и не требуют пояснений. Например, если вы разберетесь с классом DoubleAnimation, вам не придется долго думать, чтобы понять SingleAnimation, Int16Animation, Int32Animation, а также все прочие классы анимации, сопровождающие простые числовые типы, которые работают точно так же. Наряду с классами анимации простых числовых типов вы найдете несколько, работающих с прочими базовыми типами данных (byte, bool, string и char), а также много больше, имеющих дело с двумерными и трехмерными рисованными примитивами (Point, Size, Rect, Vector и т.д.). Кроме того, вы найдете классы анимации для свойств Margin и Padding для любого элемента (ThicknessAnimation), один для цвета (ColorAnimation) и один для любого объекта ссылочного типа (ObjectAnimationUsing KeyFrames). Многие из перечисленных классов анимации встретятся вам в примерах, которые мы рассмотрим на протяжении этой главы.
Перегруженное пространство имен Animation Если вы заглянете в пространство имен System.Windows.Media.Animation, то будете несколько шокированы. Оно включает в себя разнообразные классы анимации для самых разнообразных типов данных. В результате все это выглядит несколько перегруженным. Было бы неплохо иметь какой-то способ комбинации всех средств анимации в несколько классов ядра. И почему бы ни применить класс Animation, который смог бы работать с любым типом данных? Однако, к сожалению, такая модель в настоящий момент невозможна по ряду причин. Во-первых, различные классы анимации могут выполнять свою работу несколько по-разному, а это требует разного кода. Например, способ перехода между полутонами одного цвета в классе ColorAnimation отличается от способа модификации отдельного числового значения в классе DoubleAnimation. Другими словами, хотя классы анимации и предоставляют одинаковый открытый интерфейс, но их внутренняя реализация может сильно отличаться. Интерфейсы этих классов стандартизованы через наследование, поскольку все классы анимации унаследованы от одних и тех же базовых классов (начиная с Animatable). Это еще не все. Конечно, многие классы анимации разделяют некоторый объем кода, а по некоторым вообще “плачут” обобщения (generics), как, например, около сотни классов, используемых для представления ключевых кадров и коллекций
Book_Pro_WPF-2.indb 694
19.05.2008 18:11:14
Анимация
695
ключевых кадров. В идеальном мире классы анимации отличались бы типом анимации, которую они выполняют, так что вы могли бы использовать классы вроде NumericAnimation, KeyFrameAnimation или LinearInterpolationAnimation. Можно предположить, что более глубокая причина того, что классы анимации не организованы подобным образом, заключается в отсутствии прямой поддержки обобщений в XAML.
Анимация в коде Как вы уже знаете, наиболее распространенная техника анимации — это анимация интерполяцией, при которой свойство модифицируется плавно от начальной точки до конечной. Например, если вы установите в качестве начального значения 1, а конечного — 10, то ваше свойство может быстро изменяться от 1 до 1.1, 1.2, 1.3 и т.д., пока не достигнет значения 10. Тут вы можете спросить: как WPF определяет шаг инкремента для выполнения интерполяции? К счастью, это делается автоматически. WPF использует такой шаг инкремента, который необходим для выполнения плавной анимации при текущей частоте кадров видеосистемы. Стандартная частота, используемая WPF, составляет 60 кадров в секунду. (Дальше в этой главе вы узнаете, как изменить эту настройку.) Другими словами, каждую одну шестидесятую часть секунды WPF вычисляет все анимируемые значения и обновляет соответствующие свойства. Простейший способ использования анимации заключается в создания экземпляра одного из классов анимации, перечисленных выше, конфигурировании его и затем вызова BeginAnimation() элемента, который вы хотите модифицировать. Все элементы WPF наследуют метод BeginAnimation(), который является частью интерфейса IAnimable, от базового класса UIElement. Другие классы, реализующие IAnimable, включают ContentElement (базовый класс для битов содержимого потока документа) и Visual3D (базовый класс для трехмерных визуальных элементов). На заметку! Это не самый распространенный подход. Во многих ситуациях вы будете создавать анимации декларативно, с помощью XAML, как это будет описано ниже в разделе “Декларативная анимация и раскадровки”. Однако применение XAMLнемного более запутано, поскольку требует еще одного объекта, называемого раскадровкой (storyboard), для подключения анимации к соответствующему свойству. Анимации на основе кода также удобны в определенных сценариях, когда нужно использовать сложную логику для определения начального и конечного значений для вашей анимации. На рис. 21.1 показан пример крайне простой анимации, расширяющей кнопку. Когда вы щелкаете на кнопке, WPF плавно раздвигает обе стороны кнопки, пока она не заполнит окно. Чтобы создать такой эффект, вы используете анимацию, модифицирующую свойство Width кнопки. Ниже приведен код, создающий и запускающий эту анимацию при щелчке на кнопке. DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 160; widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
Есть три детали, которые составляют необходимый минимум для описания анимации, использующей линейную интерполяцию: начальное значение (From), конечное значение (To) и время, за которое анимация должна выполниться (Duration). В данном примере конечное значение основано на текущей ширине содержащего кнопку окна. Эти три свойства присутствуют во всех классах анимации, использующих интерполяцию.
Book_Pro_WPF-2.indb 695
19.05.2008 18:11:15
696
Глава 21
Рис. 21.1. Анимированная кнопка Свойства From, To и Duration выглядят достаточно очевидными, но следует отметить несколько важных деталей. В следующих разделах эти свойства рассматриваются более подробно.
From Свойство From — начальное значение свойства Width. Если вы щелкнете на кнопке несколько раз, то всякий раз ширина кнопки будет сброшена до 160, и анимация запустится вновь. Так будет, даже если вы щелкнете на кнопке в процессе уже запущенной анимации. На заметку! Этот пример раскрывает еще одну сторону анимации WPF, а именно: всякое свойство зависимостей должно обрабатываться только одной анимацией в каждый момент времени. Если вы запустите вторую анимацию, первая будет автоматически отменена. Во многих ситуациях вам не понадобится, чтобы анимация всегда начиналась с исходного значения From. Обычно на то имеются две причины.
• У вас есть анимация, которая может быть запущена многократно и при этом давать совокупный эффект. Например, вы можете пожелать создать кнопку, которая становится чуть больше при каждом щелчке.
• У вас есть анимации, которые могут перекрываться. Например, вы можете использовать событие MouseEnter для запуска анимации, расширяющей кнопку, а событие MouseLeave — для активизации дополняющей анимации, которая вернет кнопку к исходному размеру. (Это известно под названием эффекта “рыбьего глаза”.) Если вы многократно быстро перемещаете курсор мыши на такую кнопку и обратно, то каждая анимация будет прерывать предыдущую, заставляя кнопку “прыгать” обратно к размеру, заданному в свойстве From.
Book_Pro_WPF-2.indb 696
19.05.2008 18:11:15
Анимация
697
Приведенный пример подпадает под вторую категорию. Если вы щелкнете на кнопке в процессе ее роста, то ширина будет тут же сброшена до 160 пикселей, что может быть несколько неприятно. Чтобы преодолеть эту проблему, просто исключите оператор, устанавливающий свойство From: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
Это одна уловка. Чтобы такая техника работала, анимируемое свойство должно иметь ранее установленное значение. В данном примере это означает, что кнопка должна иметь жестко закодированную ширину (заданную либо в дескрипторе кнопки, либо примененную установкой стиля). Проблема в том, что во многих контейнерах компоновки принято не указывать ширину, а позволять контейнеру управлять ею на основе свойств выравнивания элементов. В данном случае применяется значение ширины по умолчанию, которое равно специальному значению Double.NaN (где NaN означает “not a number” — “не число”). Вы не можете анимировать свойство, имеющее такое значение, с применением линейной интерполяции. Итак, каково же решение? Во многих случаях оно состоит в жестком кодировании ширины кнопки. Как вы убедитесь, анимация часто требует более тонкого контроля размеров элементов и их позиционирования, чем в случае ее отсутствия. Фактически, наиболее часто используемый контейнер компоновки для “анимируемого” содержимого — это Canvas, поскольку он позволяет легко перемещать содержимое по своей поверхности (с возможностью перекрытия) и изменять его размер. Canvas также является наиболее облегченным контейнером компоновки, поскольку ему не приходится выполнять никакой дополнительной работы по компоновке при изменении свойства вроде Width. В текущем примере у вас есть и другой выбор. Вы можете извлечь текущее значение кнопки, используя свойство ActualWidth, которое содержит текущую отображаемую ширину. Вы не можете анимировать ActualWidth (оно доступно только для чтения), но можете использовать его для установки свойства From вашей анимации: widthAnimation.From = cmdGrow.ActualWidth;
Такой прием работает как с анимацией на основе кода (как в данном примере), так и с декларативной анимацией, с которой вы познакомитесь позже (что потребует применения выражения привязки для получения значения ActualWidth). На заметку! В этом примере важно использовать именно свойство ActualWidth, а не Width. Это связано с тем, что Width отражает желаемую ширину, которую вы выбрали, а ActualWidth — используемую в данный момент текущую отображаемую ширину. Если вы применяете автоматическую компоновку, то вероятно, не захотите вообще устанавливать жестко закодированное значение Width, так что свойство Width просто вернет Double.NaN, и при попытке запустить анимацию будет сгенерировано исключение. Вам следует помнить еще об одной проблеме, когда вы используете текущее значение в качестве начальной точки анимации: это может изменить скорость анимации. Причина связана с тем, что длительность анимации не подстраивается, чтобы учесть меньшую дистанцию между начальным и конечным значением. Например, предположим, что вы создаете кнопку, не использующую значение From и выполняющую анимацию, начиная с текущей позиции. Если вы щелкнете на кнопке в тот момент, когда она почти достигла своей максимальной ширины, начнется новая анимация. Эта анимация сконфигурирована так, чтобы длиться пять секунд (задано в свойстве Duration), и это
Book_Pro_WPF-2.indb 697
19.05.2008 18:11:15
698
Глава 21
несмотря на то, что до максимальной ширины ей останется несколько пикселей. В результате рост кнопки тут же замедлится. Этот эффект проявляется только когда вы перезапускаете анимацию, которая почти завершена. Хотя это существенный недостаток, большинство разработчиков не пытаются написать код для его преодоления. Вместо этого просто считается, что такое поведение более-менее приемлемо. На заметку! Вы можете скомпенсировать эту проблему, написав некоторую специальную логику, которая будет модифицировать длительность анимации, хотя это редко стоит тех усилий. Чтобы сделать это, вы должны сделать предположение относительно стандартного размера кнопки (что ограничит возможность повторного использования кода), и вам понадобится описать анимацию программно, чтобы вы могли запустить этот код (а не декларативно, что более принято, как вы вскоре убедитесь).
To Точно так же, как вы пропустили свойство From, вы можете пропустить свойство To. Фактически вы можете не задавать оба свойства — и From, и To — создав анимацию вроде следующей: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.Duration = TimeSpan.FromSeconds(5); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
На первый взгляд эта анимация выглядит как сложный способ не делать вообще ничего. Логично предположить, что поскольку пропущены оба свойства — From и To, — они будут использовать одно и то же значение. Но между ними есть одно тонкое, однако, существенное отличие. Когда вы пропускаете From, то анимация использует текущее значение и принимает во внимание анимацию. Например, если кнопка находится в процессе роста, значение From использует увеличенную ширину. Однако когда вы опускаете To, анимация использует текущее значение, не принимая во внимания саму анимацию. По сути, это означает, что To принимает первоначальное значение — то, которое вы последний раз установили в коде, в дескрипторе элемента или с помощью стиля. (Это работает благодаря системе разрешения свойств WPF, которая в состоянии вычислить значение свойства на основе нескольких перекрывающихся поставщиков свойств, не отбрасывая никакой информации. Более подробно эта система рассматривается в главе 6.) Для примера с кнопкой это означает, что если вы запустите анимацию роста, а затем прервете ее анимацией, показанной ранее (возможно, щелчком на другой кнопке), то кнопка станет уменьшаться от того размера, до которого успела вырасти, и будет уменьшаться, пока не достигнет исходной ширины, указанной в разметке XAML. С другой стороны, если вы запустите этот код, когда никакая другая анимация не выполняется, то ничего не произойдет. Это объясняется тем, что значение From (анимируемая ширина) и значение To (исходная ширина) совпадают.
By Вместо To вы можете использовать свойство By. Свойство By служит для создания анимации, которая изменяет значение на определенную величину, а не до определенной величины. Например, вы можете создать анимацию, которая увеличивает кнопку на 10 единиц больше ее текущего размера, как показано ниже: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.By = 10; widthAnimation.Duration = TimeSpan.FromSeconds(0.5); cmdGrowIncrementally.BeginAnimation(Button.WidthProperty, widthAnimation);
Book_Pro_WPF-2.indb 698
19.05.2008 18:11:15
Анимация
699
Такой подход не является необходимым в данном примере, поскольку вы можете достичь того же результата, используя простое вычисление для установки свойства To, примерно так: widthAnimation.To = cmdGrowIncrementally.Width + 10;
Однако значение By более осмысленно, когда вы определяете анимацию в XAML, поскольку XAML не обеспечивает способа выполнения простых вычислений. На заметку! Вы можете использовать значения By и From совместно, но это не сократит объем работы. Значение By просто добавляется к значению From для достижения значения To. Свойство By предоставляется большинством, хотя и не всеми классами, использующими интерполяцию. Например, оно не имеет смысла для не числовых типов данных, таких как структура Color (применяемая в ColorAnimation). Есть только один способ получить аналогичное поведение без применения By — вы можете создать аддитивную анимацию, установив свойство IsAdditive. После этого текущее значение будет автоматически добавляться к обоим значениям — From и To. Например, рассмотрим следующую анимацию: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 0; widthAnimation.To = -10; widthAnimation.Duration = TimeSpan.FromSeconds(0.5); widthAnimation.IsAdditive = true;
Она начинается с текущего значения и завершается на значении, уменьшенном на 10 единиц. С другой стороны, если вы используете следующую анимацию: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 10; widthAnimation.To = 50; widthAnimation.Duration = TimeSpan.FromSeconds(0.5); widthAnimation.IsAdditive = true;
то свойство “прыгнет” к новому значению (которое на 10 единиц больше текущего) и затем будет расти до тех пор, пока не достигнет финальной величины, которое будет на 50 единиц больше текущего значения, существовавшего на момент старта анимации.
Duration Свойство Duration достаточно очевидно — оно принимает временной интервал (в миллисекундах, минутах, часах или любых других единицах, которые вы пожелаете) между моментом запуска анимации и временем ее завершения. Хотя длительность анимации в предыдущих примерах установлена с использованием TimeSpan, свойство Duration на самом деле требует объекта Duration. К счастью, Duration и TimeSpan достаточно похожи, и структура Duration предусматривает неявное приведение, которое при необходимости может конвертировать System.TimeSpan в System.Windows. Duration. Вот почему следующая строка кода вполне уместна: widthAnimation.Duration = TimeSpan.FromSeconds(5);
Так зачем же вводить целый новый тип? Duration также включает два специальных значения, которые не могут быть представлены объектом TimeSpan. Это Duration. Automatic и Duration.Forever. Ни одно из этих значений не применимо в нашем текущем примере. (Automatic просто устанавливает анимацию в односекундную длительность, а Forever задает бесконечную длительность анимации, что предотвращает проявление какого-либо эффекта.) Однако эти значения могут оказаться удобными при создании более сложной анимации.
Book_Pro_WPF-2.indb 699
19.05.2008 18:11:15
700
Глава 21
Одновременные анимации Вы можете использовать BeginAnimation() для запуска более одной анимации одновременно. Метод BeginAnimation() возвращает управления почти мгновенно, позволяя применять код, подобный показанному ниже, чтобы анимировать два свойства одновременно. DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.From = 160; widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); DoubleAnimation heightAnimation = new DoubleAnimation(); heightAnimation.From = 40; heightAnimation.To = this.Height - 50; heightAnimation.Duration = TimeSpan.FromSeconds(5); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation); cmdGrow.BeginAnimation(Button.HeightProperty, heightAnimation);
В данном примере две анимации не синхронизированы. Это значит, что ширина и высота не будут расти точно в течение одного интервала. (Обычно вы увидите, что кнопка сначала растет в ширину, а потом — в высоту.) Это ограничение можно обойти, создав анимации, которые ограничены одной временной шкалой. Эту технику мы изучим позднее в настоящей главе, когда пойдет речь о раскадровках.
Время жизни анимации Технически анимации WPF является временными, а это означает, что они в действительности не изменяют значения лежащего в основе свойства. Пока анимация активна, она просто перекрывает значение свойства. Это связано со способом работы свойства зависимостей (как описано в главе 6) и часто имеет не выявленные детали, которые могут вызвать серьезную путаницу. Однонаправленная анимация (как наша анимация роста кнопки) остается активной и после завершения ее работы. Это объясняется тем, что анимация должна удерживать ширину кнопки в новом размере. Это может привести к неожиданной проблеме, а именно: если вы попытаетесь модифицировать значение свойства в коде после завершения анимации, то такой код не будет иметь никакого эффекта. Так происходит потому, что код просто присваивает свойству новое локальное значение, но анимированное значение имеет приоритет перед ним. Вы можете решить эту проблему несколькими способами, в зависимости от того, чего хотите достичь.
• Создать анимацию, которая сбрасывает ваши элементы в их исходное состояние. Это делается посредством не установки свойства To. Например, анимация уменьшения кнопки сокращает ее ширину в ее последнее установленное значение, после чего вы можете изменять ее в коде.
• Создать обратимую анимацию. Это делается посредством установки свойства AutoReverse в true. Например, когда завершается анимация увеличения кнопки, она запускается в обратном направлении, возвращая кнопку в ее исходное состояние. Общая длительность вашей анимации при этом удвоится.
• Изменить свойство FillBehavior. Изначально FillBehavior установлено в HoldEnd, а это означает, что когда анимация завершится, ее финальное значение будет применено к целевому свойству. Если вы измените FillBehavior на Stop, то как только анимация завершится, свойство вернется в свое исходное значение.
Book_Pro_WPF-2.indb 700
19.05.2008 18:11:15
Анимация
701
• Удалить объект анимации по ее завершении, обработав событие Completed объекта анимации. Первые три способа изменяют поведение вашей анимации. Так или иначе, они возвращают анимированному свойству его первоначальное значение. Если это не то, что вы хотите, то используйте последний способ. Прежде чем запустить анимацию, присоедините обработчик событий, который будет реагировать на завершение анимации: widthAnimation.Completed += animation_Completed;
На заметку! Completed — это нормальное событие .NET, которое принимает обычный объект EventArgs с дополнительной информацией. Это не маршрутизируемое событие. Когда возникает событие Completed, вы можете вновь привести анимацию в действие, вызвав метод BeginAnimation(). Вам просто нужно специфицировать свойство и передать null-ссылку для объекта анимации: cmdGrow.BeginAnimation(Button.WidthProperty, null);
При вызове BeginAnimation() свойство возвращается к значению, которое оно имело на момент запуска анимации. Если это не то, что вам нужно, вы можете запомнить значение, которое получило свойство в результате анимации, и затем вручную установить его, как показано ниже: double currentWidth = cmdGrow.Width; cmdGrow.BeginAnimation(Button.WidthProperty, null); cmdGrow.Width = currentWidth;
Имейте в виду, что это изменит локальное значение свойства. Это может повлиять на работу других анимаций. Например, если вы анимируете эту кнопку анимацией, не специфицирующей значение From, оно использует установленное значение в качестве начального. В большинстве случаев это именно то, что вам нужно.
Класс TimeLine Как вы видели, каждая анимация вращается вокруг нескольких ключевых свойств. Вы уже познакомились с некоторыми из них: From и To (представленными в классе анимации, использующем интерполяцию), а также Duration и FillBehavior (представленными во всех классах анимации). Перед тем, как двинуться дальше, стоит внимательнее взглянуть на свойства, с которыми вам придется работать. На рис. 21.2 показана иерархия наследования типов анимации WPF. Она включает базовые классы, но пропускает 42 типа анимаций (вместе с соответствующими классами ИмяТипаAnimationBase). Иерархия классов включает три главных ветви, унаследованные от абстрактного класса TimeLine. MediaTimeLine используется при выполнении аудио- и видеофайлов; это описано в главе 22. AnimationTimeline служит для системы анимации на основе свойств, которую мы рассматривали до сих пор. И, наконец, TimelineGroup позволяет синтезировать временные шкалы и контролировать их воспроизведение. Мы опишем это далее в этой главе, в подразделе “Одновременные анимации” раздела, посвященного раскадровкам. Первые используемые члены появляются в классе Timeline, определяющем свойство Duration, которое мы уже рассматривали, плюс некоторые другие. Свойства этого класса перечислены в табл. 21.1. Хотя BeginTime, Duration, SpeedRatio и AutoReverse достаточно очевидны, некоторые другие свойства требуют более тщательного рассмотрения. В следующих разделах мы исследуем AccelerationRatio, DecelerationRatio и RepeatBehavior.
Book_Pro_WPF-2.indb 701
19.05.2008 18:11:15
702
Глава 21 DispatcherObject
Условные обозначения Абстрактный класс
DependencyObject Конкретный класс
Freezable
Animatable
Timeline
MediaTimeline
AnimationTimeline
... TimelineGroup
ParallelTimeline
Storyboard
DoubleAnimationBase
ColorAnimationBase
DoubleAnimation
...
StringAnimationBase
...
DoubleAnimationUsingKeyFrames
DoubleAnimationUsingPath
Рис. 21.2. Иерархия классов анимации
Таблица 21.1. Свойства Timeline Имя
Описание
BeginTime
Устанавливает задержку перед запуском анимации (как TimeSpan). Эта задержка добавляется к общему времени, так что пятисекундная анимация с пятисекундной задержкой займет в сумме десять секунд. Свойство BeginTime удобно для синхронизации разных анимаций, которые запускаются в одно и то же время, но должны выполнять свои действия последовательно.
Duration
Устанавливает длительность времени выполнения анимации, от старта до финиша, как объект Duration.
SpeedRatio
Увеличивает или уменьшает скорость анимации. Изначально SpeedRatio равно 1. Если вы увеличите его, то анимация завершится быстрее (например, SpeedRatio, равное 5, выполнит анимацию впятеро быстрее). Если вы уменьшите значение этого свойства, анимация замедлится (например, установив SpeedRatio равным 0.5, вы получите анимацию, выполняющуюся вдвое дольше). Вы можете изменять Duration вашей анимации для получения того же результата. Свойство SpeedRatio не принимается во внимание, когда применяется задержка BeginTime.
Book_Pro_WPF-2.indb 702
19.05.2008 18:11:16
Анимация
703
Окончание табл. 21.1 Имя
Описание
AccelerationRatio
Делает анимацию нелинейной, так что она запускается медленно, затем происходит ускорение (посредством увеличения AccelerationRatio) либо замедление (при увеличении DecerationRatio). Оба значения устанавливаются от 0 до 1 и начинаются с 0. Более того, сумма обоих величин не может превышать 1.
и DecerationRatio
AutoReverse
Если это свойство равно true, то анимация будет запущена в обратном порядке, как только завершится. Если увеличить SpeedRatio, оно будет применено как к прямому воспроизведению анимации, так и к обратному. Свойство BeginTime применяется только в самом начале анимации — задержки при запуске в обратном направлении не происходит.
FillBehavior
Определяет то, что случится по завершении анимации. Обычно свойство остается зафиксированным в конечном значении (FillBehavior. HoldEnd), но вы можете также выбрать возврат к исходному значению (FillBehavior.Stop)
RepeatBehavior
Позволяет повторить анимацию заданное количество раз, либо в течение указанного интервала времени. Объект RepeatBehavior, используемый для установки этого свойства, определяет конкретное поведение.
AccelerationRatio и DecelerationRatio AccelerationRatio и DecelerationRatio позволяет сжать часть временной шкалы, так что она будет пройдена быстрее. Остальная часть временной шкалы будет сжата, чтобы компенсировать это, так чтобы общее время осталось неизменным. Оба эти свойства представляют процентное значение. Например, AccelerationRatio, равное 0.3, указывает на то, что вы хотите потратить 30% общей длительности анимации на ускорение. Например, в десятисекундной анимации первые три секунды будут взяты с ускорением, а остальные семь секунд пройдут на постоянной скорости. (Очевидно, что скорость в эти последние семь секунд будет выше, чем у неускоренной анимации, поскольку необходимо выполнить медленный старт.) Если вы установите AccelerationRatio равным 0.3 и DecelerationRatio также равным 0.3, то ускорение будет выполняться в первые 3 секунды, следующие 4 секунды пройдут на постоянной скорости, а в последние три секунды произойдет замедление. Если все это представить таким образом, станет ясно, что сумма AccelerationRatio и DecelerationRatio не может превысить 1, поскольку невозможно потратить больше 100% времени анимации на ее ускорение и замедление. Конечно, вы можете установить AccelerationRatio равным 1 (при этом скорость анимации будет расти от начала до ее конца), или же вы можете установить DecelerationRatio равным 1 (при этом анимации будет замедляться от начала до конца). Анимации с ускорением и замедлением часто используются для обеспечения более естественного поведения. Однако AccelerationRatio и DecelerationRatio предоставляют вам лишь относительный контроль. Например, эти свойства не дают возможности варьировать степень ускорения либо устанавливать ее специально. Если вам нужна анимация, использующая неравномерное ускорение, то вам придется определить серию анимаций, следующих одна за другой, и установить свойства AccelerationRatio и DecelerationRatio каждой из них отдельно, или же вам нужно будет использовать анимацию ключевого кадра со сплайновыми кадрами (как будет описано в конце настоящей главы). Хотя такая техника предоставляет определенную степень гибкости, отслеживание всех этих деталей затруднено, и для ее успешного применения желательно использовать инструмент дизайна, способный конструировать ваши анимации.
Book_Pro_WPF-2.indb 703
19.05.2008 18:11:16
704
Глава 21
RepeatBehavior Свойство RepeatBehavior позволяет управлять повторениями анимации. Если вы хотите повторить ее фиксированное количество раз, передайте нужное число конструктору RepeatBehavior. Например, следующая анимация повторится дважды: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); widthAnimation.RepeatBehavior = new RepeatBehavior(2); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
Когда вы запустите эту анимацию, кнопка увеличится в размере (в течение пяти секунд), прыжком вернется к исходному размеру и вырастет снова (опять в течение пяти секунд), заполнив всю ширину окна. Если вы установите AutoReverse в true, то поведение будет слегка отличаться. Полная анимация пройдет вперед и назад (т.е. кнопка вырастет, а затем уменьшится), а затем все повторится опять. На заметку! Анимации, использующие интерполяцию, предоставляют свойство IsCumulative, которое сообщает WPF, что нужно делать с каждым повтором. Если IsCumulative равно true, то анимация не повторяется от начала до конца. Вместо этого каждый последовательный ее проход добавляется к предыдущему. Например, если вы используете IsCumulative с анимацией, описанной выше, то кнопка расширится в два раза больше за вдвое большее время. Говоря иначе, первая итерация выполнится нормально, а все последующие — так, будто вы установили IsAdditive в true. Вместо применения RepeatBehavior для установки счетчика повторов вы можете также использовать его для установки интервала. Чтобы сделать это, просто передайте TimeSpan конструктору RepeatBehavior. Например, следующая анимация будет повторяться в течение 13 секунд: DoubleAnimation widthAnimation = new DoubleAnimation(); widthAnimation.To = this.Width - 30; widthAnimation.Duration = TimeSpan.FromSeconds(5); widthAnimation.RepeatBehavior = new RepeatBehavior(TimeSpan.FromSeconds(13)); cmdGrow.BeginAnimation(Button.WidthProperty, widthAnimation);
В данном примере свойство Duration специфицирует, что вся анимация занимает 5 секунд. В результате RepeatBehavior в 13 секунд вызовет два повтора и затем остановит рост кнопки на полпути при третьем проходе анимации (на отметке 3 секунды). Совет. Вы можете применять RepeatBehavior для выполнения только части анимации. Чтобы сделать это, указывайте дробную часть повторов или же используйте значение TimeSpan, меньшее длительности вашей анимации. И, наконец, вы можете заставить анимацию повторяться бесконечно, передав значение RepeatBehavior.Forever: widthAnimation.RepeatBehavior = RepeatBehavior.Forever;
Декларативная анимация и раскадровки Как вы видели, анимации WPF представлены группой классов анимации. Вы устанавливаете существенную информацию, такую как начальное значение, конечное значение и длительность, используя несколько свойств. Это очевидно делает их удобными для применения XAML. Что менее ясно — так это как привязать анимацию к конкретному элементу и свойству, и как инициировать ее в нужное время.
Book_Pro_WPF-2.indb 704
19.05.2008 18:11:16
705
Анимация
Это приводит к тому, что в декларативной анимации должны присутствовать два описанных ниже ингредиента.
• Раскадровка. Это XAML-эквивалент метода BeginAnimation(). Он позволяет направить анимацию на правильный элемент и свойство.
• Триггер события. Отвечает за изменением свойства или событие (такое как Click для кнопки) и управляет раскадровкой. Например, чтобы запустить анимацию, триггер события должен начать раскадровку. Из следующих разделов вы узнаете, как работают эти оба ингредиента.
Раскадровка Раскадровка (storyboard) — это усовершенствованная временная шкала. Вы можете применять ее для группировки множества анимаций и, кроме того, она имеет способность контролировать воспроизведение анимации — приостанавливать ее, прекращать и изменять текущую позицию. Однако самые базовые средства, предлагаемые классом StoryBoard — это способность указывать на определенное свойство и определенный элемент, используя свойства TargetProperty и TargetName. Другими словами, раскадровка заполняет пробел между анимацией и свойством, которое вы хотите анимировать. Вот как можно определить раскадровку, управляющую DoubleAnimation:
И TargetProperty, и TargetName — прикрепленные свойства. Это означает, что вы можете применять их непосредственно к анимации, как показано ниже:
Этот синтаксис более распространен, поскольку он позволяет вам поместить несколько анимаций на одну раскадровку, но при этом позволяет каждой анимации действовать на разные элементы и свойства. Определение раскадровки — первый шаг в создании анимации. Чтобы действительно запустить эту раскадровку в действие, вам понадобится триггер события.
Триггеры событий Впервые вы узнали о триггерах событий в главе 12, когда шла речь о стилях. Стили предоставляют один способ подключения триггера события к элементу. Однако вы можете определить триггер события в четырех местах:
• в стиле (коллекция Styles.Triggers); • в шаблоне данных (коллекция DataTemplate.Triggers); • в шаблоне элемента управления (коллекция ControlTemplate.Triggers); • непосредственно в элементе (коллекция FrameworkElement.Triggers). При создании триггера события вам нужно указать маршрутизируемое событие, которое запускает триггер и действие (или действия), выполняемые триггером. В случае анимаций наиболее часто используемое действие — это BeginStoryboard, которое эквивалентно вызову BeginAnimation().
Book_Pro_WPF-2.indb 705
19.05.2008 18:11:16
706
Глава 21
В следующем примере коллекция Triggers кнопки применяется для присоединения анимации к событию Click. Когда выполняется щелчок на кнопке, она начинает расти. Click and Make Me Grow
Совет. Чтобы создать анимацию, которая запускается при первой загрузке окна, добавьте триггер события в коллекцию Window.Triggers, отвечающую на событие Window.Loaded. Свойство Storyboard.TargetProperty идентифицирует свойство, которое вы хотите изменять (в данном случае — Width). Если вы не укажете имя класса, то раскадровка использует родительский элемент, которым является кнопка, которую нужно расширить. Если вы хотите установить прикрепленное свойство (например, Canvas.Left или Canvas.Top), то должны заключить все свойство в скобки, примерно так:
Свойство Storyboard.TargetName в данном примере не требуется. Если вы опустите его, то раскадровка использует родительский элемент, которым является кнопка. На заметку! Все триггеры событий способны выполнять действия. Все действия представлены классами, унаследованными от System.Windows.TriggerAction. В настоящее время WPF включает очень небольшой набор действий, предназначенных для взаимодействия с раскадровкой и контроля воспроизведения медиа. Существует разница между представленным здесь декларативным подходом и подходом только на основе кода, который был продемонстрирован выше. А именно: значение To жестко закодировано в 300 единиц, а не установлено относительно содержащего кнопку окна. Если вы хотите использовать ширину окна, то должны использовать выражение привязки данных, вроде следующего:
Это все еще не даст точно такого результата, какой вам нужно. Здесь кнопка растет от ее текущего размера до полной ширины окна. Подход на основе только кода увеличивает кнопку до размера на 30 единиц меньшего, чем полный размер, используя тривиальное вычисление. К сожалению, XAML не поддерживает встроенных вычислений. Одним решением этой проблемы может быть построение IValueConverter, который выполнит эту работу для вас. К счастью, этот хитрый трюк легко реализовать (и многим
Book_Pro_WPF-2.indb 706
19.05.2008 18:11:16
Анимация
707
разработчикам приходится это делать). Вы можете найти пример такого подхода по адресу http://blogs.msdn.com/llobo/archive/2006/11/13/Arithmetic-operationsin-Xaml.aspx или же заглянуть в загружаемый код для этой главы. На заметку! Другой вариант — создать специальное свойство зависимостей в классе вашего окна, которое выполнит вычисление. Вы можете затем привязать вашу анимацию к этому свойству зависимостей. Более подробно о создании свойств зависимостей читайте в главе 6. Теперь вы можете дублировать примеры, приведенные выше, создав триггеры и раскадровки и установив соответствующие свойства объекта DoubleAnimation.
Присоединение триггеров к стилю Коллекция FrameworkElement.Triggers — довольно причудливая вещь. Она поддерживает только триггеры событий. Другие коллекции триггеров (Styles.Triggers, DataTemplate.Triggers и ControlTemplate.Triggers) более гибки. Они поддерживают три базовых типа триггеров WPF: триггеры свойств, триггеры данных и триггеры событий. На заметку! Не существует технических причин, запрещающих FrameworkElement.Triggers поддерживать дополнительные типы триггеров, но эта функциональность не была реализована на момент выхода первой версии WPF. Использование триггеров событий — наиболее распространенный путь присоединения анимации. Однако это не единственный вариант. Если вы используете коллекцию Triggers в стиле, шаблоне данных либо в шаблоне элементов управления, то вы можете также создать триггер свойства, который будет реагировать на изменения значения свойства. Например, ниже приведен стиль, который дублирует пример, приведенный выше. Он инициирует раскадровку, когда IsPressed равно true.
Вы можете присоединить действия к триггеру свойств двумя способами. Можно использовать Trigger.EnterActions, чтобы установить действия, которые будут выполняться при изменении свойства на указанное вами значение (в предыдущем примере — когда IsPressed получает значение true), и применить Trigger.ExitActions для установки действий, которые будут выполняться при обратном изменении свойства (т.е. когда IsPressed вернется к значению false). Это удобный способ связать вместе пару взаимодополняющих анимаций. Вот как выглядит кнопка, использующая стиль, показанный выше:
Book_Pro_WPF-2.indb 707
19.05.2008 18:11:16
708
Глава 21
Click and Make Me Grow
Помните, что вам не нужно использовать триггеры свойств в стиле. Вы можете также применять триггеры свойств, как было показано в предыдущем разделе. И, наконец, вам не нужно определять стиль отдельно от кнопки, использующей его (вы можете установить свойство Button.Style встроенным образом), но разделение на две части более распространено, и оно обеспечивает вам гибкость для применения одной и той же анимации к множеству элементов.
Присоединение триггеров к шаблону Одним из наиболее мощных способов повторно использовать анимацию является определение шаблона. В главе 15 вы видели стилизованный ListBox, который использовал криволинейные границы и заштрихованный фон. Этот ListBox также использовал триггеры свойств для изменения размера шрифта ListBoxItem при прохождении над ним курсора мыши. Этот эффект был несколько раздражающим, поскольку текст должен был немедленно “прыгать” от своего начального шрифта к новому, большего размера. Применив анимацию, можно добиться более гладкого перехода, увеличивая размер шрифта постепенно, в течение краткого интервала времени. И поскольку ListBoxItem может иметь свою собственную анимацию, когда вы перемещаете курсор мыши вверх и вниз по списку, то увидите несколько раз, как позиция в списке начнет увеличиваться в размере и вслед за тем сразу уменьшится, создавая занимательный эффект “рыбьего глаза”. (Еще более экстравагантный эффект может заключаться в увеличении и некотором искажении элемента, над которым проходит курсор мыши. Как вы вскоре увидите, это также возможно в WPF при использовании анимированных трансформаций.) Хотя невозможно продемонстрировать этот эффект на одной статичной картинке, все же на рис. 21.3 показан снимок экрана с этим списком после того, как были выполнены несколько быстрых перемещений мыши. Здесь мы хотим пересмотреть весь пример шаблона ListBoxItem, потому что он был построен из множества разных частей, стилизующих ListBox, ListBoxItem и различные компоненты ListBox (такие как полоса прокрутки). Важнейшая часть, которая нас интересует — стиль, изменяющий шаблон ListBoxItem. Вы можете добавить дополнительную анимацию двумя равноценными способами — создав триггер события, Рис. 21.3. Индивидуальная ани- реагирующий на события MouseOver и MouseLeave, или же создав триггер свойства, который добавит действия мация каждого ListBoxItem входа и выхода при изменении свойства IsMouseOver. В следующем примере применяется подход с триггером события.
Book_Pro_WPF-2.indb 708
19.05.2008 18:11:16
Анимация
709
В данном примере ListBoxItem увеличивается относительно медленно (в течение одной секунды), затем уменьшается намного быстрее (за 0,2 секунды). Однако перед тем как начинается анимация уменьшения, происходит полусекундная задержка. Отметим, что для анимации уменьшения не указаны свойства From и To. Таким образом, уменьшение текста строки списка всегда происходит от текущего размера до исходного, как было описано выше. Если вы переместите курсор мыши на элемент ListBoxItem и обратно, то получите ожидаемый результат — это будет выглядеть так, что элемент будет продолжать увеличиваться, пока курсор мыши расположен над ним, и продолжать уменьшаться, когда он покинет его поверхность. Совет. Этот пример прекрасно работает, но это еще не самая забавная анимация, которую вы увидите. Всякий раз, когда размер ListBoxItem изменяется, WPF должен заставить компоновку упорядочить все элементы в ListBox. Именно подобные моменты служат причиной того, что анимация происходит вне контейнеров компоновки и используется более простой (и более гибкий) контейнер Canvas.
Перекрывающиеся анимации Раскадровка предоставляет возможность изменять способ работы с перекрывающимися анимациями — другими словами, когда вторая анимация применяется к свойству, которое уже анимируется. Это делается через свойство BeginStoryboard. HandoffBehavior. Обычно, когда анимации перекрываются, то вторая перекрывает первую немедленно. Такое поведение называется “снимок и замена” (snapshot-and-replace) и представляется значением SnapshotAndReplace из перечисления HandoffBehavior. Когда стартует вторая анимация, делается снимок свойства в его текущем состоянии (полученном в результате первой анимации), останавливается старая анимация и заменяется новой.
Book_Pro_WPF-2.indb 709
19.05.2008 18:11:17
710
Глава 21
Единственная альтернативная опция из перечисления HandoffBehavior — это Compose, которая “вливает” вторую анимацию во временную шкалу первой анимации. Например, рассмотрим измененную версию примера ListBox, использующую HandoffBehavior.Compose при уменьшении элемента списка:
Теперь, если вы переместите курсор мыши на ListBoxItem и обратно, то увидите другое поведение. Убирая курсор мыши с элемента, вы увидите, что он продолжает увеличиваться до тех пор, пока не достигнет начала полусекундной задержки. Затем вторая анимация уменьшит элемент. Если не указано поведение Compose, то элемент будет просто ожидать, зафиксированный в своем текущем размере, в течение 0,5 секунды, пока не начнется вторая анимация. Использование HandoffBehavior при композиции анимаций требует дополнительных накладных расходов. Это связано с тем, что часы, используемые для запуска исходной анимации, не могут быть остановлены при запуске второй анимации. Вместо этого она остается активной до тех пор, пока сборщик мусора не удалит ListBoxItem, или же пока не будет использована новая анимация на том же свойстве. Совет. Если производительность становится проблемой, команда разработчиков WPF рекомендует вручную отпускать часы анимации, как только она будет закончена (а не ожидать, пока сборщик мусора найдет ее). Чтобы сделать это, вам нужно обработать событие типа Storyboard. Completed. Затем потребуется вызвать BeginAnimation() для элемента, чья анимация только что завершилась, применяя соответствующее свойство и null-ссылку вместо анимации.
Одновременные анимации Класс Storyboard непрямо унаследован от TimeLineGroup, что дает ему возможность поддерживать более одной анимации. Лучше всего то, что эти анимации управляются как единая группа — в том смысле, что запускаются одновременно. Для примера рассмотрим следующую раскадровку. Она запускает две анимации: одну, работающую со свойством Width кнопки, и вторую, имеющую дело со свойством Height. Поскольку анимации сгруппированы на одной раскадровке, они увеличивают размеры кнопки в унисон, что дает более синхронизированный эффект, чем просто многократный вызов BeginAnimation() в коде.
Book_Pro_WPF-2.indb 710
19.05.2008 18:11:17
711
Анимация
В этом примере обе анимации имеют одинаковую длительность, хотя это требование не обязательно. Единственное, что следует принимать во внимание для анимаций, завершающихся в разное время — это их FillBehavior. Если свойство FillBehavior анимации установлено в HoldEnd, она удерживает значение анимируемого свойства до тех пор, пока не завершатся все анимации, определенные на раскадровке. Если свойство раскадровки FillBehavior равно HoldEnd, то финальное анимированное значение удерживается независимо (до тех пор, пока новая анимация не заменить его, или пока вы вручную не удалите анимацию). И здесь становится ясно, в чем практическая польза свойств, описания которых вы видели в табл. 21.1. Например, вы можете использовать SpeedRatio для того, чтобы заставить одну анимацию на раскадровке выполняться быстрее остальных. Или же вы можете применить BeginTime, чтобы сдвинуть одну анимацию относительно другой, чтобы она запускалась в определенный момент времени. На заметку! Поскольку StoryBoard наследуется от Timeline, вы можете использовать все свойства, которые были описаны в табл. 21.1, чтобы настроить скорость, применять ускорение или замедление, ввести время задержки и т.д. Эти свойства касаются всех содержащихся анимаций, и все они дают кумулятивный эффект. Например, если вы установите Storyboard. SpeedRatio равным 2, то данная анимация будет выполняться вдвое быстрее, чем обычно.
Управление воспроизведением До сих пор вы использовали одно действие в триггерах событий — BeginStoryboard, которое запускает анимацию. Однако вы можете применять и несколько других действий, чтобы управлять созданной однажды раскадровкой. Эти действия, унаследованные от класса ControllableStoryboardAction, перечислены в табл. 21.2.
Таблица 21.2. Классы действий для управления раскадровкой Класс
Описание
PauseStoryboard
Приостанавливает воспроизведение анимации и сохраняет ее в текущей позиции.
ResumeStoryboard
Возобновляет воспроизведение приостановленной анимации.
StopStoryboard
Останавливает воспроизведение анимации и сбрасывает ее таймер в начало.
SeekStoryboard
Перепрыгивает в определенную точку временной шкалы анимации. Если анимация в данный момент воспроизводится, то воспроизведение продолжается с новой позиции. Если же анимация приостановлена, она остается приостановленной.
SetStoryboardSpeedRatio
Изменяет SpeedRatio всей раскадровки (а не только одной анимации внутри нее).
SkipStoryboardToFill
Перемещает раскадровку в конец ее временной шкалы. Этот период известен как область заполнения (fill behavior). Для стандартной анимации, у которой FillBehavior установлен в HoldEnd, анимация продолжается для удержания финального значения.
RemoveStoryboard
Удаляет раскадровку, прерывая все текущие исполняющиеся анимации и возвращая свойства в исходные, установленные последний раз значения. Это дает тот же эффект, что и вызов BeginAnimation() для соответствующего элемента с nullобъектом анимации.
Book_Pro_WPF-2.indb 711
19.05.2008 18:11:17
712
Глава 21
На заметку! Остановка анимации не эквивалентна ее завершению (если только FillBehavior не установлен в Stop). Это объясняется тем, что даже когда анимация достигает конца своей временной шкалы, она продолжает удерживать финальное значение свойства. Аналогично, когда анимация приостановлена, она продолжает удерживать текущее промежуточное значение свойства. Однако когда анимация остановлена окончательно, она более не применяет никакого значения, и значение свойства возвращается к своей исходной величине. Однако при использовании этих действий есть одна недокументированная особенность. Чтобы они успешно работали, вы должны определить все триггеры в одной коллекции Triggers. Если вы поместите действие BeginStoryboard в другую коллекцию триггеров, отличную от PauseStoryboard, то действие PauseStoryboard работать не будет. Для того чтобы продемонстрировать дизайн, который следует применять, рассмотрим пример. Возьмем окно, представленное на рис. 21.4. Оно накладывает два элемента Image точно в одной позиции, используя при этом сетку (grid). Изначально видимо только одно изображение — фрагмент знаменитой башни Торонто днем. Но при запуске анимации прозрачность этого изображения снижается от 1 до 0, постепенно проявляя ночной снимок с того же места, пока на Торонто не опустится ночь. Эффект похож на последовательное отображение ряда фотографий, сделанных через равные промежутки времени.
Рис. 21.4. Управляемая анимация Ниже показана разметка, определяющая Grid с двумя изображениями:
А вот анимация, которая постепенно заменяет одно изображение другим:
Чтобы сделать этот пример еще более интересным, в него включено несколько кнопок в нижней части окна, которые позволяют управлять воспроизведением этой анимации. Используя эти кнопки, вы можете выполнять обычные действия по воспроиз-
Book_Pro_WPF-2.indb 712
19.05.2008 18:11:17
Анимация
713
ведению медиафайлов — паузу, продолжение и останов. (Вы можете добавить и другие кнопки, чтобы изменять скорость или указывать определенную точку во времени.) Вот разметка, определяющая эти кнопки: Start Pause Resume Stop Move To Middle
Обычно триггер события помещается в коллекцию Triggers каждой индивидуальной кнопки. Однако, как упоминалось ранее, это не работает с анимациями. Простейшее решение — определить все триггеры событий в одном месте, таком как коллекция Triggers содержащего кнопки элемента, и привязать их с помощью свойства EventTrigger.SourceName. Когда SourceName соответствует свойству Name кнопки, триггер применяется к этой кнопке. В данном примере вы можете использовать коллекцию Triggers объекта StackPanel, содержащего кнопки. Однако часто легче использовать коллекцию Triggers элемента верхнего уровня, в данном случае — окна. Подобным образом вы сможете перемещать кнопки в разные места пользовательского интерфейса, не теряя их функциональности.
Обратите внимание, что вы должны дать имя действию BeginStoryboard (в рассматриваемом примере — fadeStoryboardBegin). Другие триггеры специфицируют имя в свойстве BeginStoryboardName, чтобы привязаться к одной и той же раскадровке. Используя действия раскадровки, вы столкнетесь с одним ограничением. Свойства, которые они предоставляют (такие как SeekStoryboard.Offset и SetStoryboardSpeedRatio.SpeedRatio ), не являются свойствами зависимостей. Это ограничивает ваши возможности по использованию выражений привязки данных. Например, вы не можете автоматически читать свойство Slider.Value и применять его к действию SetStoryboardSpeedRatio.SpeedRatio, потому что свойство
Book_Pro_WPF-2.indb 713
19.05.2008 18:11:17
714
Глава 21
SpeedRatio не принимает выражений привязки данных. Может показаться, что это ограничение можно обойти, написав некоторый код, использующий SpeedRatio объекта Storyboard, но это не сработает. Когда анимация стартует, значение SpeedRatio читается и используется для создания таймера анимации. Если вы измените его после этого момента, анимация будет продолжаться в нормальном темпе. Если вы хотите динамически изменять скорость и позицию воспроизведения, то единственное решение — воспользоваться кодом. Класс Storyboard предоставляет методы, обеспечивающие ту же функциональность, что и триггеры, описанные в табл. 21.2, в том числе Begin(), Pause(), Seek(), Stop(), SkipToFill(), SetSpeedRatio() и Remove(). Чтобы получить доступ к объекту Storyboard, вы должны удостовериться, что установили в коде разметки свойство Name:
На заметку! Не путайте имя объекта Storyboard (которое необходимо, чтобы использовать раскадровку в вашем коде) с именем действия BeginStoryboard (которое нужно, чтобы привязать другие действия триггера, манипулирующие раскадровкой). Чтобы предотвратить коллизии, вы можете принять соглашение по именованию, например, добавляя слово Begin в конец имени BeginStoryboard. Теперь вам просто нужно написать соответствующий обработчик событий и воспользоваться методами объекта Storyboard. (Напомним, что простое изменение свойств раскадровки, подобных SpeedRatio, не даст никакого эффекта. Они просто конфигурируют установки, которые используются на момент запуска анимации.) Ниже приведен обработчик событий, реагирующий на перетаскивание ползунка Slider. Он получает значение ползунка (находящееся в пределах от 0 до 3) и использует его для применения нового масштаба скорости: private void sldSpeed_ValueChanged(object sender, RoutedEventArgs e) { fadeStoryboard.SetSpeedRatio(this, sldSpeed.Value); }
Обратите внимание, что SetSpeedRatio() принимает два аргумента. Первый — контейнер анимации верхнего уровня (в данном случае — текущее окно). Все методы раскадровки требуют этой ссылки. Второй аргумент — новая скорость.
Эффект вытеснения Предыдущий пример представляет постепенный переход между двумя изображениями, который обеспечивается изменением свойства Opacity изображения, находящегося сверху. Другой распространенный способ перехода между изображениями заключается в вытеснении (wipe), которое позволяет постепенно “снимать” верхнее изображение с нижнего. Основной трюк в использовании такой техники заключается в создании маски непрозрачности (opacity mask) для верхнего изображения. Вот пример:
Book_Pro_WPF-2.indb 714
19.05.2008 18:11:17
Анимация
715
Маска непрозрачности использует градиент, определяющий две точки останова: Black (где изображение будет полностью видимым) и Transparent (где изображение будет полностью прозрачным). Изначально обе точки останова позиционируются на левой границе изображения. Поскольку точка останова видимости определена первой, она имеет приоритет, и изображение в ней будет полностью непрозрачным. Обратите внимание, что обе точки именованы, и поэтому легко могут быть доступны вашей анимации. Затем необходимо запустить анимацию по смещениям (offset) LinearGradientBrush. В данном примере оба смещения движутся слева направо, приоткрывая нижнее изображение. Чтобы сделать этот пример более привлекательным, смещения не занимают одинаковую позицию в процессе движения. Вместо этого смещение видимости лидирует, а смещение прозрачности следует за ним с задержкой 0,2 секунды. В процессе анимации это создает смешанную область на границе области вытеснения.
Здесь есть одна неприятная деталь. Видимый останов перемещается на 1,2 вместо 1, что отмечает правую грань изображения. Это гарантирует, что оба смещения будут двигаться с одинаковой скоростью, поскольку общее расстояние, которое должно покрыть каждое из них, пропорционально длительности анимации. Вытеснение обычно осуществляется слева направо или сверху вниз, но при использовании масок непрозрачности возможны и более изощренные эффекты. Например, вы можете использовать DrawingBrush для вашей маски непрозрачности и модифицировать ее геометрию, чтобы приоткрывать нижнее изображение с применением какого-то замысловатого шаблона. Далее в этой главе вы еще встретите примеры анимации кистей.
Отслеживание хода анимации Проигрывателю анимации, показанному на рис. 21.4, все еще не хватает одного средства, которое присутствует в большинстве медиа-проигрывателей, а именно: возможности определения текущей позиции. Чтобы сделать его более забавным, вы можете добавить некоторый текст, который показывает временное смещение и панель прохождения, представляющую визуальную индикацию хода анимации. На рис. 21.5 показан усовершенствованный проигрыватель анимации с обеими деталями (вместе с ползунком для управления скоростью, о котором речь шла в предыдущем разделе). Добавить эти элементы достаточно просто. Сначала вам нужен элемент TextBlock, чтобы показать время, и элемент управления ProgressBar для отображения графической панели. Вы можете предположить, что значение TextBlock и содержимое ProgressBar следует устанавливать с помощью выражения привязки данных, однако это невозможно. Дело в том, что единственный способ получения информации о текущем состоянии анимации от Storyboard заключается в применении таких методов, как GetCurrentTime() и GetCurrentProgress(). Нет никакой возможности получить ту же информацию через свойства. Простейшее решение — реагировать на одно из событий раскадровки, перечисленных в табл. 21.3.
Book_Pro_WPF-2.indb 715
19.05.2008 18:11:18
716
Глава 21
Рис. 21.5. Отображение позиции и хода анимации
Таблица 21.3. События Storyboard Имя
Описание
Completed
Анимация достигла конечной точки.
CurrentGlobalSpeedInvalidated
Изменилась скорость, либо анимация была временно приостановлена, возобновлена, остановлена или перемещена в новую позицию. Это событие также случается, когда таймер анимации запускается в обратном направлении (в конце реверсивной анимации), а также когда она ускоряется или замедляется.
CurrentStateInvalidated
Анимация стартовала или завершилась.
CurrentTimeInvalidated
Таймер анимации перемещен вперед на инкремент, изменив анимацию. Это событие также случается, когда анимация стартует, останавливается или завершается.
RemoveRequested
Анимация удалена. Анимируемое свойство будет впоследствии возвращено в исходное значение.
В этом случае необходимое вам событие — CurrentTimeInvalidated, которое инициируется при всяком перемещении таймера вперед. (Обычно это происходит 60 раз в секунду, но если ваш код требует больше времени на выполнение, некоторые “тики” таймера могут быть потеряны.) Когда возникает событие CurrentTimeInvalidated, то отправителем является объект Clock (из пространства имен System.Windows.Media.Animation). Объект Clock позволяет получить текущее время как TimeSpan и текущий показатель хода анимации как значение между 0 и 1. Ниже приведен код, обновляющий метку и панель прохождения. private void storyboard_CurrentTimeInvalidated(object sender, EventArgs e) { Clock storyboardClock = (Clock)sender;
Book_Pro_WPF-2.indb 716
19.05.2008 18:11:18
Анимация
717
if (storyboardClock.CurrentProgress == null) { lblTime.Text = "[[ stopped ]]"; progressBar.Value = 0; } else { lblTime.Text = storyboardClock.CurrentTime.ToString(); progressBar.Value = (double)storyboardClock.CurrentProgress; } }
Совет. Если вы используете свойство Clock.CurrentProgress, то не должны выполнять никаких вычислений для определения значений для вашей панели прохождения. Вместо этого просто сконфигурируйте ее с минимумом 0 и максимумом 1. Таким образом, вы можете просто применять Clock.CurrentProgress для установки ProgressBar.Value, как показано в этом примере.
Желательная частота кадров Как вы уже узнали из настоящей главы, WPF пытается выполнять анимацию с частотой 60 кадров в секунду. Это гарантирует гладкую, плавную анимацию от начала до конца. Конечно, WPF может и не справиться с такой задачей. Если у вас выполняется множество сложных анимаций одновременно, и процессор или видеокарта не справляется с нагрузкой, общая частота кадров может снизиться (в лучшем случае) либо начать отображаться прыжками (в худшем случае). Поскольку редко удается повысить частоту кадров, вы можете предпочесть ее снизить. Такое решение может быть продиктовано одной из двух причин:
• ваша анимация выглядит хорошо при низкой частоте кадров, так что ни к чему тратить дополнительные циклы процессора;
• ваше приложение выполняется на менее мощном процессоре или видеокарте, и вы знаете, что полноценная анимация с высокой частотой невозможна. На заметку! Иногда разработчики предполагают, что WPF включает код, масштабирующий частоту кадров, снижая ее для менее производительной видео-аппаратуры. На самом деле это вовсе не так. Вместо этого WPF всегда пытается вывести 60 кадров в секунду, если только вы не укажете иначе. Отрегулировать частоту кадров достаточно просто. Вы просто используете прикрепленное свойство Timeline.DesiredFrameRate раскадровки, содержащей вашу анимацию. Ниже приведен пример, сокращающий вдвое частоту кадров:
На рис. 21.6 показано простое тестовое приложение, анимирующее передвигающийся по Canvas кружок. Приложение начинается с помещения объекта Ellipse на Canvas. Свойство Canvas. ClipToBounds устанавливается в true, так что границы кружка не выходят за границу Canvas на остальное поле окна.
Book_Pro_WPF-2.indb 717
19.05.2008 18:11:18
718
Глава 21
Рис. 21.6. Тестирование частоты кадров с помощью простой анимации Чтобы перемещать круг по Canvas, запускаются две анимации одновременно: одна обновляет свойство Canvas.Left (перемещая его слева направо), а другая изменяет свойство Canvas.Top (вызывая перемещение по вертикали). Анимация Canvas.Top обратима — как только кружок достигает наивысшей точки, он начинает падать обратно вниз. Анимация Canvas.Left не обратима, однако она выполняется вдвое медленнее, так что обе анимации перемещают кружок одновременно. Финальный трюк заключается в использовании свойства DecelerationRatio анимации Canvas.Top. Таким образом, кружок поднимается все медленнее, пока не достигнет вершины, что создает более реалистичный эффект. Ниже приведен полный код разметки для этой анимации.
Обратите внимание на то, что свойства Canvas.Left и Canvas.Top заключены в скобки. Это говорит о том, что они не принадлежат целевому элементу (эллипсу), а являются прикрепленными свойствами. Также вы увидите, что анимация определена в коллекции Resources окна. Это позволяет запускать анимацию более чем одним способом. В нашем примере анимация запускается при щелчке на кнопке Repeat (Повтор) и первой загрузке окна с помощью примерно такого кода:
Book_Pro_WPF-2.indb 718
19.05.2008 18:11:18
Анимация
719
Действительное назначение этого примера — испытать различные частоты кадров. Чтобы увидеть эффект от определенной частоты кадров, вы просто должны ввести соответствующее число в текстовом поле и щелкнуть на кнопке Repeat. После этого анимация запускается с новой частотой кадров (которая указывается выражением привязки данных), и вы тут же можете наблюдать результат. На более низких частотах кадров эллипс не движется гладко, а вместо этого беспорядочно скачет по поверхности Canvas. Также вы можете в коде изменить свойство Timeline.DesiredFrame. Например, вы можете прочитать статическое свойство RenderCapability.Tier, чтобы определить уровень поддержки видеокарты. На заметку! Приложив совсем немного усилий, вы также можете создать вспомогательный класс, который позволит запустить некоторую логику в коде разметки XAML. Один из примеров этого можно найти по адресу http://blogs.msdn.com/henryh/archive/2006/08/23/719568.aspx, где демонстрируется декларативное снижение частоты кадров на основе уровня поддержки видеокарты.
Еще раз о типах анимаций Теперь вы знакомы с основами системы анимации свойств WPF — как определяются анимации, как они подключаются к элементам и как можно контролировать воспроизведение на раскадровке. Теперь самое время вернуться на шаг назад и еще раз присмотреться к классам анимации для разных типов данных, а также узнать, как можно достичь желаемого эффекта. Первая задача при создании любой анимации — выбор правильного свойства, подлежащего анимации. Выбор пути между результатом, которого вы хотите достичь (например, перемещение элемента в пределах окна), и свойством, которое необходимо использовать (в нашем случае — Canvas.Left и Canvas.Top), не всегда очевиден. Ниже приведено несколько рекомендаций.
• Если вы хотите применять анимацию для того, чтобы заставить элемент появляться и исчезать, не используйте свойство Visibility (которое позволяет переключаться только между полной видимостью и полной невидимостью). Вместо этого обратитесь к свойству Opacity, чтобы можно было управлять видимостью плавно.
• Если вы хотите анимировать позицию элемента, рассмотрите использование Canvas . Этот объект предоставляет наиболее прямолинейные свойства (Canvas.Left и Canvas.Top) и требует минимума накладных расходов. В качестве альтернативы вы можете получить аналогичный эффект в других контейнерах компоновки, анимируя такие свойства, как Margin и Padding, с применением класса ThicknessAnimation. Вы также можете анимировать MinWidth или MinHeight либо же колонку или строку в Grid.
Book_Pro_WPF-2.indb 719
19.05.2008 18:11:18
720
Глава 21
Совет. Многие эффекты анимации разработаны для постепенного показа элемента. Часто используется плавное проявление элемента до полной видимости, или же распахивание из маленькой точки. Однако есть и множество альтернатив. Например, вы можете сделать элемент расплывчатым, применив BlurBitmapEffect, описанный в главе 13, а также анимировать свойство Radius для уменьшения расплывчатости, таким образом, плавно “наводя фокус” на элемент.
• Наиболее часто применяемый вид анимации — это трансформации. Вы можете использовать их для перемещения или переворота элемента (TranslateTransform), вращения (RotateTransform), изменения размера или сжатия (ScaleTransform) и т.п. При аккуратном применении они позволят обойтись без жесткого кодирования размеров и позиций в вашей анимации.
• Хороший способ изменения поверхности элемента посредством анимации заключается в модификации свойств кисти. Вы можете использовать ColorAnimation для изменения цвета или другого объекта анимации, чтобы трансформировать свойство более сложной кисти, например, смещение в градиенте. Следующие примеры демонстрируют, как анимировать трансформации и кисти, а также как использовать еще несколько типов анимации. Вы также узнаете, как создаются многомерные анимации с ключевыми кадрами, анимации на основе пути, а также анимации на основе кадра.
Анимация трансформаций Трансформации представляют собой один из наиболее мощных способов изменения элемента. Когда вы используете трансформации, вы не просто изменяете границы элемента. При этом все визуальное представление элемента движется, опрокидывается, перекашивается, растягивается, увеличивается, сжимается или вращается. Например, если вы анимируете размер кнопки с помощью ScaleTransform, то вся кнопка изменяется в размерах, включая ее рамку и внутреннее содержимое. Эффект получается более впечатляющий, чем когда вы анимируете только Width и Height или свойство FontSize, затрагивающее ее текст. Как вы знаете из главы 13, каждый элемент способен применять трансформацию двумя разными способами: через свойства RenderTransform и LayoutTransform. RenderTransform более эффективно, поскольку применяется после того, как выполняется компоновка и используется для трансформации финального отображаемого вывода. LayoutTransform применяется перед проходом компоновки, и в результате другие элементы управления оказываются переупорядоченными с целью заполнения контейнера. Изменение свойства LayoutTransform инициирует новую операцию компоновки (если только вы не используете ваш элемент в составе Canvas — в этом случае RenderTransform и LayoutTransform эквивалентны). Чтобы использовать в анимации трансформацию, первым делом нужно определить трансформацию (анимация может изменить существующую трансформацию, но не создать новую). Например, предположим, что вы хотите позволить кнопке вращаться. Для этого потребуется трансформация RotateTransform: A Button
Book_Pro_WPF-2.indb 720
19.05.2008 18:11:18
Анимация
721
Кроме того, имеется триггер, который заставит кнопку вращаться, когда курсор мыши проходит над ней. Он использует целевое свойство RenderTransform.Angle — другими словами, читает свойство кнопки RenderTransform и модифицирует свойство Angle объекта RenderTransform , определенного там. Тот факт, что свойство RenderTransform может содержать широкое разнообразие объектов трансформации, каждый со своим набором свойств, не вызывает проблемы. До тех пор, пока вы используете трансформацию, которая имеет свойство Angle, этот триггер будет работать.
Кнопка делает один оборот каждые 0,8 секунды и продолжает свое вращение бесконечно. Пока кнопка вращается, она остается полностью функциональной, например, вы можете щелкнуть на ней и обработать событие Click. Чтобы обеспечить вращение кнопки вокруг ее центральной точки (а не верхнего левого угла), вы должны установить свойство RenderTransformOrigin, как показано ниже:
Напомним, что свойство RenderTransformOrigin использует относительные единицы — от 0 до 1, так что 0.5 представляет среднюю точку. Чтобы остановить вращение, вы можете использовать второй триггер, реагирующий на событие MouseLeave. В этот момент вы можете удалить раскадровку, выполняющую вращение, однако это заставит кнопку “прыгнуть” в свою исходную ориентацию за один шаг. Более удачный подход заключается в запуске второй анимации, которая заменит первую. В этой анимации свойства To и From опущены, вследствие чего кнопка плавно развернется в свое исходное положение примерно на 0,2 секунды:
Чтобы создать собственную вращающуюся кнопку, вам нужно добавить оба эти триггера в коллекцию Button.Triggers. Или же вы можете поместить их (вместе с трансформацией) в стиль и применить этот стиль к стольким кнопкам, к скольким захотите. Например, ниже представлен код разметки для окна, полного “вращающихся” кнопок, показанного на рис. 21.7.
Book_Pro_WPF-2.indb 721
19.05.2008 18:11:18
722
Глава 21
... ... One Two Three Four
При щелчке на любой кнопке в элементе TextBox отображается сообщение. Этот пример также дает вам отличный шанс понять разницу между RenderTransform и LayoutTransform. Если вы модифицируете код для использования LayoutTransform, то увидите, что другие кнопки отодвигаются, освобождая место активной кнопке для поворота (рис. 21.8). Например, если верхняя кнопка поворачивается, то все, находящиеся ниже, отодвинутся, чтобы не мешать ей. Конечно, чтобы получить представление о том, как “чувствуют” себя кнопки в этом случае, стоит обратиться к загружаемому коду примеров.
Рис. 21.7. Использования трансформации отображения
Book_Pro_WPF-2.indb 722
Рис. 21.8. Использование трансформации компоновки
19.05.2008 18:11:19
Анимация
723
Анимация множества трансформаций Вы можете легко использовать разные трансформации в комбинации друг с другом. Фактически это несложно — вы просто должны применить TransformGroup для установки свойства LayoutTransform или RenderTransform. В TransformGroup можно вкладывать столько трансформаций, сколько нужно. На рис. 21.9 показан интересный эффект, полученный в результате использования двух трансформаций. Окно документа начинается с маленькой пиктограммы в левом верхнем углу окна. Когда окно появляется, это содержимое вращается, расширяется и быстро становится видимым. Это концептуально подобно эффекту, используемому Windows при максимизации окна. В WPF вы можете использовать этот трюк в любых элементах, применяющих трансформацию.
Рис. 21.9. Содержимое, которое “запрыгивает” в представление Чтобы создать такой эффект, в TransformGroup определяются две трансформации, которые затем используются для установки свойства RenderTransform объекта Border, включающего все содержимое.
Book_Pro_WPF-2.indb 723
19.05.2008 18:11:19
724
Глава 21
Ваша анимация может взаимодействовать с обоими этими объектами трансформации посредством спецификации числового смещения (0 — для ScaleTransform, появляющейся первой, и 1 — для последующей RotateTransform). Например, вот так выглядит анимация, увеличивающая содержимое:
А вот — анимация в той же раскадровке, которая вращает его:
На самом деле эта анимация несколько более сложная, нежели показано здесь. Например, здесь также присутствует анимация, увеличивающая в то же время значение свойства Opacity, и когда Border достигает полного размера, то кратко “отскакивает” назад, создавая более естественное впечатление. Создание временной шкалы для такой анимации и управление разными свойствами объектов анимации требует времени. В идеале подобные вещи лучше делать с применением инструментов дизайна, таких как Expression Blend, чем кодировать их вручную. Но еще лучше поручить эту работу сторонним разработчикам, которые специализируются на таких вещах, чтобы они собрали всю логику в единую анимацию, которую вы смогли бы впоследствии использовать повторно и применять к своим объектам по мере необходимости. (На данный момент вы можете использовать повторно эту анимацию, сохранив Storyboard как ресурс уровня приложения.) Такой эффект оказывается неожиданно полезным. Например, вы можете применять его, чтобы привлечь внимание к новому содержимому, вроде только что открытого пользователем файла. Возможным вариациям нет конца. Например, компания, занимающаяся розничной торговлей, может создать каталог продуктов, в котором сдвигаются панели с подробностями о продукте или в окне разворачивается “свиток” с изображением продукта, когда вы проводите курсором над именем соответствующего продукта.
Анимированные кисти Анимированные кисти — еще одна распространенная техника в анимации WPF, и реализовать их также легко, как анимированные трансформации. Опять же эта техника заключается в проникновении в определенное подсвойство, которое нужно изменить, используя для этого анимацию соответствующего типа. На рис. 21.10 показан пример, в котором изменяется RadialGradientBrush. При запуске анимации центральная часть радиального градиента движется по эллиптической траектории, создавая некий трехмерный эффект. В то же время внешний цвет градиента изменяется от синего к черному. Чтобы выполнить эту анимацию, вам понадобятся анимации двух типов, которые мы пока еще не рассматривали. ColorAnimation обеспечивает плавный переход между двумя цветами, создавая тонкий эффект сдвига цвета. PointAnimation позволяет перемещать точку из одного места в другое (по сути, это то же самое, что и одновременная модификация обеих координат X и Y с использованием отдельной анимации
Book_Pro_WPF-2.indb 724
19.05.2008 18:11:19
Анимация
725
DoubleAnimation с линейной интерполяцией). Вы можете применять PointAnimation для деформации фигуры, состоящей из точек, или же изменять местоположение центра радиального градиента, как в данном примере.
Рис. 21.10. Изменение радиального градиента Так выглядит код разметки, определяющий эллипс и его кисть:
А вот две анимации, которые перемещают центральную точку и изменяют второй цвет:
Вы можете создать огромное разнообразие завораживающих эффектов, варьируя цвета и смещения в LinearGradientBrush и RadialGradientBrush. Мало того, градиентные кисти также имеют свое собственное свойство RelativeTransform, которое можно использовать для вращения, масштабирования, растяжения и наклона. У команды разработчиков WPF есть забавный инструмент, называемый Gradient Obsession, предназначенный для построения анимаций на основе градиентов. Вы можете найти его (вместе с исходным кодом) по адресу http://wpf.netfx3.com/files/folders/designer/entry7718.aspx. Некоторые дополнительные идеи можно почерпнуть в примерах от Чарльза Петцольда (Charles Petzold) по адресу http://www.charlespetzold.com/blog/2006/07/230620.html, где изменяется геометрия разнообразных объектов DrawingBrush, создаются мозаичные шаблоны, которые преобразуются в различные фигуры.
Book_Pro_WPF-2.indb 725
19.05.2008 18:11:19
726
Глава 21
VisualBrush Как вы знаете из главы 13, VisualBrush позволяет “перехватить” внешний вид любого элемента и использовать его для заполнения другой поверхности. Эта другая поверхность может быть чем угодно — от обычного прямоугольника до букв текста. На рис. 21.11 показан базовый пример. Сверху находится реальная живая кнопка. Ниже используется VisualBrush для заполнения прямоугольника картинкой этой кнопки, которая растягивается и вращается, создавая эффект разнообразных трансформаций. VisualBrush также открывает некоторые интересные возможности для анимации. Например, Рис. 21.11. Анимация элемента, вместо анимации живого реального элемента вы закрашенного VisualBrush можете анимировать простой прямоугольник, имеющий то же заполнение. Чтобы понять, как работает этот механизм, рассмотрим пример, приведенный выше на рис. 21.9, который “вталкивает” элемент в визуальное представление. Пока выполняется эта анимация, анимируемый элемент трактуется как любой другой элемент WPF, а это означает, что можно выполнить щелчок на кнопке внутри или прокрутить его содержимое с помощью клавиатуры (если успеете). В некоторых ситуациях это может привести к путанице. В других ситуациях — ухудшить производительность из-за дополнительных накладных расходов, необходимых для выполнения трансформации ввода (например, щелчков мыши) и передачи его исходному элементу. Заменить этот эффект с помощью VisualBrush очень легко. Для начала потребуется создать другой элемент, который заполняет себя с использованием VisualBrush. Этот VisualBrush должен нарисовать себя на основе внешнего вида элемента, который вы собираетесь анимировать (в данном примере элементом является именованная рамка).
Чтобы поместить прямоугольник в позицию исходного элемента, вы можете вставить их обоих в одну и ту же ячейку Grid. Размер ячеек устанавливается равным размеру исходного элемента (рамки), а прямоугольник растягивается до его размеров. Другой выбор — перекрывающий Canvas поверх вашего реального контейнера компоновки. (Вы можете затем привязать свойства анимации к свойствам ActualWidth и ActualHeight реального элемента, лежащего в основе, чтобы гарантировать их совпадение.) Добавив прямоугольник, вам просто нужно выровнять ваши анимации, чтобы анимировать его трансформации. Финальный шаг — скрыть прямоугольник по окончании анимации:
Book_Pro_WPF-2.indb 726
19.05.2008 18:11:19
Анимация
727
private void storyboardCompleted(object sender, EventArgs e) { rectangle.Visibility = Visibility.Collapsed; }
Анимация ключевого кадра Все виды анимации, которые вы видели до сих пор, используют линейную интерполяцию для перемещения из начальной точки в конечную. Но что, если вам понадобится создать анимацию, имеющую множество сегментов и движущуюся менее равномерно? Например, может понадобиться создать анимацию, сначала быстро задвигающую элементы в представление, а затем медленно подвигающую их на окончательное место. Такого эффекта можно достичь, создав последовательность двух анимаций и используя свойство BeginTime для запуска второй анимации после окончания первой. Однако существует более простой подход — вы можете использовать анимацию ключевого кадра. Анимация ключевого кадра (key frame animation) — это анимация, состоящая из множества коротких сегментов. Каждый сегмент в анимации состоит из начального, конечного и промежуточного значений. Когда вы запускаете анимацию, она плавно переходит от одного значения к другому. Например, рассмотрим анимацию Point, позволяющую перемещать центральную точку RadialGradientBrush из одного места в другое:
Вы можете заменить этот объект PointAnimation эквивалентным PointAnimation UsingKeyFrames, как показано ниже:
Эта анимация содержит два ключевых кадра. Первый устанавливает значение
Point при первом запуске анимации (если вы хотите использовать текущее значение, установленное в RadialGradientBrush, то этот ключевой кадр можно опустить). Второй ключевой кадр определяет конечное значение, которое достигается через десять секунд. Объект PointAnimationUsingKeyFrames выполняет линейную интерполяцию для плавного перемещения от первого кадра ко второму — так же, как это делает PointAnimation с From и To. На заметку! Каждый ключевой кадр использует свой собственный объект анимации (вроде LinearPointKeyFrame). По большей части эти классы одинаковы — они включают свойство Value, хранящее целевое значение, и свойство KeyTime, указывающее момент, когда кадр достигнет целевого значения. Единственное отличие — тип данных свойства Value. В LinearPointKeyFrame это Point, в DoubleKeyFrame — double и т.д. Вы можете создать и более интересный пример, используя последовательности ключевых кадров. Следующая анимация проводит центральную точку через серию позиций, достигаемых в разные моменты времени. Скорость перемещения центральной точ-
Book_Pro_WPF-2.indb 727
19.05.2008 18:11:19
728
Глава 21
ки меняется в зависимости от того, насколько длительной является задержка между кадрами, и какую дистанцию ей нужно пройти.
Эта анимация не является обратимой, но она повторяется. Чтобы исключить прыжки между финальным значением оной итерации и стартовым значением следующей, анимация завершается в той же точке, что и начинается. В главе 23 приведен другой пример анимации ключевого кадра. Он использует анимацию Point3DanimationUsingKeyFrames для перемещения камеры по трехмерной сцене и Vector3DanimationUsingKeyFrames для одновременного вращения камеры. На заметку! Использование анимации ключевого кадра не так мощно, как применение последовательности множества анимаций. Наиболее существенное отличие в том, что вы не можете применять разные значение AccelerationRatio и DecelerationRatio к каждому ключевому кадру. Вместо этого можно применять только одно значение ко всей анимации в целом.
Дискретные анимации ключевого кадра Анимация ключевого кадра, которую вы видели в предыдущем примере, использует линейные ключевые кадры. В результате происходит гладкий переход между значениями ключевых кадров. В данном случае не выполняется никакой интерполяции. Когда достигается время очередного ключа, свойство мгновенно принимает новое значение. Другое дело — дискретные ключевые кадры. В этом случае никакой интерполяции не выполняется. Когда достигается ключевой момент, значение свойства меняется скачкообразно. Имена классов линейных ключевых кадров образуются в форме LinearТипДанныхKeyFrame. Имена классов дискретных ключевых кадров строятся в соответствии со схемой Discrete ТипДанныхKeyFrame. Рассмотрим измененную версию примера RadialGradientBrush, использующего дискретные ключевые кадры:
Book_Pro_WPF-2.indb 728
19.05.2008 18:11:20
Анимация
729
При запуске этой анимации центральная точка будет перепрыгивать из одной позиции в другую в соответствующее время. Все классы анимации ключевого кадра поддерживают дискретные ключевые кадры, но только некоторые из них поддерживают линейные ключевые кадры. Все зависит от типа данных. Типы данных, поддерживающие линейные ключевые кадры — те же самые, что поддерживают и линейную интерполяцию, и они представляют класс ТипДанныхAnimation. Примеры включают Point, Color и double. Типы данных, не поддерживающие линейную интерполяцию, включают строки и объекты. В главе 22 вы увидите пример, использующий класс StringAnimationUsingKeyFrames для отображения разных частей текста в процессе анимации. Совет. Вы можете комбинировать оба типа ключевых кадров — линейные и дискретные — в одной анимации ключевого кадра.
Сплайновые анимации ключевого кадра Существует еще один тип ключевых кадров: сплайновый ключевой кадр. Каждый класс, поддерживающий линейные ключевые кадры, поддерживает также и сплайновые ключевые кадры, и их имена формируются по шаблону SpliteТипДанныхKeyFrame. Подобно линейным ключевым кадрам, сплайновые ключевые кадры применяют интерполяцию для гладкого перемещения от одного ключевого значения к другому. Отличие состоит в том, что каждый сплайновый ключевой кадр оснащен свойством KeySpline. Используя это свойство, вы определяете кубическую кривую Безье, которая формирует путь интерполяции. Хотя здесь довольно трудно получить нужный эффект (по крайней мере, без инструментов дизайна, которые могут в этом помочь), все же эта техника предоставляет возможность создавать более плавное ускорение и замедление, а также другие естественные движения. Как вы помните из главы 14, кривая Безье определяется начальной точкой, конечной точкой и двумя контрольными точками. В случае ключевого сплайна стартовая точка всегда находится в начале координат (0,0), а конечная точка — в (1,1). Вы просто задаете две контрольных точки. Создаваемая вами кривая описывает отношение между временем (осью X) и анимируемым значением (ось Y). Рассмотрим пример, демонстрирующий анимацию ключевого сплайна, сравнив движение двух эллипсов по поверхности Canvas . Первый эллипс использует DoubleAnimation для медленного равномерного перемещения по окну. Второй эллипс применяет DoubleAnimationUsingKeyFrames с двумя объектами SplineDoubleKeyFrame. Оба они достигают конечной точки одновременно (через 10 секунд), но второй ускоряется и замедляется на протяжении своего пути, обгоняя и отставая от первого эллипса.
Book_Pro_WPF-2.indb 729
19.05.2008 18:11:20
730
Глава 21
Наибольшее ускорение достигается вскоре после пятисекундной отметки, когда вступает в действие второй SplineDoubleKeyFrame. Его первая контрольная точка соответствует относительно большому значению по оси Y, представляющему ход анимации (0.8) с относительно малым значением по оси X, представляющей время. В результате эллипс ускоряется на протяжении малого расстояния, прежде чем снова замедлиться. На рис. 21.12 показана графическая траектория двух кривых, управляющих движением эллипса. Чтобы интерпретировать эти кривые, вспомните, что они отображают ход анимации сверху вниз. Посмотрев на первую кривую, вы увидите, что она описывает относительно равномерное движение вниз, с краткой паузой в начале и плавным выравниванием в конце. Однако вторая кривая ниспадает вниз относительно быстрее, достигая максимальной скорости, а затем в оставшейся части анимации выравнивается.
Рис. 21.12. График выполнения анимации ключевого сплайна
Анимация на основе пути Анимация на основе пути использует объект PathGeometry для установки значения свойства. Хотя такая анимация может в принципе применяться для модификации любого свойства с правильным типом данных, но наиболее удобна для анимации свойств, описывающих позицию. Фактически классы анимации на основе пути, прежде всего, предназначены для того, чтобы помочь перемещать визуальные эффекты по некоторой траектории. Как вы знаете из главы 14, объект PathGeometry описывает фигуру, которая может включать прямые линии, дуги и кривые. На рис. 21.13 демонстрируется пример с объектом PathGeometry, состоящий из двух дуг и сегмента прямой линии, которая соединяет последнюю определенную точку с начальной. Это формирует замкнутую траекторию, по которой равномерно движется маленькое векторное изображение. Создать такой пример довольно легко. Первый шаг — задать путь, который вы хотите использовать. В данном примере он описан как ресурс:
Book_Pro_WPF-2.indb 730
19.05.2008 18:11:20
Анимация
731
Рис. 21.13. Перемещение изображения по траектории Хотя это и не обязательно, в данном примере путь отображается. Таким образом, вы можете убедиться, что картинка следует по определенному вами маршруту. Чтобы показать маршрут, просто достаточно добавить элемент Path, использующий заданную вами геометрию:
Элемент Path помещается в Canvas наряду с элементом Image, который вы хотите перемещать по этому пути:
Заключительный шаг — создание анимации, перемещающей картинку. Чтобы перемещать изображение, вам нужно изменять свойства Canvas.Left и Canvas.Top. Этот трюк выполняет анимация DoubleAnimationUsingPath, но вам понадобится их две — одна работает со свойством Canvas.Left, а другая — с Canvas.Top. Ниже показано полное описание раскадровки.
Book_Pro_WPF-2.indb 731
19.05.2008 18:11:20
732
Глава 21
Как видите, при создании анимации на основе пути начальные и конечные значения не указываются. Вместо этого задается PathGeometry, которую хотите использовать в свойстве PathGeometry. Некоторые классы анимации на основе пути, такие как PointAnimationUsingPath, применяют к целевому свойству оба компонента — X и Y. Класс DoubleAnimationUsingPath лишен такой возможности, поскольку устанавливает единственное значение типа double. Вследствие этого вам также понадобится установить свойство Source в X и Y, чтобы указывать, когда вы используете координату пути X, а когда — Y. Хотя анимация на основе пути может использовать траекторию, заданную кривой Безье, это несколько отличается от применения ее в анимации ключевого сплайна, о котором шла речь в предыдущем разделе. В анимации ключевого сплайна кривая Безье описывала отношение между ходом анимации и временем, позволяя создать анимацию с переменной скоростью. Но в анимации на основе пути коллекция отрезков и кривых, образующих путь, определяет значения, которые будут использованы для анимируемого свойства. На заметку! Анимации на основе пути всегда выполняются на постоянной скорости. WPF берет общую длину маршрута и длительность, указанную вами, чтобы вычислить скорость.
Анимация на основе фрейма Наряду с системой анимации, основанной на изменении свойств, WPF предоставляет возможность создания анимаций на основе фрейма, не используя ничего помимо кода. Все, что вам понадобится — реагировать на событие CompositionTarget.Rendering, которое возбуждается для получения содержимого каждого фрейма. Это — довольно низкоуровневый подход, который стоит применять только в том случае, когда вы уверены, что стандартная модель анимации на основе изменения свойств не подходит для реализации вашего сценария (например, если вы строите простую игру с прокруткой экрана, создавая анимации на основе физических законов, либо моделируете такие эффекты, как огонь, снег или пузыри). Основная техника для построения анимации на базе фрейма проста. Вам просто нужно присоединить обработчик событий к статическому событию CompositionTarget. Rendering. После этого WPF начнет непрерывно вызывать этот обработчик. (До тех пор, пока ваш код отображения выполняется достаточно быстро, WPF будет вызывать его 60 раз в секунду.) В обработчике события визуализации создание и управление элементами в окне полностью ложится на вас. Другими словами, вам нужно управлять всей работой самостоятельно. Когда анимация завершится, отключите обработчик события. На рис. 21.14 показан достаточно простой пример. Здесь случайное количество кругов падают от верхней границы Canvas к нижней. Они падают с разной (случайно выбранной) скоростью, но по мере движения скорость каждого возрастает в одинаковой степени. Анимация завершается, когда все круги упадут вниз.
Book_Pro_WPF-2.indb 732
19.05.2008 18:11:20
Анимация
733
Рис. 21.14. Анимация падающих кружков на основе фрейма В данном примере каждый падающий кружок представлен элементом Ellipse. Специальный класс по имени EllipseInfo сохраняет ссылку на эллипс и отслеживает детали, существенные для его физической модели. В данном случае здесь присутствует только одна единица информации — смещение эллипса по оси X. (Вы можете легко расширить этот класс, добавив скорость по оси Y, дополнительную информацию относительно ускорения и т.п.) public class EllipseInfo { private Ellipse ellipse; public Ellipse Ellipse { get { return ellipse; } set { ellipse = value; } } private double velocityY; public double VelocityY { get { return velocityY; } set { velocityY = value; } } public EllipseInfo(Ellipse ellipse, double velocityY) { VelocityY = velocityY; Ellipse = ellipse; } }
Приложение отслеживает объект EllipseInfo для каждого эллипса, используя для этого коллекцию. Есть еще несколько полей уровня окна, которые хранят различные подробности, используемые при вычислении падения эллипса. Вы можете легко сделать их настраиваемыми.
Book_Pro_WPF-2.indb 733
19.05.2008 18:11:21
734
Глава 21
private private private private private private private private
List ellipses = new List(); double accelerationY = 0.1; int minStartingSpeed = 1; int maxStartingSpeed = 50; double speedRatio = 0.1; int minEllipses = 20; int maxEllipses = 100; int ellipseRadius = 10;
По щелчку на кнопке коллекция очищается, и обработчик события присоединяется к событию CompositionTarget.Rendering: private bool rendering = false; private void cmdStart_Clicked(object sender, RoutedEventArgs e) { if (!rendering) { ellipses.Clear(); canvas.Children.Clear(); CompositionTarget.Rendering += RenderFrame; rendering = true; } }
Если эллипс не существует, код визуализации создаст его автоматически. Он создает случайное количество эллипсов (в данном случае — от 20 до 100) и установит для каждого из них одинаковый размер и цвет. Эллипсы помещаются в верхнюю часть Canvas, но их смещение по оси X задается случайным образом. private void RenderFrame(object sender, EventArgs e) { if (ellipses.Count == 0) { // Анимация запущена. Создать эллипсы. int halfCanvasWidth = (int)canvas.ActualWidth / 2; Random rand = new Random(); int ellipseCount = rand.Next(minEllipses, maxEllipses+1); for (int i = 0; i < ellipseCount; i++) { // Создание эллипса. Ellipse ellipse = new Ellipse(); ellipse.Fill = Brushes.LimeGreen; ellipse.Width = ellipseRadius; ellipse.Height = ellipseRadius; // Размещение эллипса. Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth)); Canvas.SetTop(ellipse, 0); canvas.Children.Add(ellipse); // Отслеживание эллипса. EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed)); ellipses.Add(info); } } ...
Если данный эллипс уже существует, код выполняет наиболее интересную работу по его анимации. Каждый эллипс слегка смещается с использованием метода Canvas.SetTop(). Размер смещения определяется назначенной ему скоростью.
Book_Pro_WPF-2.indb 734
19.05.2008 18:11:21
Анимация
735
... else { for (int i = ellipses.Count-1; i >= 0; i--) { EllipseInfo info = ellipses[i]; double top = Canvas.GetTop(info.Ellipse); Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY); ...
Для повышения производительности эллипсы удаляются из коллекции, как только достигают нижней части Canvas. Таким образом, после этого вам уже не нужно их обрабатывать. Чтобы позволить выполнять эту работу, не теряя текущего места при прохождении коллекции, нужно выполнить шаг назад с конца коллекции к ее началу. Если эллипс еще не достиг нижней границы Canvas, код увеличивает его скорость. (Альтернативно вы можете установить скорость на основе близости к нижней границе Canvas, тем самым создавая эффект “магнита”.) ... if (top >= (canvas.ActualHeight - ellipseRadius*2)) { // Если эллипс достиг дна Canvas. // прекратить его анимацию. ellipses.Remove(info); } else { // Увеличить скорость. info.VelocityY += accelerationY; } ...
И, наконец, если все эллипсы удалены из коллекции, обработчик событий удаляется, завершая анимацию: ... if (ellipses.Count == 0) { // Завершить анимацию. // Нет смысла продолжать вызывать этот метод, // когда ему нечего делать. CompositionTarget.Rendering -= RenderFrame; rendering = false; } } } }
Очевидно, что вы можете расширить эту анимацию, заставив кружки подпрыгивать, разлетаться и т.п. Техника одна и та же — вам просто нужно реализовать более сложные формулы для вычисления скорости. Необходимо упомянуть об одном обстоятельстве, которое следует иметь в виду при построении анимации на основе фрейма: они не зависят от времени. Другими словами, ваша анимация может работать быстрее на быстрых компьютерах, поскольку частота кадров будет расти и ваше событие CompositionTarget.Rendering станет инициироваться чаще. Чтобы компенсировать этот эффект, потребуется написать код, принимающий во внимание текущее время. Наилучший способ начать работать с анимацией на базе фрейма — исследовать неожиданно подробный пример анимации, входящий в состав WFP SDK (и также вклю-
Book_Pro_WPF-2.indb 735
19.05.2008 18:11:21
736
Глава 21
ченный в загружаемый код для этой главы). Он демонстрирует несколько практических эффектов и использует класс TimeTracker для реализации зависящей от времени анимации фрейма.
Резюме В настоящей главе мы детально рассмотрели поддержку анимации WPF. Теперь, когда вы овладели основами, вы можете потратить больше времени для совершенствования в искусстве анимации — выборе свойств для анимации и модификации их для достижения требуемого эффекта. Практически бесчисленные примеры анимации можно найти в Internet, включая и те, что упомянуты в этой главе. (Если вам лень набирать длинные URL, обратитесь по адресу www.prosetech.com за списком полезных ссылок.)
Будущее анимации WPF Модель анимации WPF весьма богата. Однако получить нужный эффект не всегда легко. Если вы хотите анимировать отдельные части вашего интерфейса как часть одной анимированной “сцены”, вам часто придется писать довольно объемный код разметки с множеством взаимозависимых деталей, периодически возвращаясь к программному коду для выполнения вычислений конечного значения анимации. А если вам нужен тонкий контроль анимации, например, при моделировании части физической системы, вам придется контролировать каждый шаг, используя анимацию на основе фрейма. Будущее анимации WPF обещает появление высокоуровневых классов, построенных на базе тех, что вы изучили в настоящей главе. В идеале вы сможете встраивать анимации в свое приложение, просто используя заранее разработанные классы, упаковывая ваши элементы в специализированные контейнеры и устанавливая несколько прикрепленных свойств. Действительная реализация, генерирующая нужный эффект — будь то плавное превращение одного изображения в другое или серия анимированных летающих объектов, которые строят окно — все это будет вам предоставлено. Чтобы увидеть примеры направления будущего развития, обратите внимание на проект с открытым кодом под названием Animation Behaviors по адресу http://www.codeplex.com/ AnimationBehaviors, который предлагает удобный способ присоединения небольшого набора заранее разработанных анимаций к вашим интерфейсным элементам. Хотя это только один из ранних примеров (которые могут принести плоды, а могут и не принести), команда WPF указывает, что предварительно встроенные анимации — очень нужное средство, одно из тех, которые они желают поддержать в будущем.
Book_Pro_WPF-2.indb 736
19.05.2008 18:11:21
ГЛАВА
22
Звук и видео В
этой главе мы затронем еще две области функциональности WPF — аудио и видео. Именно поддержка аудио в WPF стала заметным шагом вперед по сравнению с предыдущими версиями .NET, хотя она еще и далека от совершенства. WPF предоставляет возможность воспроизводить широкое разнообразие аудио-форматов, включая файлы MP3 и все остальное, что поддерживает проигрыватель Windows Media. Однако звуковые возможности WPF пока еще много беднее DirectSound (расширенного звукового API-интерфейса DirectX), который позволяет применять динамические эффекты и помещать звуки в эмулируемое трехмерное пространство. WPF также недостает возможности получения спектральных данных, которые сообщат максимальный и минимальный уровни звука, что полезно при создании некоторых типов синхронизирующих эффектов и управляемой звуком анимации. Поддержка видео в WPF более впечатляющая. Хотя возможность воспроизведения видео (такого как файлы MPEG и WMV) не особо потрясающа, способ ее интеграции с остальной частью модели WPF весьма неплох. Например, вы можете применять видео для заполнения тысяч элементов за раз и комбинировать его с эффектами, анимацией, прозрачностью и даже трехмерными объектами. В этой главе вы увидите, как интегрировать видео- и аудио-содержимое в приложения. Мы даже представим краткий обзор поддержки синтеза и распознавания речи в WPF. Но, прежде чем обратиться к более экзотичным примерам, мы начнем с рассмотрения базового кода, необходимого для воспроизведения скромного аудио в формате WAV.
Воспроизведение WAV-аудио Каркас .NET Framework имеет небогатую историю поддержки звука. Версии 1.0 и 1.1 не предлагали никакого управляемого способа воспроизведения аудио, а когда долгожданная поддержка, наконец, появилась в .NET 2.0, она была представлена в форме не приводящего в восторг класса SoundPlayer (который вы можете найти в “малонаселенном” пространстве имен System.Media). Класс SoundPlayer довольно ограничен: он может воспроизводить только файлы в формате WAV, не поддерживает воспроизведения одновременно более одного звука и совсем не предоставляет возможностей управления никакими аспектами воспроизведения аудио (например, таких вещей, как громкость и баланс). Чтобы получить эти возможности, разработчики, использующие Windows Forms, вынуждены были работать с библиотекой неуправляемого кода quartz.dll. На заметку! Библиотека quartz.dll — ключевая часть DirectX, и она присутствует в проигрывателе Windows Media и операционной системе Windows. (Тот же компонент известен под более маркетингово-дружественным названием DirectShow, а предыдущие версии назывались ActiveMovie.) Подробности использования quartz.dll в Windows Forms изложены в книге Pro .NET 2.0 Windows Forms and Custom Controls in C# (Apress, 2005 г.).
Book_Pro_WPF-2.indb 737
19.05.2008 18:11:21
738
Глава 22
Класс SoundPlayer поддерживается в приложениях WPF. Если вы можете смириться с его существенными ограничениями, то можно сказать, что он предлагает наиболее простой и легкий способ добавления работы с аудио в ваши приложения. Класс SoundPlayer также упаковывается в класс SoundPlayerAction, который позволяет воспроизводить звук через декларативный триггер (вместо написания нескольких строк кода C# в обработчике событий). В следующих разделах мы представим краткий обзор обоих классов, прежде чем перейдем к описанию более мощных WPF-классов MediaPlayer и MediaElement.
SoundPlayer Чтобы воспроизвести звук с помощью класса SoundPlayer, нужно выполнить перечисленные ниже шаги. 1. Создать экземпляр SoundPlayer. 2. Специфицировать звуковое содержимое, установив либо свойство Stream, либо SoundLocation. Если у вас есть объект Stream, содержащий звук в формате WAV, используйте свойство Stream. Если же есть путь к файлу или URL, указывающий на файл WAV, применяйте свойство SoundLocation. 3. Установив свойство Stream или SoundLocation , вы можете заставить SoundPlayer в действительности загрузить аудиоданные, вызвав метод Load() или LoadAsync(). Метод Load() наиболее прост — он останавливает выполнение вашего кода до тех пор, пока весь звуковой фрагмент не будет загружен в память. LoadAsync() выполняет свою работу в другом потоке и по завершении инициирует событие LoadCompleted. На заметку! Технически вы не обязаны использовать Load() или LoadAsync(). Экземпляр SoundPlayer загружает аудиоданные при необходимости, когда вызывается Play() или PlaySync(). Однако явно загрузить аудио-фрагмент — хорошая идея; это не только позволит сэкономить накладные расходы при многократном воспроизведении, но также упростит обработку исключений, связанных с файловыми проблемами, отдельно от исключений, вызванных причинами, относящимися к процессу воспроизведения. 4. После этого вы можете вызвать PlaySync(), который приостановит ваш код на время воспроизведения аудио-фрагмента, или же применить Play() для воспроизведения в другом потоке, обеспечивая способность интерфейса вашего приложения реагировать на действия пользователя. Единственный другой вариант, имеющийся в вашем распоряжении — это PlayLooping(), воспроизводящий аудио-фрагмент асинхронно в бесконечном цикле (идеально для всех этих раздражающих саундтреков). Чтобы остановить текущее воспроизведение в любой момент, просто вызовите Stop(). Совет. Если вы ищете WAV, чтобы протестировать SoundPlayer, обратитесь в каталог Media, находящийся внутри каталога Windows — в нем содержатся WAV-файлы для всех системных звуков Windows. Следующий фрагмент кода показывает простейший подход к загрузке и асинхронному воспроизведению аудиофайла: SoundPlayer player = new SoundPlayer(); player.SoundLocation = "test.wav"; try {
Book_Pro_WPF-2.indb 738
19.05.2008 18:11:21
Звук и видео
739
player.Load(); player.Play(); } catch (System.IO.FileNotFoundException err) { // Если файл не будет найден, здесь возникнет ошибка. } catch (FormatException err) { // Здесь сгенерируется исключение FormatException, // если файл не будет иметь правильный аудиоформат WAV. }
До сих пор код предполагал, что аудиофайл присутствует в том же каталоге, что и скомпилированное приложение. Однако вам не обязательно загружать SoundPlayerаудио из файла. Если вы создаете короткие звуки, которые воспроизводятся в нескольких местах вашего приложения, может быть, более разумно встроить звуковые файлы непосредственно в скомпилированную сборку в виде двоичного ресурса (не путать с декларативными ресурсами, определяемыми в коде разметки XAML). Эта техника, которая обсуждается в главе 11, работает со звуковыми файлами так же хорошо, как и с графическими изображениями. Например, если вы добавите файл ding.wav под именем ресурса Ding (просто перейдите к узлу PropertiesResources (СвойстваРесурсы) в Solution Explorer и воспользуйтесь поддержкой визуального конструктора), то сможете применить следующий код для его воспроизведения: SoundPlayer player = new SoundPlayer(); player.Stream = Properties.Resources.Ding; player.Play();
На заметку! Класс SoundPlayer не слишком хорошо работает с большими аудиофайлами, поскольку он должен весь файл целиком загрузить в память. Вы можете подумать, что эту проблему можно разрешить, разбив большой аудиофайл на кусочки, однако SoundPlayer не предназначен для этого. Не существует простого способа такой синхронизации SoundPlayer, чтобы он мог воспроизвести множество аудиофрагментов один за другим, поскольку он не обеспечивает никаких средств для организации очередей. Всякий раз, когда вы вызываете PlaySound() или Play(), текущее воспроизведение останавливается. Обходные пути возможны, но намного лучше вместо этого использовать класс MediaElement, который мы обсудим ниже в этой главе.
SoundPlayerAction SoundPlayerAction — новое средство, представленное в WPF, которое облегчает применение класса SoundPlayer . Класс SoundPlayerAction унаследован от TriggerAction, который позволяет использовать его в ответ на любое событие. На заметку! Триггеры событий впервые встречались в главе 12. Кроме того, несколько примеров применения триггеров событий для анимации было приведено в главе 21.
SoundPlayerAction использует единственное свойство — Source, которое отображается на свойство SoundPlayer.Source. Ниже приведена кнопка, применяющая SoundPlayerAction для подключения события Click к звуку. Триггер организован так, что вы можете применить его к множеству кнопок (если перенести его в коллекцию Resources).
Book_Pro_WPF-2.indb 739
19.05.2008 18:11:21
740
Глава 22
Play Sound
При использовании SoundPlayerAction звук всегда воспроизводится асинхронно. Вы также ограничены свойством Source — здесь нет удобного свойства Stream, чтобы воспроизводить аудио из встроенного ресурса. Это означает, что источником аудио-информации может быть только файл. (К сожалению, система URI упаковки приложений, описанная в главе 11, не применима к классу SoundPlayer, поскольку она не является частью WPF.)
Системные звуки Одной из особенностей операционной системы Windows является ее способность отображать аудиофайлы на определенные системные события. Наряду с SoundPlayer в .NET 2.0 также предоставлен класс System.Media.SystemSounds, позволяющий получить доступ к наиболее часто используемым из этих звуков и задействовать их в собственных приложениях. Эта техника работает лучше, если все, что вам нужно — это простые короткие звуки, предназначенные для того, чтобы уведомить о завершении какой-нибудь долгоиграющей операции или подать сигнал предупреждения. К сожалению, класс SystemSounds основан на MessageBeep из Win32 API, в результате чего он обеспечивает доступ только к следующим общим системным звукам:
• Asterisk (Звездочка) • Beep (Уведомление о получении почты) • Exclamation (Восклицание) • Hand (Критическая ошибка) • Question (Вопрос) Класс SystemSounds предоставляет свойство для каждого из этих звуков, возвращающее объект SystemSound, который можно использовать для воспроизведения звука с помощью метода Play(). Например, для воспроизведения звука Beep в коде служит следующая строка: SystemSounds.Beep.Play();
Чтобы указать системе, какие файлы WAV следует применять для каждого системного звука, войдите в панель управления и выберите пиктограмму Audio Devices (в Windows XP) либо пиктограмму Sound (в Windows Vista).
MediaPlayer Классы SoundPlayer, SoundPlayerAction и SystemSounds легко использовать, но все они относительно маломощны. В современном мире вместо исходного формата WAV
Book_Pro_WPF-2.indb 740
19.05.2008 18:11:22
741
Звук и видео
намного более распространен сжатый формат звука MP3 для всех целей, за исключением простейших звуков. Но если вы хотите воспроизводить MP3 или MPEG-видео, то для этого обратитесь к следующим двум классам — MediaPlayer и MediaElement. Оба класса зависят от ключевых элементов технологии, представленной в проигрывателе Windows Media. Однако тут содержится ловушка — они требуют наличия проигрывателя Windows Media версии 10 или выше. Windows Vista облегчает задачу, поскольку включает в себя проигрыватель Windows Media версии 11, но существующие инсталляции Windows XP не гарантируют этого. На заметку! Windows XP припасла другую ловушку для программистов 64-разрядных приложений, а именно: 64-разрядная версия Windows XP включает 32-разрядную версию Media Player. В результате этого вы должны компилировать ваше приложение WPF в 32-разрядном формате, чтобы гарантировать поддержку аудио и видео. (Это стандарт для любого проекта WPF, если только вы явно не сконфигурируете его как 64-разрядное приложение, которое будет запускаться только на 64-разрядных версиях Windows.) Класс MediaPlayer (находящийся в специфичном для WPF пространстве имен System.Windows.Media) — это WPF-эквивалент класса SoundPlayer. Хотя ясно, что он не настолько легковесен, все же работает он примерно так же. Вы создаете объект MediaPlayer, вызываете метод Open() для загрузки аудиофайла и вызываете Play() для асинхронного запуска воспроизведения. (Опция синхронного воспроизведения не предусмотрена.) Рассмотрим пример: private MediaPlayer player = new MediaPlayer(); private void cmdPlayWithMediaPlayer_Click(object sender, RoutedEventArgs e) { player.Open(new Uri("test.mp3", UriKind.Relative)); player.Play(); }
Есть несколько важных деталей, на которые нужно обратить внимание в этом примере.
• MediaPlayer создается вне обработчика событий, поэтому он существует в течение жизненного цикла окна. Это потому, что метод MediaPlayer.Close() вызывается тогда, когда объект MediaPlayer удаляется из памяти. Если вы создадите объект MediaPlayer в обработчике событий, он будет освобожден из памяти почти немедленно и, вероятно, вскоре после этого будет удален сборщиком мусора, и тогда будет вызван метод Close() и воспроизведение прервется. На заметку! Вы должны создать обработчик события Window.Unloaded, чтобы вызвать Close() для остановки любого воспроизводящегося в данный момент звука при закрытии окна.
• Местоположение файла указывается в виде URI. К сожалению, этот URI не использует синтаксис упаковки приложений, с которым вы ознакомились в главе 11, так что невозможно встроить аудиофайл и воспроизвести его, используя класс MediaPlayer. Это ограничение объясняется тем, что класс MediaPlayer построен на функциональности, которая не является “родной” для WPF, а представлена отдельным, неуправляемым компонентом проигрывателя Windows Media.
• Код обработки исключений отсутствует. Это возмутительно, но методы Open() и Play() не возбуждают исключений (процессы асинхронной загрузки и воспроизведения в некоторой степени заслуживают порицания). Вместо этого вам предлагается самостоятельно обрабатывать события MediaOpened и MediaFailed, если вы хотите определить, было ли запущено воспроизведение вашего аудио.
Book_Pro_WPF-2.indb 741
19.05.2008 18:11:22
742
Глава 22
MediaPlayer достаточно прост, хотя имеет больше возможностей, чем SoundPlayer. Он предоставляет небольшой набор полезных методов, свойство и событий. Их полный перечень представлен в табл. 22.1. Таблица 22.1. Ключевые члены MediaPlayer Член
Описание
Balance
Устанавливает баланс между левым и правым каналом, как число от -1 (только левый канал) до 1 (только правый канал).
Volume
Устанавливает громкость в виде числе от 0 (полная тишина) до 1 (полная громкость). Значение по умолчанию — 0.5.
SpeedRatio
Устанавливает повышенную скорость при воспроизведении звука (или видео). Значение по умолчанию — 1, что означает нормальную скорость, в то время как 2 — двойная скорость, 10 — скорость, вдесятеро выше нормальной, 0.5 — половина нормальной скорости и т.д. Можно использовать любое положительное значение типа double.
HasAudio и HasVideo
Указывает на то, содержит ли текущий загруженный медиафайл, соответственно, аудио- и видеосоставляющие. Чтобы показать видео, следует использовать класс MediaElement, описанный ниже.
NaturalDuration, NaturalVideoHeight, NaturalVideoWidth
Указывают на то, идет ли воспроизведение на нормальной скорости, а также — размер видео-окна. (Как вы убедитесь ниже, вы можете растягивать и сжимать видео для заполнения окон разного размера.)
Position
TimeSpan, указывающий текущее местоположение в медиафайле. Вы можете устанавливать это свойство, чтобы пропустить часть файла и продолжить воспроизведение с указанного места.
DownloadProgress и BufferingProgress
Показывает процент выгружаемого файла (удобно в тех случаях, когда Source представляет собой URL, указывающий на местоположение в Internet или другой компьютер). Процент представлен в виде числа от 0 до 1.
Clock
Получает или устанавливает MediaClock, ассоциированный с проигрывателем. MediaClock используется только тогда, когда вы синхронизируете аудио с временной шкалой (примерно так же, как это делалось при синхронизации анимации с временной шкалой в главе 21). Если вы используете методы MediaPlayer для выполнения воспроизведения вручную, это свойство равно null.
Open()
Загружает новый медиафайл.
Play()
Начинает воспроизведение. Не имеет никакого эффекта, если файл уже воспроизводится.
Pause()
Приостанавливает временно воспроизведение, не меняя его позиции. Если вызвать Play() снова, то воспроизведение начнется с текущей позиции. Если воспроизведение не происходит, не дает никакого эффекта.
Stop()
Останавливает воспроизведение и сбрасывает позицию на начало файла. Если снова вызвать Play(), то воспроизведение начнется с начала файла. Не имеет эффекта, если воспроизведение уже остановлено.
Используя эти члены класса, вы можете построить базовый полнофункциональный медиапроигрыватель. Однако программисты WPF обычно использует другой, относительно более простой элемент, который мы опишем в следующем разделе — MediaElement.
Book_Pro_WPF-2.indb 742
19.05.2008 18:11:22
Звук и видео
743
MediaElement MediaElement — это элемент WPF, служащий оболочкой функциональности класса MediaPlayer. Подобно всем элементам, MediaElement помещается непосредственно в пользовательский интерфейс. Если вы применяете MediaElement для воспроизведения аудио, это не имеет значения, но если воспроизводится видео, то вы должны разместить его там, где у вас должно появиться видео-окно. Простейший дескриптор MediaElement — это все, что вам нужно для воспроизведения звука. Например, если вы добавляете такую разметку к вашему пользовательскому интерфейсу:
то аудиофайл test.mp3 будет воспроизведен немедленно после загрузки (что более или менее совпадет с загрузкой окна).
Программное воспроизведение аудио Обычно в тонком управлении воспроизведением необходимости нет. Например, может потребоваться, чтобы в определенный момент оно было запущено, постоянно воспроизводилось повторно и т.д. Один способ достичь этого заключается в применении методов класса MediaElement в надлежащий момент. Поведение MediaElement при запуске определяется его свойством LoadedBehavior, которое является одним из нескольких свойств, которые добавлены в классе MediaElement и которых нет в классе MediaPlayer. Свойство LoadedBehavior принимает любое значение из перечисления MediaState. По умолчанию оно равно Play, но вы также можете использовать Manual — в этом случае файл загружается, а потом уже ваш код отвечает за запуск воспроизведения в нужный момент. Другое значение этого свойства — Pause, установка которого приостанавливает воспроизведение, но не позволяет вам использовать методы воспроизведения. (Вместо этого вам придется запускать воспроизведение с помощью триггеров и раскадровки, что будет описано в следующем разделе.) На заметку! Класс MediaElement также предоставляет свойство UnloadedBehavior, определяющее, что произойдет при выгрузке элемента. В данном случае единственным осмысленным выбором может быть Close, поскольку это закроет файл и освободит все системные ресурсы. Итак, чтобы воспроизвести аудиофайл программно, вы должны начать с изменения
LoadedBehavior, как показано ниже:
Кроме того, вы должны выбрать имя, чтобы можно было взаимодействовать с медиа-элементом в программном коде. Обычно взаимодействие состоит из вызова очевидных методов Play(), Pause() и Stop(). Также вы можете установить Position, чтобы перемещаться по аудиозаписи. Ниже приведен простой обработчик событий, который “перематывает” запись к началу и начинает воспроизведение: private void cmdPlay_Click(object sender, RoutedEventArgs e) { media.Position = TimeSpan.Zero; media.Play(); }
Book_Pro_WPF-2.indb 743
19.05.2008 18:11:22
744
Глава 22
Если этот код запускается во время воспроизведения, то его первая строка сбросит позицию на начало, и воспроизведение начнется оттуда. Вторая строка не будет иметь эффекта, поскольку медиафайл уже воспроизводится. Если же вы попытаетесь применить этот код в отношении MediaElement, у которого свойство LoadedBehavior не установлено равным Manual, то получите исключение. На заметку! В типичном медиапроигрывателе вы можете выполнять типовые команды вроде воспроизведения, паузы и останова более чем одним способом. Очевидно, что это отличное место для того, чтобы применить модель команд WPF. Фактически, для этого существует класс команд, который уже включает некоторую удобную инфраструктуру, и этот класс — System. Windows.Input.MediaCommands. Однако MediaElement не имеет никаких привязок команд по умолчанию, которые поддерживают класс MediaCommands. Другими словами, на вашей совести лежит задача написания логики обработки событий, которая реализует каждую команду и вызовет соответствующий метод MediaElement. В ваших силах организовать код так, чтобы несколько элементов пользовательского интерфейса были привязаны к одной и той же команде для сокращения дублирования кода. В главе 10 о командах рассказано подробнее.
Обработка событий MediaElement не возбуждает исключения, если не может найти или загрузить файл. Вместо этого вам предлагается обработать событие MediaFailed. К счастью, это простая задача. Просто подкорректируйте дескриптор MediaElement:
И в обработчике событий используйте свойство ExceptionRoutedEventArgs. ErrorException, чтобы получить объект исключения, описывающий проблему: private void media_MediaFailed(object sender, ExceptionRoutedEventArgs e) { lblErrorText.Content = e.ErrorException.Message; }
Воспроизведение аудио с помощью триггеров До сих пор вы не получили никаких преимуществ от перехода от класса MediaPlayer к MediaElement (помимо поддержки видео, о чем мы поговорим в этой главе позже). Однако, используя MediaElement, вы также получаете возможность управлять аудио декларативно, через код разметки XAML, вместо программного кода. Это делается с помощью триггеров и раскадровок, с которыми вы знакомы из главы 21, где шла речь об анимации. Единственный новый ингредиент — MediaTimeline, который управляет временной шкалой вашего аудио- или видеофайла, и работает совместно с MediaElement для координации воспроизведения. MediaTimeline наследуется от Timeline и добавляет свойство Source, идентифицирующее аудиофайл, который вы хотите воспроизвести. Следующий код разметки демонстрирует простой пример. Он использует действие BeginStoryboard для запуска воспроизведения звука, когда выполняется щелчок на кнопке. (Понятно, что вы можете с тем же успехом отреагировать и на другие события мыши и клавиатуры.)
Book_Pro_WPF-2.indb 744
19.05.2008 18:11:22
Звук и видео
745
Click me to hear a sound.
Поскольку этот пример воспроизводит аудио, позиционирование MediaElement несущественно. В данном примере он помещается внутри Grid, перед Button. (Порядок не важен, поскольку во время выполнения программы MediaElement не будет иметь никакого визуального представления.) Когда осуществляется щелчок на кнопке, создается Storyboard c MediaTimeline. Обратите внимание, что источник не специфицирован в свойстве MediaElement.Source. Вместо этого источник передается через свойство MediaElement.Source. На заметку! Когда вы используете MediaElement в качестве цели для MediaTimeline, уже не имеет значения, установлены ли LoadedBehavior и UnloadedBehavior. Как только вы применили MediaTimeline, ваше аудио или видео управляется таймером анимации WPF (конкретно — экземпляром класса MediaClock, который представлен в свойстве MediaElement.Clock). Вы можете использовать единственный экземпляр Storyboard для контроля воспроизведения в единственном MediaElement. Другими словами, не только останавливать, но также приостанавливать временно и возобновлять воспроизведение. Например, рассмотрим крайне простой четырехкнопочный медиапроигрыватель, показанный на рис. 22.1.
Рис. 22.1. Окно управления воспроизведением Это окно использует единственный MediaElement, MediaTimeline и Storyboard. Storyboard и MediaTimeline объявлены в коллекции Window.Resources:
Book_Pro_WPF-2.indb 745
19.05.2008 18:11:22
746
Глава 22
Единственная сложность состоит в том, что вы должны не забыть определить все триггеры для управления раскадровкой в одной коллекции. Вы можете затем присоединить их к соответствующим элементам управления, используя свойство EventTrigger. SourceName. В данном примере все триггеры объявлены внутри StackPanel, содержащей кнопки. Вот эти триггеры и кнопки, использующие их для управления аудио: Play Stop Pause Resume
Обратите внимание, что даже несмотря на то, что реализация MediaElement и MediaPlayer позволяют возобновить воспроизведение после паузы вызовом Play() , Storyboard работает иначе. Вместо этого требуется отдельное действие ResumeStoryboard. Если это не то, что вам нужно, вы можете добавить некоторый код к вашей кнопке воспроизведения вместо применения декларативного подхода. На заметку! Загружаемый код для этой главы включает примеры окна декларативного медиапроигрывателя и более гибкого окна медиапроигрывателя, управляемого кодом.
Воспроизведение множества звуков Хотя предыдущий пример демонстрирует воспроизведение единственного медиафайла, нет никаких причин, которые помешали бы вам расширить его, добавив возможность одновременного воспроизведения нескольких аудиофайлов. Следующий пример включает две кнопки, каждая из которых запускает воспроизведение своего собственного звука. Когда выполняется щелчок на кнопке, создается новый объект Storyboard, с новой MediaTimeline, которая используется для воспроизведения отдельного аудиофайла через один и тот же MediaElement.
Book_Pro_WPF-2.indb 746
19.05.2008 18:11:22
Звук и видео
747
Click me to hear a sound. Click me to hear a different sound.
В данном примере, если вы быстро щелкнете на обеих кнопках подряд, то обнаружите, что второй звук прервет воспроизведение первого. Это следствие применения одного и того же MediaElement для обеих временных шкал. Более гибкий (но и более ресурсоемкий) подход предусматривает использование отдельного MediaElement для каждой кнопки и установке каждой MediaTimeline на соответствующий MediaElement. (В этом случае вы можете специфицировать Source непосредственно в дескрипторе MediaElement, поскольку он не изменяется.) Теперь, если вы быстро щелкнете подряд на двух кнопках, оба звука будут воспроизводиться одновременно. То же самое касается класса MediaPlayer. Если вы хотите воспроизводить несколько аудиофайлов, то вам понадобится несколько объектов MediaPlayer. Если вы решите использовать в коде MediaPlayer или MediaElement, то у вас появится шанс осуществить более разумную оптимизацию, которая, например, позволит воспроизводить одновременно только два звука, но не больше. Базовая техника заключается в определении двух объектов MediaPlayer с переключением между ними всякий раз, когда вы хотите запустить воспроизведение нового звука. (Отслеживать, какой объект использовался последний раз, можно с помощью переменной булевского типа.) Чтобы облегчить применение этого приема, вы можете поместить имена аудиофайлов в свойство Tag соответствующего элемента, так что весь ваш код обработки событий находил для применения правильный MediaPlayer, устанавливал свойство Source и вызывал метод Play().
Book_Pro_WPF-2.indb 747
19.05.2008 18:11:23
748
Глава 22
Изменение громкости, баланса, скорости и позиции воспроизведения MediaElement предоставляет в ваше распоряжение те же свойства, что и MediaPlayer (перечисленные в табл. 22.1) для управления громкостью, балансом, скоростью и текущей позицией медиафайла. На рис. 22.2 показано простое окно, расширяющее пример аудиопроигрывателя на рис. 22.1, с дополнительными элементами для управления этими параметрами.
Рис. 22.2. Управление дополнительными параметрами воспроизведения Бегунки громкости и баланса привязать проще всего. Поскольку Volume и Balance — свойства зависимостей, вы можете подключить их элементы управления к MediaElement выражением двунаправленной привязки. Вот что вам нужно:
Хотя выражение двунаправленной привязки требует некоторых дополнительных накладных расходов, они обеспечивают обратную связь: если свойства MediaElement будут изменены каким-то другим способом, эти бегунки останутся синхронизированными с текущими значениями свойств. Свойство SpeedRatio может быть подключено аналогичным образом:
Однако здесь есть несколько нюансов. Во-первых, SpeedRatio не задействовано в управляемом таймером аудио (где применяется MediaTimeline). Чтобы использовать его, вам придется установить свойство LoadedBehavior из SpeedRatio в Manual и принять управление воспроизведением на себя через соответствующие методы. Совет. Если вы используете MediaTimeline , то получите тот же эффект от действия SetStoryboardSpeedRatio, что и установка свойства MediaElement.SpeedRatio. Подробно обо всех этих деталях было рассказано в главе 21.
Book_Pro_WPF-2.indb 748
19.05.2008 18:11:23
Звук и видео
749
Во-вторых, SpeedRatio не является свойством зависимостей, и WPF не принимает уведомлений о его изменениях. Это значит, что если вы включите код, модифицирующий свойство SpeedRatio, то бегунок не будет обновлен соответственно. (Одним из обходных путей может быть модификация его положения в коде вместо прямой модификации MediaElement.) На заметку! Изменение скорости воспроизведения аудио может исказить звук и вызвать появление эффектов вроде эха. И последняя деталь — текущая позиция, которая представлена свойством Position. Опять-таки, MediaElement должен быть в режиме Manual, прежде чем вы установите свойство Position, что означает невозможность применения MediaTimeline. (При использовании TimeLine подумайте о применении действия BeginStoryboard вместе с Offset для установки требуемой позиции, как описано в главе 21.) Чтобы заставить это работать, не применяйте никаких привязок данных в бегунке:
Для установки позиции бегунка при открытии медиафайла вы можете использовать код, подобный следующему: private void media_MediaOpened(object sender, RoutedEventArgs e) { sliderPosition.Maximum = media.NaturalDuration.TimeSpan.TotalSeconds; }
Затем при перемещении бегунка вы можете перепрыгнуть в определенную позицию: private void sliderPosition_ValueChanged(object sender, RoutedEventArgs e) { // Приостановка воспроизведения перед переходом в другую позицию // исключит "заикания" при слишком быстрых движениях бегунка. media.Pause(); media.Position = TimeSpan.FromSeconds(sliderPosition.Value); media.Play(); }
Недостаток такого решения состоит в том, что бегунок не будет обновляться по мере воспроизведения. Если вам нужно это, придется прибегнуть к обходному маневру (вроде DispatcherTimer, который будет выполнять периодическую проверку текущей позиции в процессе воспроизведения и соответственно обновлять положение бегунка). То же самое справедливо и при использовании MediaTimeline. По разным причинам вы не можете привязаться напрямую к информации MediaElement.Clock. Вместо этого вам придется обрабатывать событие Storyboard.CurrentTimeInvalidated, как было показано в примере AnimationPlayer из главы 21.
Синхронизация анимации с аудио В некоторых случаях вам может понадобиться синхронизировать другую анимацию с определенной точкой медиафайла (аудио или видео). Например, если у вас есть длинный аудиофайл, инструктирующий о каком-то наборе шагов, вы можете решить показывать разные изображения после каждой паузы. В зависимости от ваших нужд, дизайн может получиться очень сложным, и, возможно, для его упрощения и достижения лучшей производительности стоит сегментировать аудио в несколько файлов. Таким образом, вы сможете загружать новый аудиофрагмент
Book_Pro_WPF-2.indb 749
19.05.2008 18:11:23
750
Глава 22
и выполнять соответствующие действия одновременно, просто реагируя на соответствующее событие MediaEnded. В таких ситуациях вам понадобится синхронизировать нечто с продолжительным, непрерывным воспроизведением медиафайла. Один прием, позволяющий связать воспроизведение с другими действиями, состоит в применении анимации ключевого кадра (которая была представлена в главе 21). Вы можете поместить анимацию ключевого кадра вместе с вашим MediaTimeline на одну раскадровку. Подобным образом вы можете применить определенные смещения времени в анимации, которые будут соответствовать определенным моментам времени в аудиофайле. Фактически, вы даже можете воспользоваться программой независимых разработчиков, которая может аннотировать аудио и экспортировать список важных моментов времени. Затем вы можете применить эту информацию для установки времени для каждого ключевого кадра. Используя анимацию ключевого кадра, важно установить свойство Storyboard. SlipBehavior в Slip. Это укажет, что ваша анимация ключевого кадра не должна обгонять MediaTimeline, если происходит задержка воспроизведения. Это важно потому, что MediaTimeline может тормозить из-за буферизации (когда зависит от потока с сервера), или же, что бывает чаще, по причине задержки при загрузке. Следующий код разметки демонстрирует базовый пример применения аудиофайла с двумя синхронизированными анимациями. Первая изменяет текст в метке по достижении определенного места в аудиофайле. Вторая показывает маленький кружок на полпути воспроизведения аудио, который пульсирует во времени за счет изменения свойства Opacity.
Book_Pro_WPF-2.indb 750
19.05.2008 18:11:23
Звук и видео
751
Чтобы сделать этот пример более интересным, мы также включили в него бегунок, позволяющий изменять его позицию. Вы увидите, что даже если вы изменяете позицию бегунком, все три анимации выравниваются автоматически в соответствующую точку MediaTimeline. (Бегунок синхронизируется с помощью события Storyboard. CurrentTimeInvalidated, а событие ValueChanged обрабатывается для поиска новой позиции после того, как пользователь передвинет бегунок с помощью мыши. Оба приема продемонстрированы в главе 21, в примере AnimationPlayer.) На рис. 22.3 показана эта программа в действии.
Рис. 22.3. Синхронизация анимаций
Воспроизведение видео Все, что вы узнали о применении класса MediaElement, в равной степени касается и воспроизведения видеофайлов. Как можно ожидать, класс MediaElement поддерживает все видеоформаты, которые поддерживает проигрыватель Windows Media. Хотя поддержка и зависит от инсталлированных кодеков, вы вполне можете рассчитывать на базовую поддержку форматов WMV, MPEG и AVI. Ключевое отличие при воспроизведении видеофайлов заключается в том, что здесь становятся важными визуальные свойства MediaPlayer, а также свойства, касающиеся его компоновки. Важнее всего свойства Stretch и StretchDirection, определяющие, как масштабируется видео-окно для заполнения контейнера (эти свойства работают так же, как свойства Stretch и StretchDirection классов-наследников Shape). При установке значения Stretch вы можете использовать None для сохранения оригинального размера, Uniform — чтобы растянуть его для заполнения контейнера без изменения пропорций, Fill — для растяжения по обоим измерениям до размеров контейнера (даже если это значит искажение пропорций) и UniformToFill — для растяжения картинки, чтобы она уместилась в максимальное измерение контейнера, а пропорции сохранились (при этом, если пропорции видео-окна не будут совпадать с пропорциями контейнера, то часть видео-окна будет усечена). Совет. Предпочтительный размер MediaElement основан на “родных” пропорциях видео. Например, если вы создаете MediaElement со значением Stretch, равным Uniform (по умолчанию так и есть) и помещаете его внутрь строки Grid c Height, установленным в Auto, то строка будет подогнана по размеру так, чтобы вместить видео в его стандартном размере, чтобы масштабирование не понадобилось.
Book_Pro_WPF-2.indb 751
19.05.2008 18:11:23
752
Глава 22
Видео-эффекты Поскольку MediaElements работает как любой другой элемент WPF, у вас есть возможность манипулировать им несколько неожиданным образом. Ниже описаны некоторые примеры.
• Вы можете использовать MediaElement как содержимое элемента управления, скажем, кнопки.
• Вы можете установить содержимое для тысяч элементов сразу с множеством объектов MediaElement, хотя процессор поведет себя не очень хорошо при такой нагрузке.
• Вы можете комбинировать видео с трансформациями через свойства LayoutTransformor или RenderTransform. Это позволит перемещать видео-окно, растягивать, наклонять или вращать его. Совет. Обычно для MediaElement трансформация RenderTransform предпочтительнее, чем LayoutTransformor, поскольку она более легковесная. Кроме того, она принимает во внимание значение удобного свойства RenderTransformOrigin, позволяя использовать относительные координаты для определенных трансформаций (таких как вращение).
• Вы можете установить свойство Clipping объекта MediaElement так, чтобы обрезать видео-окно по определенной фигуре или пути и показывать только часть полного окна.
• Вы можете устанавливать свойство Opacity, позволяя отображать другое содержимое сквозь ваше видео-окно. Фактически, вы даже можете сложить вместе в стопку несколько полупрозрачных видео-окон (с тяжелыми последствиями для производительности).
• Вы можете использовать анимацию, чтобы динамически изменять свойство MediaElement (или одного из его трансформаций).
• Вы можете копировать текущее содержимое видео-окна в другое место вашего пользовательского интерфейса с использованием VisualBrush, что позволяет создавать специфические эффекты типа отражений.
• Вы можете поместить видео-окно на трехмерную поверхность и использовать анимацию для перемещения его во время воспроизведения видео (как описано в главе 23). Например, следующий код разметки создает эффект отражения, показанный на рис. 22.4. Он делает это посредством создания объекта Grid, состоящего из двух строк. Верхняя строка содержит MediaElement, воспроизводящий видеофайл. Нижняя строка содержит Rectangle, который рисуется с помощью VisualBrush. Трюк в том, что VisualBrush принимает свое содержимое от видео-окна, расположенного над ним, используя выражение привязки. Видео-содержимое затем опрокидывается посредством свойства RelativeTransform, а затем плавно затеняется сверху вниз с помощью градиента OpacityMask.
Book_Pro_WPF-2.indb 752
19.05.2008 18:11:23
Звук и видео
753
Рис. 22.4. Отраженное видео
Book_Pro_WPF-2.indb 753
19.05.2008 18:11:23
754
Глава 22
Этот пример работает очень хорошо. Эффект отражения влечет за собой те же накладные расходы, что и два видео-окна, поскольку каждый кадр должен копироваться в нижний прямоугольник. В дополнение, каждый кадр должен быть опрокинут и затенен, чтобы создать эффект отражения (WPF использует промежуточную поверхность отображения для выполнения таких трансформаций). Но на современном компьютере этими накладными расходами можно пренебречь. Это не так в случае других видеоэффектов. Фактически видео — одна из немногих областей WPF, где очень легко перегрузить процессор и получить неважно работающие интерфейсы. Средние компьютеры не могут обрабатывать более чем несколько видеоокон одновременно (конечно, в зависимости от размера вашего видеофайла — более высокое разрешение и большая частота кадров, естественно, означает больший объем данных, на обработку которых требуется больше времени). Среди загружаемого кода для этой главы есть еще один пример, демонстрирующий видеоэффекты: анимация, вращающая видео-окно, в котором продолжается воспроизведение. Несмотря на необходимость стирать каждый видеокадр и перерисовывать новый под слегка измененным углом, такая анимация работает относительно неплохо на современных видеокартах, но вызывает заметное мерцание на картах послабее. Если вы сомневаетесь, то лучше ориентируйте ваш пользовательский интерфейс на маломощный компьютер, чтобы посмотреть, справится ли он, а затем постепенно повышать сложность эффектов либо отключать их на слабых видеокартах.
Класс VideoDrawing WPF включает в себя класс VideoDrawing, унаследованный от класса Drawing, о котором мы рассказывали в главе 14. VideoDrawing может быть использован для создания кисти DrawingBrush, которая, в свою очередь, может применяться для заполнения поверхности элемента, создавая тот же эффект, что был продемонстрирован выше в примере с VisualBrush. Однако есть одно отличие, которое может сделать подход с VideoDrawing более эффективным. Это связано с тем, что VideoDrawing использует класс MediaPlayer, в то время как подход на основе VisualBrush требует применения класса MediaElement. Класс MediaPlayer не нуждается в управлении компоновкой, фокусом или другими деталями элемента, поэтому он более легковесный, чем класс MediaElement. В некоторых ситуациях применение VideoDrawing и DrawingBrush вместо MediaElement и VisualBrush помогает избежать необходимости в промежуточной поверхности визуализации и за счет этого повышает производительность (хотя в результате нашего собственного тестирования мы не заметили существенной разницы между этими двумя подходами). Использование VideoDrawing требует несколько больших усилий, поскольку MediaPlayer должен быть запущен в коде (вызовом метода Play()). Обычно вы будете создавать все три объекта — MediaPlayer, VideoDrawing и DrawingBrush — в коде. Приведем базовый пример, выводящий видео в фоне текущего окна. // Создать временную шкалу. // Это не обязательно, но позволит конфигурировать // детали, которые иначе конфигурировать невозможно. MediaTimeline timeline = new MediaTimeline( new Uri("test.mpg", UriKind.Relative)); timeline.RepeatBehavior = RepeatBehavior.Forever; // Создать часы, разделяемые с MediaPlayer. MediaClock clock = timeline.CreateClock(); MediaPlayer player = new MediaPlayer(); player.Clock = clock;
Book_Pro_WPF-2.indb 754
19.05.2008 18:11:24
Звук и видео
755
// Создать VideoDrawing. VideoDrawing videoDrawing = new VideoDrawing(); videoDrawing.Rect = new Rect(150, 0, 100, 100); videoDrawing.Player = player; // Присвоить DrawingBrush. DrawingBrush brush = new DrawingBrush(videoDrawing); this.Background = brush; // Запустить временную шкалу. clock.Controller.Begin();
Речь Поддержка аудио и видео — несущий стержень платформы WPF. Однако WPF также включает библиотеки, вращающиеся вокруг двух менее широко используемых средств мультимедиа: синтеза и распознавания речи. Оба эти средства поддерживаются классами из сборки System.Speech.dll. По умолчанию Visual Studio не добавляет ссылок на эту сборку в новые проекты WPF, так что в своих проектах вы должны сделать это самостоятельно. На заметку! Речь — периферийная часть WPF. Хотя поддержка речи технически считается частью WPF, и появилась там, начиная с версии .NET Framework 3.0, пространства имен классов для поддержки речи начинаются с System.Speech, а не System.Windows.
Синтез речи Синтез речи — это средство, генерирующее говорящий аудиосигнал на основе предоставленного вами текста. Синтез речи не является встроенным средством в WPF — это средство доступа к Windows. Такие системные утилиты, как Narrator (облегченное экранное средство чтения), включены в Windows XP и Windows Vista, и используют синтез речи для помощи пользователям в навигации по основным диалоговым окнам. Говоря в общем, синтез речи может быть использован для создания аудиоруководств и “говорящих” инструкций, хотя заранее записанное аудио обеспечивает более высокое качество. На заметку! Синтез речи имеет смысл, когда вам нужно создать аудио для динамического текста — другими словами, когда на момент компиляции вы не знаете, какие слова должны быть произнесены во время выполнения. Но если аудио неизменно, то предварительно записанное легче использовать, это более эффективно и качество звука выше. Единственная причина отдать предпочтение синтезу речи — это когда вам нужно прочитать огромный объем текста, и предварительная его запись непрактична. Хотя и Windows XP и Windows Vista имеют встроенный синтез речи, используемый ими компьютеризованный голос отличается. Windows XP использует голос, похожий на голос робота и называемый Sam, в то время как Windows Vista включает более натуральный женский голос по имени Anna. Вы можете загрузить и инсталлировать дополнительные голоса для обеих операционных систем. Воспроизведение речи обманчиво просто. Все, что вам нужно — это создать экземпляр класса SpeechSynthesizer из пространства имен System.Speech.Synthesis и вызвать его метод Speak() со строкой текста. Вот пример: SpeechSynthesizer synthesizer = new SpeechSynthesizer(); synthesizer.Speak("Hello, world");
Book_Pro_WPF-2.indb 755
19.05.2008 18:11:24
756
Глава 22
При использовании такого подхода — передаче простого текста объекту
SpeechSynthesizer — в некоторой степени вы утрачиваете контроль. Вы можете встретить слова, которые произносятся неправильно, с неправильными ударениями или не с должной скоростью. Чтобы получить больший контроль за произносимым текстом, вам нужно использовать класс PromptBuilder для конструирования определения речи. Вот как можно изменить предыдущий пример с полностью эквивалентным кодом, использующим PromptBuilder: PromptBuilder prompt = new PromptBuilder(); prompt.AppendText("Hello, world"); SpeechSynthesizer synthesizer = new SpeechSynthesizer(); synthesizer.Speak(prompt);
Этот код не дает никаких преимуществ. Однако класс PromptBuilder содержит в себе множество других методов, которые вы можете использовать для настройки произношения текста. Например, вы можете подчеркнуть определенное слово (или несколько слов), используя перегруженную версию метода AppendText(), которая принимает значение из перечисления PromptEmphasis. Хотя точный эффект от выделения слова зависит от используемого голоса, следующий код подчеркивает слово are в предложении “How are you?”: PromptBuilder prompt = new PromptBuilder(); prompt.AppendText("How "); prompt.AppendText("are ", PromptEmphasis.Strong); prompt.AppendText("you");
Метод AppendText() имеет еще две перегрузки: одна принимает значение PromptRate, которое позволяет увеличивать или уменьшать скорость, а другая получает значение PromptVolume, позволяющее увеличивать или уменьшать громкость. Вы можете применять значения для всех трех параметров либо только одного или двух, которые хотите использовать. Чтобы использовать объект PromptStyle , следует вызвать PromptBuilder. BeginStyle(). Созданный вами PromptStyle затем применяется ко всему произносимому тексту, до тех пор, пока не будет вызван EndStyle(). Ниже приведен измененный пример, использующий выделения и изменения скорости для ударения на слова are. PromptBuilder prompt = new PromptBuilder(); prompt.AppendText("How "); PromptStyle style = new PromptStyle(); style.Rate = PromptRate.ExtraSlow; style.Emphasis = PromptEmphasis.Strong; prompt.StartStyle(style); prompt.AppendText("are "); prompt.EndStyle(); prompt.AppendText("you");
На заметку! Если вы вызвали BeginStyle() , то позднее должны вызвать в своем коде EndStyle(). Если вы забудете это сделать, то получите ошибку времени выполнения. Перечисления PromptEmphasis, PromptRate и PromptVolume предлагают довольно грубый способ изменения голоса. Пока нет возможности более тонкого контроля или внесения нюансов и специфических оттенков живой речи в синтезируемый текст. Однако PromptBuilder включает метод AppendTextWithHind(), который позволяет иметь дело с телефонными номерами, датами, временем и словами, которые написаны прописью. Вы применяете ваш выбор, используя перечисление SayAs. Вот пример: prompt.AppendText("The word laser is spelt "); prompt.AppendTextWithHint("laser", SayAs.SpellOut);
Book_Pro_WPF-2.indb 756
19.05.2008 18:11:24
Звук и видео
757
Это синтезирует предложение “The word laser is spelt l-a-s-e-r”. Наряду с методами AppendText() и AppendTextWithHint() класс PromptBuilder также включает небольшую коллекцию дополнительных методов для добавления в поток обычного аудио (AppendAudio()), пауз заданной длительности (AppendBreak()), переключения голосов (StartVoice() и EndVoice()), а также произношения текста в соответствии с определенным заданным фонетическим произношением (AppendTextWithPronounciation()). На самом деле PromptBuilder — это оболочка для стандарта Synthesis Markup Language (SSML), описанного по адресу http://www.w3.org/TR/speech-synthesis. И как таковой, он разделяет все ограничения этого стандарта. Когда вызываются методы PromptBuilder, то “за кулисами” генерируется соответствующий код разметки SSML. Вы можете увидеть финальное SSML-представление вашего кода, вызвав PromptBuilder.ToXml() в конце работы, также можно вызвать PromptBuilder.AppendSsml(), взяв существующий код разметки SSML и добавив его в поток чтения текста.
Распознавание текста Распознавание текста — это средство трансляции произносимой пользователем речи с переводом ее в текст. Как и в случае синтеза речи, распознавание речи — средство операционной системы Windows. Распознавание текста встроено в Windows Vista, но не в Windows XP. Пользователям Windows XP оно доступно в составе Office XP и более поздних версий, в Windows XP Plus! Pack либо в бесплатном комплекте Microsoft Speech Software Development Kit (который можно загрузить по адресу http:// www.microsoft.com/speech/download/sdk51). На заметку! Если в данный момент у вас не запущено распознавание текста, панель инструментов распознавания речи появится, когда вы создадите экземпляр класса SpeechRecognizer. Если при попытке создать экземпляр класса SpeechRecognizer не будет сконфигурировано распознавание речи по вашему голосу, то Windows автоматически запустит мастер, который проведет вас по всем шагам этого процесса. Распознавание речи — это также средство облегчения работы с Windows для людей с ограниченными возможностями. Благодаря ему, такие люди могут голосом взаимодействовать с обычными элементами управления. Распознавание речи также позволяет использовать компьютер, не занимая руки, что очень удобно в некоторых ситуациях. Наиболее простой способ использовать распознавание речи — это создать экземпляр класса SpeechRecognizer из пространства имен System.Speech.Recognition. Затем вы можете присоединить обработчик к событию SpeechRecognized, которое инициируется всякий раз, когда произнесенное слово успешно преобразуется в текст: SpeechRecognizer recognizer = new SpeechRecognizer(); recognizer.SpeechRecognized += recognizer_SpeechReconized;
Затем в обработчике событий вы можете извлечь текст из свойства SpeechRecognized
EventArgs.Result: private void recognizer_SpeechReconized(object sender, SpeechRecognizedEventArgs e) { MessageBox.Show("You said:" + e.Result.Text); }
SpeechRecognizer служит оболочкой для COM-объекта. Чтобы избежать неприятных сюрпризов, вы должны объявить его как переменную-член в окне вашего класса (чтобы объект оставался “живым” до тех пор, пока существует окно), и при закрытии
Book_Pro_WPF-2.indb 757
19.05.2008 18:11:24
758
Глава 22
окна вы должны вызывать его метод Dispose() (чтобы освободить ресурсы, используемые распознавателем речи). На заметку! Класс SpeechRecognizer в действительности генерирует последовательность событий при обнаружении аудиосигнала. В начале инициируется SpeechDetected, если звук идентифицируется как речь. Затем один или более раз инициируется SpeechHypothesized, когда слова распознаются на основе опыта. И, наконец, SpeechRecognizer инициирует событие SpeechRecognized, если ему удается успешно обработать текст, либо SpeechRecog nitionRejected — если нет. Событие SpeechRecognitionRejected включает информацию о предположении SpeechRecognizer относительно того, что может означать произнесенное слово, когда степень уверенности недостаточно высока, чтобы принять ввод. Обычно использовать распознавание речи в такой манере не рекомендуется. Это связано с тем, что WPF имеет собственное средство UI Automation, которое работает совместно с механизмом распознавания речи. При правильной конфигурации оно позволяет пользователям вводить текст в текстовых элементах управления и инициировать щелчки на кнопках при произнесении их автоматизированных (automation) имен. Однако вы можете применять SpeechRecognition для добавления поддержки более специализированных команд в специфических сценариях. Это делается путем определения грамматики, основанной на спецификации Speech Recognition Grammar Specification (SRGS). Грамматика SRGS идентифицирует правильные команды для вашего приложения. Например, она может определять, что команды могут использовать только одно из небольшого набора слов (on или off), и что эти слова могут использоваться только в определенных комбинациях (blue on, red on, blue off и т.д.). Вы можете сконструировать грамматику SRGS двумя способами. Можно загрузить ее из документа SRGS, который описывает правила грамматики с применением синтаксиса на основе XML. Чтобы сделать это, вам нужно воспользоваться классом SrgsDocument из пространства имен System.Speech.Recognition.SrgsGrammar: SrgsDocument doc = new SrgsDocument("app_grammar.xml"); Grammar grammar = new Grammar(doc); recognizer.LoadGrammar(grammar);
В качестве альтернативы вы можете сконструировать грамматику декларативно, используя для этого GrammarBuilder. Класс GrammarBuilder играет роль, аналогичную рассмотренному в предыдущем разделе PromptBuilder — позволяет добавлять правила грамматики одно за другим, создавая постепенно полное описание грамматики. Например, ниже приведена декларативно сконструированная грамматика, принимающая ввод из двух слов, где первое слово имеет пять возможных вариантов, а второе — два: GrammarBuilder grammar = new GrammarBuilder(); grammar.Append(new Choices("red", "blue", "green", "black", "white")); grammar.Append(new Choices("on", "off")); recognizer.LoadGrammar(new Grammar(grammar));
Этот код разметки принимает команды вроде red on и green off. Альтернативный ввод вроде yellow on или on red не будет распознан. Объект Choices представляет SRGS-правило on-off, позволяющее пользователю говорить одно слово из диапазона допустимых. Это наиболее универсальный ингредиент, используемый при построении грамматики. Еще несколько дополнительных перегрузок метода GrammarBuilder.Append() принимают различный ввод. Вы можете передать обычную строку — в этом случае грамматика требует от пользователя произнесения
Book_Pro_WPF-2.indb 758
19.05.2008 18:11:24
Звук и видео
759
именно этого слова. Вы можете передать строку, за которой следует значение из перечисления SubsetMatchingMode, требующее от пользователя произнесения определенной части слова или фразы. И, наконец, вы можете передать строку, за которой следует минимальное и максимальное количество повторений. Это позволяет грамматике игнорировать одно и то же слово, когда оно повторяется несколько раз, а также позволяет сделать слово необязательным (задав минимальное число повторов 0). Грамматика, использующая все эти свойства, может стать достаточно сложной. Подробнее о стандарте SRGS и правилах грамматики читайте на http:// www.w3.org/TR/speech-grammar.
Резюме Из данной главы вы узнали, как интегрировать звук и видео в приложения WPF. Мы рассказали о двух разных способах управления воспроизведением медиафайлов — программно, с применением методов классов MediaPlayer или MediaTimeline, либо декларативно, используя раскадровку. Как всегда, наилучший подход зависит от существующих требований. Подход на основе кода предоставляет более высокую степень контроля и гибкости, но также заставляет вас управлять большим количеством деталей и вносит дополнительную сложность. Общее правило можно сформулировать так: подход на основе кода лучше, когда вам нужен тонкий контроль воспроизведения, однако если вам нужно комбинировать воспроизведение медиа с анимацией, то декларативный подход будет намного проще.
Book_Pro_WPF-2.indb 759
19.05.2008 18:11:24
ГЛАВА
23
Трехмерная графика У
же много лет разработчики используют DirectX и OpenGL для построения трехмерных интерфейсов. Однако сложная программная модель и серьезные требования к видеокартам были причиной того, что трехмерное программирование оставалось в стороне от основного потока заказных приложений и программного обеспечения для бизнеса. WPF предлагает новую расширенную трехмерную модель, которая обещает в корне изменить ситуацию. Используя WPF, вы сможете строить сложные трехмерные сцены на основе понятного кода разметки. Вспомогательные классы предоставят проверенные операции вращения с помощью мыши, наряду с другими фундаментальными блоками. И почти любой компьютер, работающий под управлением Windows XP или Windows Vista, сможет отображать трехмерное содержимое, благодаря способности WPF переходить к программной визуализации, когда поддержка со стороны видеокарты недостаточна. Наиболее заслуживающая упоминания характеристика библиотек WPF для трехмерного программирования заключается в том, что они спроектированы как ясное, согласованное расширение модели WPF, с основами которой вы уже ознакомились. Например, вы используете тот же набор классов кистей для рисования трехмерных поверхностей, что и для рисования двумерных фигур. Вы применяете похожую модель для вращения, деформации и перемещения трехмерных объектов, что и при выполнении тех же операций над двумерным содержимым. Здесь предоставлена такая поддержка высокоуровневых средств WPF, которая делает трехмерную графику подходящей для любых целей — от обманчивых визуальных эффектов в простых играх до графической визуализации данных в бизнес-приложениях. (Есть только одна ситуация, в которой трехмерной модели WPF недостаточно — это сложные игры реального времени. Если вы хотите построить вторую Halo, то вам все же лучше обратиться к мощи DirectX.) Даже несмотря на то, что модель трехмерной графики WPF неожиданно ясна и согласована, создание богатых трехмерных интерфейсов остается достаточно сложной задачей. Для того чтобы вручную закодировать трехмерную анимацию (или даже просто понять положенные в ее основу концепции), вам понадобится нечто большее, чем немного математики. А моделирование чего-либо, кроме самых тривиальных трехмерных сцен на основе вручную написанного кода XAML — это громадная, чреватая ошибками работа, намного более сложная, чем двумерный эквивалент ручного создания векторного изображения XAML. По этой причине, скорее всего, вам стоит обратиться к инструментам от независимых разработчиков для создания трехмерных объектов, экспортировать их в XAML, а затем добавлять к своим приложениям. На эту тему написаны целые книги: о математике трехмерного программирования, инструментальных средствах и трехмерных библиотеках для WPF. Из настоящей главы вы узнаете достаточно для понимания модели WPF трехмерного рисования, создадите базовые трехмерные фигура, спроектируете более сложные трехмерные сцены с использованием инструмента трехмерного моделирования и используете некоторый
Book_Pro_WPF-2.indb 760
19.05.2008 18:11:24
Трехмерная графика
761
ценный код, предоставленный командой разработчиков WPF, а также независимыми разработчиками.
Основы трехмерной графики Трехмерная графика в WPF включает в себя следующие ингредиенты:
• • • •
окно просмотра (viewport), содержащее ваше трехмерное содержимое; трехмерный объект; источник света, освещающий часть вашей трехмерной сцены; камера, представляющая точку, из которой вы наблюдаете трехмерную сцену.
Конечно, наиболее сложные трехмерные сцены будут содержать множество объектов и могут включать множество источников света. (Можно также создать трехмерный объект, не требующий источника света, если он сам является таковым.) Однако перечисленные ингредиенты представляют собой хорошую начальную точку. Трехмерную графику от двумерной отличают, прежде всего, второй и третий компоненты. Новички в трехмерном программировании иногда предполагают, что трехмерные библиотеки — это лишь упрощенный способ создания трехмерного внешнего вида, такого как светящийся куб или вращающаяся сфера. Но если это все, что вам нужно, то, вероятно, для такого трехмерного рисования вам стоит ограничиться классами двумерной графики, о которых мы рассказали выше. В конце концов, нет причин, которые помешали бы вам использовать фигуры, трансформации и геометрию, о которой шла речь в главах 13 и 14, чтобы сконструировать формы, выглядящие объемными. Фактически, это даже проще, чем с то же самое с применением трехмерных библиотек. Так в чем же преимущество использования поддержки трехмерной графики в WPF? Первое преимущество состоит в том, что вы можете создавать эффекты, которые чрезвычайно сложно вычислить на основе эмулируемой трехмерной модели. Хорошим примером могут быть такие эффекты, как отражение, которое становится чрезвычайно сложным, когда приходится иметь дело с множественными источниками света и разными материалами с различной отражающей способностью. Другое преимущество использования трехмерной графической модели заключается в том, что она позволяет взаимодействовать с вашим изображением как с набором трехмерных объектов. Это значительно расширяет ваши программистские возможности. Например, однажды построив требуемую трехмерную сцену, вы получаете простую возможность вращения объекта или вращения камеры вокруг объекта. Для выполнения той же работы на основе двумерной программной модели потребуется чудовищный объем кода (и математики). Теперь, когда вы знаете, что вам нужно, попробуем построить пример, включающий все перечисленные выше составляющие. Потом в последующих разделах мы будем постепенно изменять этот пример.
Окно просмотра Если вы хотите работать с трехмерным содержимым, вам понадобится контейнер, который может его в себе разместить. Этот контейнер представлен классом Viewport3D, находящимся в пространстве имен System.Windows.Controls. Класс Viewport3D унаследован от FrameworkElement и потому может быть размещен везде, где размещается любой обычный элемент. Например, вы можете использовать его в качестве содержимого окна либо страницы или же поместить внутрь более сложной компоновки. Класс Viewport3D лишь намекает на сложность трехмерного программирования. Он добавляет только два свойства: Camera, определяющее точку зрения на трехмерную сцену, и Children, содержащее все трехмерные объекты, которые вы хотите поместить
Book_Pro_WPF-2.indb 761
19.05.2008 18:11:24
762
Глава 23
на сцене. Достаточно интересно то, что источник света, освещающий вашу сцену, сам является объектом в окне просмотра. На заметку! Среди унаследованных свойств класса Viewport3D одно является особенно важным — ClipToBounds. Если оно установлено в true (по умолчанию так и есть), то содержимое, выходящее за пределы окна просмотра, усекается. Если же упомянутое свойство равно false, это содержимое появляется поверх любых соседних элементов. Это то же поведение, что вы получаете от свойства ClipToBounds класса Canvas. Однако при использовании Viewport3D тут есть одно существенное отличие: производительность. Установив Videport3D.ClipToBounds в false, можно в значительной мере увеличить производительность при визуализации сложной, часто обновляемой трехмерной сцены.
Трехмерные объекты Окно просмотра может содержать в себе любой трехмерный объект, унаследованный от Visual3D (из пространства имен System.Windows.Media.Media3D, в котором находится подавляющее большинство трехмерных классов). Однако чтобы создать трехмерное представление, вам придется выполнить немного больше работы, чем вы можете предположить. В версии 1.0 библиотеки WPF не хватало коллекции трехмерных фигур-примитивов. Если вам нужен был куб, цилиндр или тор, то приходилось строить их самостоятельно. Одним из наиболее удачных проектных решений команды разработчиков WPF, принятых при создании классов рисования трехмерной графики, была структуризация их способом, подобным структуризации классов рисования плоской графики. Это означает, что вы можете легко разобраться в назначении многих классов, образующих основу трехмерной графики (даже не зная еще, как их следует использовать). В табл. 23.1 представлены соответствующие аналогии.
Таблица 23.1. Сравнение классов 2-D и 3-D Класс 2-D
Класс 3-D
Примечания
Visual
Visual3D
Visual3D — базовый класс для всех трехмерных объектов (объектов, отображаемых внутри контейнера Viewport3D). Подобно классу Visual, вы можете использовать класс Visual3D для наследования от него облегченных трехмерных фигур или создания сложных трехмерных элементов управления, предоставляющих богатый набор событий и каркасных служб. Однако не рассчитывайте на значительную помощь. Более вероятно, что вы воспользуетесь одним из классов-наследников Visual3D, таким как ModelVisual3D или ModelUIElement3D.
Geometry
Book_Pro_WPF-2.indb 762
Geometry3D
Класс Geometry — это абстрактный способ определения двумерной фигуры. Геометрия часто используется для определения сложных фигур, состоящих из дуг, отрезков прямых и многоугольников. Класс Geometry3D — его трехмерный аналог, он представляет трехмерную поверхность. Однако в то время как существует несколько геометрий 2-D, WPF включает только один конкретный класс, унаследованный от Geometry3D — MeshGeometry3D. Класс MeshGeometry3D имеет важнейшее значение в трехмерном рисовании, поскольку вы применяете его для определения всех своих трехмерных объектов.
19.05.2008 18:11:25
Трехмерная графика
763
Окончание табл. 23.1 Класс 2-D
Класс 3-D
Примечания
GeometryDrawing
GeometryModel3D
Существует несколько способов использования двумерного объекта Geometry. Вы можете упаковать его в GeometryDrawing и применить для рисования поверхности элемента или содержимого Visual. Класс GeometryModel3D имеет то же назначение — он принимает Geometry3D, который затем может быть использован в вашем Visual3D.
Transform
Transform3D
Вы уже знаете, что трансформации 2-D — чрезвычайно удобное средство манипулирования элементами и фигурами всех видов и всеми способами, включая перемещение, деформацию и вращение. Трансформации также незаменимы для выполнения анимации. Классы, унаследованные от Transform3D, осуществляют то же “волшебство” с трехмерными объектами. Фактически вы обнаружите неожиданно много похожих классов, таких как RotateTransform3D, ScaleTransform3D, TranslateTransform3D, Transform3DGroup и MatrixTransform3D. Конечно, возможности, предлагаемые дополнительным измерением, существенны, и трехмерные трансформации обеспечивают такие деформации и преобразования, которые выглядят совершенно иначе.
Поначалу вам может показаться, что распутать отношения между этими классами довольно трудно. По сути, Viewport3D содержит объекты Visual3D. Чтобы действительно предоставить Viewport3D некоторое содержимое, необходимо определить объект Geometry3D, описывающий фигуру, и упаковать его в GeometryModel3D. Затем вы можете использовать его как содержимое Visual3D. На рис. 23.1 демонстрируется это отношение.
ModelVisual3D (Порожденный от Visual3D) Содержимое
GeometryModel3D
Геометрия
MeshGeometry (Порожденный от Geometry3D)
Рис. 23.1. Определение трехмерного объекта Это двухшаговый процесс — определение фигур, которые вы хотите использовать, в абстрактном виде, а затем включение их в визуальное представление — является необязательным при двумерном рисовании. Однако он обязателен для рисования трехмерной графики, поскольку в библиотеке отсутствуют предварительно построенные классы 3-D. (Члены команды разработчиков WPF, а также некоторые другие разработчики поместили в Internet пример кода, который призван заполнить пробел, но все это еще находится в процессе развития.) Двухшаговый процесс также важен потому, что трехмерные модели сложнее двумерных. Например, когда вы создаете объект Geometry3D, то не только специфицируете вершины фигуры, но также и материал, из которого она состоит. Разные материалы обладают разными свойствами в отношении отражения и поглощения света.
Book_Pro_WPF-2.indb 763
19.05.2008 18:11:25
764
Глава 23
Геометрия Чтобы построить трехмерный объект, надо начать с построения геометрии. Как вы уже знаете, для этой цели существует только один класс — MeshGeometry3D. Неудивительно, что объект MeshGeometry3D представляет сетку (mesh). Если раньше вы имели дело с трехмерным рисованием (или читали что-нибудь о технологиях, положенных в основу современных видеокарт), то уже должны знать, что компьютеры предпочитают строить трехмерные объекты из треугольников. Это объясняется тем, что треугольники — это простейший, наиболее подробный способ описания поверхности. Треугольники просты, потому что каждый из них определяется всего тремя точками (вершинами в углах). Дуги и кривые поверхности определенно более сложны. Треугольники подробны, потому что все прочие фигуры с прямыми гранями (квадраты, прямоугольники и более сложные многоугольники) могут быть разбиты на коллекции треугольников. Хорошо это или плохо, но современное аппаратное обеспечение графики и ее программирование строится на основе этой базовой абстракции. Очевидно, что большинство трехмерных объектов не будут выглядеть как простые плоские треугольники. Вместо этого вам придется комбинировать треугольники для их построения — иногда всего несколько, но чаще — сотни и тысячи, соединенных друг с другом под разными углами. Такая комбинация треугольников образует пространственную сетку. При достаточном количестве треугольников вы можете, в конечном счете, создать иллюзию чего угодно, включая наиболее сложные поверхности. (Разумеется, следует учитывать фактор производительности, к тому же трехмерные сцены часто используют битовые карты или двумерную графику в треугольниках объемной сетки, создавая иллюзию сложной поверхности с минимальными накладными расходами. WPF поддерживает эту технику.) Представление о том, как определяется сетка, является одним из ключевых моментов трехмерного программирования. Если вы взглянете на класс MeshGeometry3D, то обнаружите, что он включает в себя четыре свойства, описанные в табл. 23.2.
Таблица 23.2. Свойства класса MeshGeometry3D Имя
Описание
Positions
Содержит коллекцию всех точек, определяющих сетку. Каждая точка — вершина треугольника. Например, если ваша сетка состоит из 10 полностью различных треугольников, в этой коллекции будет содержаться 30 точек. Очень часто некоторые из ваших треугольников имеют общие грани, а это означает, что одна вершина может быть вершиной нескольких треугольников. Например, для описания куба требуется 12 треугольников (по два на каждую грань), но только 8 отличающихся точек. Вы можете определить общие вершины несколько раз, что еще более усложнит систему, поэтому лучше контролировать затенение отдельных треугольников с помощью свойства Normals.
TriangleIndices
Определяет треугольники. Каждый элемент этой коллекции представляет отдельный треугольник, ссылаясь на три точки из коллекции Positions.
Normals
Представляет вектор для каждой вершины (каждой точки из коллекции Positions). Этот вектор указывает, как точка повернута для вычисления освещенности. Когда WPF затеняет поверхность треугольника, он измеряет освещенность каждой из трех вершин, чтобы заполнить поверхность треугольника. Получение правильных нормальных векторов определяет разницу в затенении трехмерных объектов. Например, это позволяет выполнить плавный переход между соседними треугольниками или выделить их границу в виде четкой линии.
Book_Pro_WPF-2.indb 764
19.05.2008 18:11:25
Трехмерная графика
765
Окончание табл. 23.2 Имя
Описание
TextureCoordinates
Определяет, как двумерная текстура отображается на ваш трехмерный объект, когда вы используете VisualBrush для его прорисовки. Коллекция TextureCoordinates представляет двумерную точку для каждой трехмерной точки из коллекции Positions.
Затенение с нормалями и отображение текстур мы рассмотрим далее в этой главе. А сначала расскажем о том, как строить базовую сетку. Следующий пример демонстрирует простейшую сетку, состоящую из единственного треугольника. Используемые единицы измерения не имеют значения, поскольку вы можете перемещать камеру ближе или дальше, а также изменять размер или расположение индивидуальных трехмерных объектов, применяя трансформации. Что действительно важно здесь — так это координатная система, показанная на рис. 23.2. Как видите, оси X и Y имеют ту же ориентацию, что и при отображении плоской графики. Новой является ось Z. По мере увеличения значения Z точка отдаляется, по мере уменьшения — приближается. +
Ось Y
\
(1, 0, 0)
\
(0, 1, 0)
(0, \1, 0)
Ос
+ Ось X
ьZ
+ \
Рис. 23.2. Треугольник в трехмерном пространстве Здесь показан элемент MeshGeometry, который вы можете использовать для определения этой фигуры внутри трехмерной области. Объект MeshGeometry3D в этом примере не использует свойства Normals или TextureCoordinates, поскольку фигура очень проста и будет нарисована кистью SolidColorBrush:
Очевидно, что здесь присутствуют только три точки, перечисленные одна за другой в свойстве Positions. Порядок их следования в коллекции Positions не имеет значения, поскольку свойство TriangleIndices ясно определяет треугольник. По сути, свойство TriangleIndices устанавливает только один треугольник, состоящий из точек #0, #2 и #1. Другими словами, свойство TriangleIndices сообщает WPF, что нужно нарисовать треугольник, проведя линии от (-1,0,0) до (1,0,0) и затем до (0,1,0).
Book_Pro_WPF-2.indb 765
19.05.2008 18:11:25
766
Глава 23
Обратите внимание, что трехмерное программирование подчиняется нескольким тонким правилам, которые легко нарушить. При определении фигуры вы сталкиваетесь с первым из них, а именно: перечислять точки, составляющие фигуру, следует в направлении против часовой стрелки относительно оси Z. Данный пример следует этому правилу. Однако вы можете легко нарушить его, если измените TriangleIndices на 0, 1, 2. В данном случае вы все равно определяете тот же треугольник, но вывернутый наизнанку. Другими словами, если вы посмотрите вниз по оси Z (как на рис. 23.2), то на самом деле увидите изнанку треугольника. На заметку! Разница между передней частью трехмерной фигуры и ее изнанкой не так тривиальна, как может показаться. В некоторых случаях вы можете рисовать их разными кистями. Или же вы можете решить вообще не рисовать изнанку, чтобы избежать расхода ресурсов на невидимую часть сцены. Если вы нечаянно укажете точки фигуры в порядке движения часовой стрелки и не определите материала изнанки вашей фигуры, она просто исчезнет из вашей трехмерной сцены.
Геометрическая модель и поверхности Определив нужное свойство MeshGeometry3D, вам нужно поместить его в оболочку GeometryModel3D. Класс GeometryModel3D имеет только три свойства: Geometry , Material и BackMaterial. Свойство Geometry принимает MeshGeometry3D, определяющий фигуру вашего трехмерного объекта. В дополнение вы можете применять свойства Material и BackMaterial для определения поверхностей, из которых состоит ваша фигура. Поверхность важна по двум причинам. Во-первых, она определяет цвет объекта (хотя вы можете использовать и более сложные кисти, рисующие текстуры вместо сплошного цвета). Во-вторых, она определяет то, как материал реагирует на свет. WPF включает четыре класса материалов, каждый из которых наследуется от абстрактного класса Material из пространства имен System.Windows.Media. Media3D. Эти материалы перечислены в табл. 23.3. В данном примере мы используем DiffuseMaterial, который применяется наиболее часто, поскольку его поведение ближе всего к реальным поверхностям.
Таблица 23.3. Классы материалов Имя
Описание
DiffuseMaterial
Создает плоскую матовую поверхность, распределяющую свет равномерно во всех направлениях.
SpecularMaterial
Создает блестящую бесцветную поверхность (типа металла или стекла). Отражает свет в противоположном направлении подобно зеркалу.
EmissiveMaterial
Создает раскаленную поверхность, которая сама излучает свет (хотя этот свет не отражают другие объекты сцены).
MaterialGroup
Позволяет комбинировать более одного материала. Комбинируемые материалы накладываются друг на друга в том порядке, в каком добавлялись к MaterialGroup.
DiffuseMaterial предоставляет единственное свойство Brush, принимающее объект Brush, который вы хотите использовать для рисования поверхности трехмерного объекта. (Если вы применяете кисть, отличную от SolidColorBrush, то вам придется установить свойство MeshGeometry3D.TextureCoordinates для определения способа ее отображения на объект, как будет показано далее в этой главе.)
Book_Pro_WPF-2.indb 766
19.05.2008 18:11:25
Трехмерная графика
767
Вот как можно сконфигурировать треугольник, чтобы он имел желтую матовую поверхность:
В этом примере свойство BackMaterial не установлено, так что с изнанки треугольник просто не будет виден. Все, что остается — это применить GeometryModel3D для установки свойства Content объекта ModelVisual3D и затем поместить его в окно просмотра. Но для того, чтобы увидеть ваш объект, понадобятся еще две вещи: источник света и камера.
Источники света Чтобы создать реалистически окрашенные трехмерные объекты, WPF использует модель освещения. Основная идея состоит в том, что вы добавляете один (или несколько) источников света к трехмерной сцене. Характер освещения ваших объектов зависит от выбранного типа источника света, его положения, направления и интенсивности. Прежде чем погрузиться в изучение освещения WPF, важно понять, что модель освещения WPF ведет себя не так, как свет в реальном мире. Хотя система освещения WPF и предназначена для того, чтобы эмулировать реальный мир, вычисление правильного отражения — задача, требующая серьезных вычислительных ресурсов. WPF допускает ряд упрощений, гарантирующих практичность модели освещения, даже в случае анимированных трехмерных сцен с несколькими источниками света. К таким упрощениям относятся перечисленные ниже.
• Световые эффекты вычисляются для объектов индивидуально. Свет, отраженный от одного объекта, не отражается в другом объекте. Аналогично, объект не отбрасывает тени на другой объект, независимо от его местоположения.
• Освещенность вычисляется для вершин каждого треугольника, а затем интерполируется по его поверхности. (Другими словами, WPF определяет интенсивность света в каждом углу, а затем сглаживает его для заполнения всего треугольника.) В результате такого дизайна объекты, состоящие из относительно небольшого числа треугольников, могут быть освещены неправильно. Чтобы обеспечить лучшее освещение, вам следует делить ваши фигуры на сотни или даже тысячи треугольников. В зависимости от эффекта, которого вы пытаетесь достичь, вам может понадобиться как-то обойти эти ограничения, комбинируя несколько источников света, используя разные материалы и даже добавляя дополнительные фигуры. Фактически получение наилучшего результата — это часть искусства проектирования трехмерных сцен. На заметку! Даже если вы не укажете источник света, ваш объект будет видимым. Однако без него все, что вы увидите — это сплошной черный силуэт. WPF предлагает четыре класса источников света, каждый из которых унаследован от абстрактного класса Light (табл. 23.4). В данном примере мы ограничимся только одним DirectionalLight, который представляет наиболее распространенный тип освещения.
Book_Pro_WPF-2.indb 767
19.05.2008 18:11:25
768
Глава 23
Таблица 23.4. Классы источников света Имя
Описание
DirectionalLight
Заполняет сцену параллельными лучами света, идущими в указанном вами направлении.
AmbientLight
Наполняет сцену рассеянным светом.
PointLight
Свет распространяется во все стороны из точечного источника.
SpotLight
Свет распространяется из одной точки по конусу.
Вот как можно определить белый DirectionalLight: Ось Y
В этом примере вектор, определяющий направление света, начинается в точке начала координат (0,0,0) и продолжается до (-1,-1,-1). Это означает, что каждый луч света представляет собой прямую линию, направленную сверху слева и вниз направо. Это имеет смысл для данного примера, поскольку треугольник повернут под углом к направлению света (как показано на рис. 23.3). Ось X При вычислении направления света важен угол его падения, а не длина вектора. Это значит, что направьZ ление света (-2,-2,-2) эквивалентно нормализованному Ос вектору (-1,-1,-1), поскольку описывает тот же угол. В данном примере направление света не направРис. 23.3. Путь прямого света в лено перпендикулярно к поверхности треугольника. направлении (-1,-1,-1). Если вы хотите добиться такого эффекта, то вектор света следует направить точно вниз по оси Z, используя направление (0,0,-1). Но такое направление было бы несколько искусственным. Поскольку лучи света падают на треугольник под углом, его поверхность будет немного затенена, что создает более реалистичный эффект. На рис. 23.3 показано примерное направление прямого (directional) света (-1,-1,-1), как он падает на треугольник. Напомним, что прямой свет заполняет всю трехмерную сцену. На заметку! Прямой свет иногда сравнивают с солнечным. Это потому, что лучи света, поступающие от далекого источника (такого как солнце), становятся почти параллельными. Все объекты, описывающие свет, непрямо наследуются от GeometryModel3D . Это значит, что вы можете трактовать их как трехмерные объекты, помещая внутрь ModelVisual3D и добавляя в окно просмотра. Рассмотрим пример окна просмотра, включающего как ранее показанный треугольник, так и источник света: ...
Book_Pro_WPF-2.indb 768
19.05.2008 18:11:26
Трехмерная графика
769
Есть одна деталь, которая опущена в этом примере, а именно: данное окно просмотра не включает камеру, определяющую точку зрения на сцену. Этим мы займемся в следующем разделе.
Более подробно о трехмерном освещении Наряду с DirectionalLight, AmbientLight — еще один класс, описывающий освещение. Сам по себе AmbientLight дает плоское представление трехмерных фигур, но вы можете комбинировать его с другим источником света, чтобы добавить некоторую подсветку затененных областей. Секрет заключается в применении AmbientLight неполной интенсивности. Вместо использования белого AmbientLight, применяйте одну треть от белого (установив свойство Color в #555555) или меньше. Вы также можете установить свойство DiffuseMaterial. AmbientColor, чтобы управлять тем, насколько сильно AmbientLight будет влиять на материал в заданной сетке. Использование белого (по умолчанию) дает наиболее сильный эффект, в то время как черный создает впечатление материала, не отражающего никакого рассеянного света. DirectionalLight и AmbientLight — наиболее часто используемые виды освещения для простых трехмерных сцен. PointLight и SpotLight создают нужный эффект, только когда ваша сетка состоит из огромного числа треугольников — обычно, порядка нескольких тысяч. Это связано с тем, как WPF затеняет поверхности. Как вы уже знаете, WPF экономит время на вычислении интенсивности освещения только в вершинах треугольника. Если ваша фигура состоит из небольшого количества треугольников, такое приближение разрушает эффект. Некоторые точки попадут в пределы PointLight и SpotLight, а другие — нет. В результате может получиться, что некоторые треугольники будут освещены, в то время как другие останутся в полной темноте. Вместо получения мягко очерченного круга света на вашем объекте вы получите группу подсвеченных треугольников, в результате граница света окажется “зубчатой”. Проблема в том, что PointLight и SpotLight используются для создания мягких округлых световых эффектов, а для изображения круглой фигуры требуется очень много треугольников. (Чтобы создать идеальный круг, нужен треугольник для каждого пикселя, лежащего на периметре круга.) Если у вас есть сетка, состоящая из сотен или тысяч треугольников, то модель частично освещенных треугольников может быть легче приближена к кругу, и вы получите желаемый световой эффект.
Камера Прежде чем трехмерная сцена будет отображена, вам нужно расположить камеру в корректной позиции и ориентировать ее в правильном направлении. Это делается установкой в свойство Viewport3D.Camera объекта Camera. По сути, камера определяет способ проекции трехмерной сцена на двумерную поверхность окна отображения. WPF включает три класса камер: наиболее часто используемый PerspectiveCamera и более экзотичные OrthographicCamera и MatrixCamera. Камера PerspectiveCamera отображает сцену так, что объекты, находящиеся дальше, всегда выглядят меньшими. Именно такого поведения большинство людей ожидают
Book_Pro_WPF-2.indb 769
19.05.2008 18:11:26
770
Глава 23
от трехмерных сцен. Камера OrthographicCamera выравнивает трехмерные объекты, так что сохраняется точный начальный масштаб, независимо от местоположения каждой фигуры. Это выглядит немного странно, но удобно для некоторых инструментов визуализации. Например, приложения, предназначенные для технического черчения, часто используют именно этот тип представления. (На рис. 23.4 продемонстрирована разница между PerspectiveCamera и OrthographicCamera.) И, наконец, MatrixCamera позволяет определить матрицу, используемую для трансформации данной трехмерной сцены в двумерное представление. Это совершенный инструмент, предназначенный для высокоспециализированных эффектов и для переноса кода из других каркасов (вроде Direct3D), использующих камеру этого типа.
Ортогональная проекция
Перспективная проекция
Рис. 23.4. Отображение перспективы камерами разного типа Выбор правильной камеры — относительно простая задача, но размещение и конфигурирование ее — несколько сложнее. Первое, что нужно специфицировать — точку в трехмерном пространстве, где будет позиционирована камера, установив значение ее свойства Position. Второй шаг — установка трехмерного вектора в свойстве LookDirection, указывающем ориентацию камеры. В типичной трехмерной сцене камера размещается чуть дальше одного из углов сцены через свойство Position, а затем наклоняется для получения требуемого вида с помощью свойства LookDirection. На заметку! Позиция камеры определяет, насколько крупной будет ваша сцена в окне просмотра. Чем ближе камера, тем больше масштаб. В дополнение к этому окно просмотра растягивается, чтобы вместить его контейнер и все его текущее содержимое. Например, вы можете создать представление, заполняющее все окно, затем растягивая или сжимая всю сцену за счет изменения размера окна. Свойства Position и LookDirection нужно устанавливать в сочетании. Если вы используете Position для смещения камеры, но забудете ориентировать ее в правильном направлении с помощью LookDirection, то можете вообще не увидеть содержимого вашей трехмерной сцены. Чтобы правильно ориентировать камеру, укажите точку, которую нужно видеть из вашей камеры. Вы можете вычислить направление взгляда, используя следующую формулу: НаправлениеКамеры = ЦентральнаяТочкаИнтереса - ПозицияКамеры
В примере с треугольником камера помещается в левом верхнем углу с координатами (-2,2,2). Если предположить, что вы хотите навести фокус на центральную точку сцены (0,0,0), которая приходится на середину нижней грани треугольника, то направление взгляда вычисляется следующим образом: НаправлениеКамеры = (0, 0, 0) - (-2, 2, 2) = (2, -2, -2)
Это эквивалентно нормализованному вектору (1,-1,-1), поскольку он описывает то же направление. Как и в случае свойства Direction объекта DirectionalLight, здесь важно направление вектора, а не его длина.
Book_Pro_WPF-2.indb 770
19.05.2008 18:11:26
Трехмерная графика
771
Ось Y
ection UpDir
Ось Y
UpDirection
Установив свойства Position и LookDirection, вы можете также установить свойства UpDirection. Свойство UpDirection определяет наклон камеры. Изначально UpDirection имеет значение (0,1,0), что означает направление прямо вверх, т.е. отсутствие наклона, как показано на рис. 23.5. Если вы немного сместите это направление, скажем, до (0.25,1,0), то камера будет слегка повернута вокруг оси X, как показано на рис. 23.6. В результате трехмерные объекты окажутся немного наклоненными в противоположном направлении. Это как если бы вы, разглядывая сцену, наклонили голову.
Loo
kDi
rec
Loo tion
kDi
rec
tion
Ось X
Ось X
Ос
ьZ
Ос
Рис. 23.5. Позиционирование и наклон камеры
ьZ
Рис. 23.6. Другой способ наклона камеры
Имея в виду все эти детали, вы можете определить PerspectiveCamera для показа простой сцены с одним треугольником, описанной в предыдущих разделах: ...
Финальная сцена показана на рис. 23.7.
Рис. 23.7. Полная трехмерная сцена с одним треугольником
Book_Pro_WPF-2.indb 771
19.05.2008 18:11:26
772
Глава 23
Линии осей На рис. 23.7 присутствует одна новая деталь: линии осей. Эти линии — прекрасное средство тестирования, поскольку наглядно показывают, как расположены ваши оси координат. Если вы осуществляете визуализацию трехмерной сцены, и ничего не появляется, то линии осей могут помочь изолировать потенциальную проблему, которая может быть вызвана неправильным направлением камеры, либо ее неправильным позиционированием, или же тем, что фигура повернута изнанкой (и потому невидима). К сожалению, WPF не включает никакого класса для рисования прямых линий. Вместо этого вам придется отображать длинные узкие треугольники. К счастью, есть инструмент, который может помочь. Команда WPF 3-D создала удобный класс ScreenSpaceLines3D, решающий эту проблему, в доступной свободно загружаемой библиотеке классов (с полным исходным кодом) на http://www.codeplex.com/3DTools. Этот проект включает также несколько других полезных ингредиентов кода, среди которых Trackball, который будет описан далее в этой главе, в разделе “Интерактивность и анимация”. Класс ScreenSpaceLines3D позволяет рисовать прямые линии неизменной толщины. Другими словами, эти линии имеют фиксированную толщину, независимо от местоположения камеры (они не становятся толще с приближением камеры и тоньше — с ее удалением). Это делает такие линии удобными для изображения каркасов, областей пространства, охватывающих области содержимого, векторных линий, указывающих нормали для вычисления освещения, и т.д. Все это особенно полезно при построении инструментов трехмерного дизайна, а также во время отладки приложений. Пример на рис. 23.5 использует класс ScreenSpaceLines3D для рисования линий осей. У камеры есть еще несколько важных свойств. Одно из них — FieldOfView, управляющее размером сцены, которую вы можете увидеть сразу. FieldOfView подобно трансфокатору в камере — по мере уменьшения FieldOfView вы видите все меньший участок сцены (который увеличивается, заполняя собой Viewport3D). При увеличении FieldOfView вы видите все большую часть сцены. Однако важно помнить, что изменение поля зрения — это не то же самое, что перемещение камеры ближе или дальше от объектов вашей сцены. Меньшие поля зрения сокращают расстояние между ближними и дальними объектами, в то время как большие усиливают перспективные отличия между ближними и дальними объектами. (Если вы имели дело с реальными камерами, то должны знать об этом эффекте.) На заметку! Свойство FieldOfView применяется к PerspectiveCamera . Класс OrthographicCamera включает свойство Width, служащее его аналогом. Свойство Width определяет область зрения но не изменяет перспектив, поскольку эффект перспективы в OrthographicCamera не используется. Классы камер также включают свойства NearPlaneDistance и FarPlaneDistance, устанавливающие “мертвые зоны” камеры. Объекты, находящиеся ближе, чем NearPlaneDistance, не появляются вообще, и объекты, расположенные дальше, чем FarPlaneDistance, также невидимы. Обычно NearPlaneDistance по умолчанию установлено в 0.125, а FarPlaneDistance по умолчанию равно Double.PositiveInfinity. Такие значения этих свойств делают этот эффект практически незаметным. Однако в некоторых случаях возникает необходимость изменить эти значения, чтобы предотвратить визуализацию некоторых артефактов. Наиболее часто встречающийся пример — это когда сложная сетка находится слишком близко к камере, что может привести к z-тушению (z-fighting; также известно под названием совмещения (stitching)). В этой ситуации видеокарта не в состоянии корректно определить, какие треугольники находятся ближе к камере и должны быть отображены. В результате смешиваются разные артефакты поверхности вашей сетки.
Book_Pro_WPF-2.indb 772
19.05.2008 18:11:26
Трехмерная графика
773
Z-тушение обычно случается из-за ошибок округления чисел с плавающей точкой в видеокарте. Во избежание этой проблемы, вы можете увеличить NearPlaneDistance, чтобы отсечь объекты, находящиеся чересчур близко к камере. Далее в этой главе мы покажем пример, анимирующий камеру, так что она “пролетит” через центр тора. Чтобы создать этот эффект, избежав z-тушения, необходимо увеличить NearPlaneDistance. На заметку! Появление артефактов почти всегда является результатом того, что объекты находятся слишком близко к камере и слишком большого значения NearPlaneDistance. Подобные проблемы с очень удаленными объектами и FarPlaneDistance случаются гораздо реже.
Дополнительные сведения о 3-D Все эти сложности с камерами, светом, материалами и геометрией сетки представляют собой огромный объем работы для отображения не особо впечатляющего треугольника. Однако теперь вы ознакомились с основами поддержки трехмерной графики в WPF. В этом разделе мы поговорим о том, как использовать ее для построения более сложных фигур. Разобравшись с отображением примитивного треугольника, ваш следующий шаг — создание сложной многогранной фигуры, состоящей из небольшой группы треугольников. В следующем примере мы с вами создадим код разметки для отображения куба, показанного на рис. 23.8.
Рис. 23.8. Трехмерный куб
На заметку! Вы заметите, что стороны куба, представленного на рис. 23.8, имеют мягкие, сглаженные границы. К сожалению, если вы осуществляете визуализацию в среде Windows XP, вы не получите качества такого уровня. Из-за упрощенной поддержки в XP видеодрайверов WPF не пытается выполнить сглаживание граней трехмерных фигур, оставляя их “зазубренными”. Первая задача в построении куба — это определение способа разбиения его на треугольники, которые распознает объект MeshGeometry. Каждый треугольник подобен простой двумерной фигуре.
Book_Pro_WPF-2.indb 773
19.05.2008 18:11:26
774
Глава 23
Рис. 23.9. Разбиение куба на треугольники
Куб состоит из шести квадратных сторон. Каждая квадратная сторона требует для своего отображения двух треугольников. На рис. 23.9 показано, как можно разбить куб на треугольники. Чтобы сократить накладные расходы и повысить производительность в программе, формирующей трехмерные фигуры, принято избегать визуализации тех фигур, которые невидимы. Например, если вы знаете, что никогда не увидите задней стороны куба, показанного на рис. 23.8, то нет причин определять треугольники для этой стороны. Однако в данном примере мы определим все стороны, чтобы можно было свободно вращать куб во все стороны. Так выглядит объект MeshGeometry3D, описывающий куб:
Коллекция Positions определяет углы куба. Она начинается с четырех точек задней стороны (где z = 0), а затем добавляет четыре точки передней стороны (где z = 10). Свойство TriangleIndices отображает эти точки на треугольники. Например, первый элемент в этой коллекции — 0, 2, 1. Он описывает треугольник от первой точки (0,0,0) до второй (0,0,10) и третьей (0,10,0). Это — один из двух треугольников, формирующих заднюю грань куба (индекс 1, 2, 3 описывает второй треугольник задней грани). Напомним, что при определении треугольников вы должны определять их в направлении против часовой стрелки, чтобы их лицевая сторона смотрела вперед. Однако куб нарушает это правило. Квадраты передней стороны определяются в порядке против часовой стрелки (см. индексы 4, 5, 6 и 7, 6, 5), но поверхность задней стороны описана по часовой стрелке, включая индексы 0, 2, 1 и 1, 2, 3. Это объясняется тем, что обратная сторона куба должна обращать свою лицевую сторону назад. Чтобы лучше представить это, предположим, что наш куб будет вращаться вокруг оси Y, так что обратная сторона переместится вперед. Теперь те треугольники, которые смотрели назад, будут повернуты вперед, что сделает их полностью видимыми, и мы получим именно то поведение, которое нужно.
Затенение и нормали Есть еще одна проблема, связанная с сеткой куба, продемонстрированной в предыдущем разделе. Дело в том, что она не создает четко ограненного куба, показанного на рис. 23.8. Вместо этого вы получите куб, показанный на рис. 23.10, с явно видимыми стыками между треугольниками. Эта проблема возникает из-за способа, каким WPF вычисляет освещение. Для того чтобы упростить процесс вычислений WPF находит уровень освещенности каждой вершины фигура; другими словами, внимание уделяется только углам треугольников, а их поверхности заполняются переходными цветами. Хотя это обеспечивает приятную штриховку каждого треугольника, но может стать причиной появления других артефактов. Например, в этой ситуации это мешает равномерному окрашиванию двух треугольников, образующих сторону куба. Чтобы понять, почему так происходит, вам нужно немного узнать о нормалях. Каждая нормаль определяет, как вершина ориентирована относительно источника света. В большинстве случаев необходимо, чтобы нормаль была перпендикулярна поверхности вашего треугольника.
Book_Pro_WPF-2.indb 774
19.05.2008 18:11:27
Трехмерная графика
775
Видимые стыки
Рис. 23.10. Куб с артефактами освещенности На рис. 23.11 эта концепция иллюстрируется на примере передней поверхности куба. Передняя поверхность состоит из двух треугольников и всего четырех вершин. Каждая из этих четырех вершин должна иметь нормаль, направленную под прямым углом к поверхности квадрата. Другими словами, нормаль должна иметь направление (0,0,1). Совет. Нормали можно представлять себе по-другому. Когда вектор нормали направлен противоположно вектору освещения, то поверхность будет освещена полностью. В данном примере это значит, что прямой свет, направленный в (0,0,-1), полностью осветит переднюю грань куба, т.е. вы получите то, чего ожидали. Треугольники на других сторонах куба также должны иметь собственные нормали. В каждом случае нормали должны быть перпендикулярны поверхности. На рис. 23.12 показаны нормали на передней, верхней и правой гранях куба. Куб на рис. 23.12 — это тот же самый куб, что и показанный на рис. 23.8. Когда WPF затеняет куб, то просматривает каждый треугольник по одному.
Рис. 23.11. Нормали передней стороны куба
Book_Pro_WPF-2.indb 775
Рис. 23.12. Нормали на видимых сторонах куба
19.05.2008 18:11:27
776
Глава 23
Например, возьмем переднюю поверхность. Каждая точка встречает направленный свет одинаково. По этой причине каждая точка будет освещена одинаково. В результате, когда WPF распределяет освещенность на четыре угла, то создаст плоскую, равномерно окрашенную поверхность без затенения. Так почему же созданный нами куб ведет себя подобным образом? Виноваты общие точки в коллекции Positions. Хотя нормали определяют затенение треугольников, но определены они только в вершинах треугольника. Каждая точка в коллекции Positions имеет только одну нормаль, определенную для нее. Это означает, что если вы разделяете одни и те же точки между разными треугольниками, то также вы разделяете и их общие нормали. Именно это произошло на рис. 23.10. Разные точки одной и той же стороны освещены по-разному, потому что они имеют разные нормали. А WPF сглаживает освещенность между этими точками, заполняя поверхность каждого треугольника. Это разумное поведение по умолчанию, но поскольку сглаживание выполняется для каждого треугольника, разные треугольники по освещенности не совпадают точно, в результате чего мы наблюдаем “стыки” цвета между ними. Один простой (однако нудный) способ решения этой проблемы заключается в обеспечении того, чтобы ни одна точка не была разделена между разными треугольниками, а для этого объявлена несколько раз (по одному для каждого использования). Вот как будет выглядеть удлиненный код разметки, который обеспечит это:
Все последующие символы автоматически преобразуются в верхний регистр по мере набора их пользователем.
\
Отменяет символ маски, превращая его в литерал. Таким образом, если вы используете \&, это интерпретируется как литеральный символ &, который и будет помещен в текстовое поле.
Все прочие символы
Все прочие символы трактуются как литералы и отображаются в текстовом поле как есть.
24_Pro-WPF2.indd 825
Необязательная десятичная цифра или пробел. Если остается пустым, пробел вставляется автоматически.
Необязательный символ ASCII. Обязательный символ Unicode. Допускает все, что не является управляющим символом, включая знаки препинания и специальные символы. Обязательный буквенно-цифровой символ (допускает буквы и цифры, но не знаки препинания и спец. символы). Десятичный разделитель. Разделитель тысяч. Разделитель времени. Разделитель даты Символ валюты. Все последующие символы автоматически преобразуются в нижний регистр по мере набора их пользователем (Нет способа вернуться обратно в режим смешанного ввода после использования этого символа.)
20.05.2008 17:04:21
826
Глава 24
MaskedTextProvider “За кулисами” MaskedTextBox, предоставленный Windows Forms, полагается на другой компонент — System.ComponentModel.MaskedTextProvider. Хотя элемент управления System.Windows.Forms.MaskedTextBox специфичен для Windows Forms, тем не менее, MaskedTextProvider может быть использован для реализации маскируемого редактирования в любой технологии отображения, если только вы можете перехватывать нажатия клавиш до того, как они попадают в редактирующий элемент управления. Для создания собственного маскируемого элемента управления необходимо выполнить перечисленные ниже шаги.
• Создать элемент управления, который поддерживает внутри себя экземпляр MaskedTextProvider. Элемент MaskedTextProvider обладает состоянием — он отслеживает текст, который введен пользователем в маску до каждого данного момента.
• Всякий раз, когда пользовательский элемент управления принимает нажатие клавиши, вам нужно определить действие, которое она пытается выполнить, и передать ее MaskedTextProvider через такие методы, как Add(), Insert(), Remove() и Replace(). Элемент MaskedTextProvider будет автоматически игнорировать недопустимые символы.
• После того, как вы отправляете изменение MaskedTextProvider, необходимо вызвать MaskedTextProvider.ToDisplayString(), чтобы получить самый последний текст. Затем вы можете обновить пользовательский элемент управления. В идеале вы должны обновлять только те символы, которые изменились, хотя часто это не получается, если вы осуществляете наследование от других элементов управления — в этом случае может понадобиться заменять весь текст за одну операцию, что может вызвать мерцание. Сложность применения MaskedTextProvider заключается в необходимости отслеживания всех низкоуровневых деталей, таких как текущее положение пользователя в строке ввода.
Реализация маскируемого текстового поля WPF Чтобы создать наиболее устойчивый текстовый элемент управления в WPF, вам нужно наследовать его от низкоуровневого класса System.Windows.Controls.Primitives. TextBoxBase (от которого наследованы TextBox и PasswordBox). Однако вы можете создать относительно неплохой маскируемый редактирующий элемент управления, потратив намного меньше усилий, если унаследуете его непосредственно от TextBox, как в приведенном ниже примере. MaskedTextBox начинается с объявления важнейшего свойства Mask. Это свойство зависимостей хранит строку, использующую описанный выше синтаксис маски. Свойство Mask подключается к обратному вызову изменения свойства, сбрасывающего текст в элементе управления при изменении маски. public class MaskedTextBox : System.Windows.Controls.TextBox { public static DependencyProperty MaskProperty; static MaskedTextBox() { MaskProperty = DependencyProperty.Register("Mask", typeof(string), typeof(MaskedTextBox), new FrameworkPropertyMetadata(MaskChanged)); }
Book_Pro_WPF-2.indb 826
19.05.2008 18:11:35
Пользовательские элементы
827
public string Mask { get { return (string)GetValue(MaskProperty); } set { SetValue(MaskProperty, value); } } ... }
Следующий шаг состоит в добавлении двух важных приватных методов. Первый —
GetMaskProvider() — создает MaskedTextProvider, используя текущую маску и затем применяя текст из элемента управления: private MaskedTextProvider GetMaskProvider() { MaskedTextProvider maskProvider = new MaskedTextProvider(Mask); maskProvider.Set(Text); return maskProvider; }
Второй — RefreshText() — получает наиболее свежий текст из MaskedTextProvider, отображает его в текущем элементе управления и сбрасывает курсор в правильную позицию: private void RefreshText(MaskedTextProvider maskProvider, int pos) { // Обновление строки. this.Text = maskProvider.ToDisplayString(); // Позиционирование курсора. this.SelectionStart = pos; }
Когда это сделано, вы готовы начать работу с маскируемым текстом. Например, легко добавить доступное только для чтения свойство, которое вычисляет текущую маску и текст, и определяет, полностью ли заполнена маска, используя свойство MaskedTextProvider.MaskCompleted: public bool MaskCompleted { get { MaskedTextProvider maskProvider = GetMaskProvider(); return maskProvider.MaskCompleted; } }
Столь же легко написать обратный вызов для изменения свойства, который обновляет текст при изменении маски: private static void MaskChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { MaskedTextBox textBox = (MaskedTextBox)d; MaskedTextProvider maskProvider = textBox.GetMaskProvider(); textBox.RefreshText(maskProvider, 0); }
Прежде чем двигаться дальше, вы можете облегчить себе жизнь, закодировав еще одну полезную приватную функцию. Это метод по имени SkipToEditableCharacter(), который возвращает позицию редактирования, куда должен быть установлен курсор. Вам нужно вызывать его в разные моменты при перемещении пользователя по маске, чтобы пропускать символы маски. MaskedTextProvider.FindEditPositionFrom() вы-
Book_Pro_WPF-2.indb 827
19.05.2008 18:11:35
828
Глава 24
полняет тяжелую работу, находя следующую разрешенную точку для вставки, расположенную справа от текущей позиции курсора. private int SkipToEditableCharacter(int startPos) { MaskedTextProvider maskProvider = GetMaskProvider(); int newPos = maskProvider.FindEditPositionFrom(startPos, true); if (newPos == -1) { // Уже в конце строки. return startPos; } else { return newPos; } }
Как вы знаете из главы 6, обработка нажатий клавиш в TextBox — дело довольно непростое. Чтобы получать все события клавиш, которые вам нужны, приходится обрабатывать два события: PreviewKeyDown и PreviewTextInput. Вместо присоединения обработчиков к этим событиям, вы можете переопределить соответствующий метод OnСобытие(). Совет. Нет никакой гарантии, что данное событие имеет соответствующий метод OnСобытие(), который вы можете переопределить. Однако существует соглашение, которому следуют многие разработчики элементов управления, и его придерживаются во всех элементах WPF. Вы можете использовать OnPreviewTextInput для реагирования на обычные символы и нажатия клавиши . Однако при вставке символа вам следует особо позаботиться о том, чтобы определить, включен ли на клавиатуре режим вставки (а не замены). Обратите внимание, что код устанавливает свойство e.Handled в true, так что клавиша не может быть обработана далее никакими обработчиками событий. protected override void OnPreviewTextInput(TextCompositionEventArgs e) { MaskedTextProvider maskProvider = GetMaskProvider(); int pos = this.SelectionStart; // Добавление символа. if (pos < this.Text.Length) { pos = SkipToEditableCharacter(pos); // Включен режим замены. if (Keyboard.IsKeyToggled(Key.Insert)) { if (maskProvider.Replace(e.Text, pos)) { pos++; } } // Включен режим вставки. else { if (maskProvider.InsertAt(e.Text, pos)) { pos++; } }
Book_Pro_WPF-2.indb 828
19.05.2008 18:11:35
Пользовательские элементы
829
// Поиск новой позиции курсора. pos = SkipToEditableCharacter(pos); } RefreshText(maskProvider, pos); e.Handled = true; base.OnPreviewTextInput(e); }
Метод OnPreviewKeyDown() позволяет вам обработать специальные расширенные клавиши вроде : protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnKeyDown(e); MaskedTextProvider maskProvider = GetMaskProvider(); int pos = this.SelectionStart; // Удаление символа (клавишей ). // Не делает ничего, если вы пытаетесь удалить символ формата. if (e.Key == Key.Delete && pos < (this.Text.Length)) { if (maskProvider.RemoveAt(pos)) { RefreshText(maskProvider, pos); } e.Handled = true; } // Удаление символа (клавишей ). // Переступает через символ формата, но не удаляет следующий символ. else if (e.Key == Key.Back) { if (pos > 0) { pos--; if (maskProvider.RemoveAt(pos)) { RefreshText(maskProvider, pos); } } e.Handled = true; } }
На рис. 24.4 показан MaskedTextBox в действии. Редактирование в MaskedTextBox вполне интуитивно понятно. Пользователь может перемещаться в любую позицию текстового поля и удалять или вставлять символы (в последнем случае существующие символы сдвигаются направо или налево, если они допустимы в их новых позициях). Необязательные символы могут быть проигнорированы (пользователь может просто пропустить их, используя клавиши со стрелками) либо на их месте можно вставить пробел.
Book_Pro_WPF-2.indb 829
Рис. 24.4. Ввод данных в маскируемом текстовом поле
19.05.2008 18:11:35
830
Глава 24
Усовершенствование MaskedTextBox Этот длинный код пока еще не обеспечивает всей необходимой функциональности. В таком виде маскируемое текстовое поле демонстрирует не совсем правильное поведение, когда вы вырезаете или вставляете текст из буфера обмена. Оба эти действия могут разрушить маску, и она не будет восстановлена до следующего нажатия клавиши. Аналогично, программная установка свойства Text представляет собой другой способ ввести значения, которые не допускаются маской. Решение этих проблем требует несколько запутанных обходных маневров. В идеале вам стоило бы создать маскируемое текстовое поле, наследуя его от TextBoxBase и реализуя значительный объем функциональности самостоятельно. Однако даже при существующем дизайне вполне можно преодолеть возникающие проблемы. Простейший способ решения проблемы, связанной с вырезкой и вставкой — вообще отключить эти средства. Как вы знаете из главы 10, этого можно добиться, добавив новую привязку команд, которая переопределит привязку команд класса и пометит команду как выполненную. Для этого потребуется следующий код: public MaskedTextBox() : base() { CommandBinding commandBinding1 = new CommandBinding( ApplicationCommands.Paste, null, SuppressCommand); this.CommandBindings.Add(commandBinding1); CommandBinding commandBinding2 = new CommandBinding( ApplicationCommands.Cut, null, SuppressCommand); this.CommandBindings.Add(commandBinding2); } private void SuppressCommand(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = false; e.Handled = true; }
Вы можете перекрыть “черный ход”, открытый свойством Text, несколькими способами. Очевидный подход — использовать средство свойств зависимостей, такое как обратный вызов проверки достоверности. (По правде говоря, принудительное приведение свойства имеет больше смысла, поскольку вы можете разрешить установку свойства Text перед установкой свойства Mask, в то время, когда еще действует предыдущее значение маски.) Однако возникает проблема — свойство Text определено в базовом классе, так что у вас нет никаких шансов зарегистрировать его и установить соответствующие метаданные. К счастью, есть одно решение. Вы можете вызвать свойство OverrideMetadata на TextProperty для применения новых метаданных, которые будут применены исключительно к MaskedTextBox. Такая техника концептуально представляет собой то же самое, что и техника, используемая для переопределения свойства DefaultStyleKey при специфицировании стиля по умолчанию для элемента, управляемого шаблонами. Чтобы использовать эту технику, вам нужно добавить следующий код в статический конструктор: FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.CoerceValueCallback = CoerceText; TextProperty.OverrideMetadata(typeof(MaskedTextBox), metadata);
Затем вы сможете использовать следующий метод обратного вызова для принуждения свойства Text:
Book_Pro_WPF-2.indb 830
19.05.2008 18:11:35
Пользовательские элементы
831
private static object CoerceText(DependencyObject d, object value) { MaskedTextBox textBox = (MaskedTextBox)d; MaskedTextProvider maskProvider = new MaskedTextProvider(textBox.Mask); maskProvider.Set((string)value); return maskProvider.ToDisplayString(); }
Метод MaskedTextProvider.Set() автоматически отклоняет ввод, если в нем присутствуют символы, противоречащие маске. Однако заполнители не являются обязательными, так что оба следующих присваивания эквивалентны: maskedTextBox.Text = "(123) 456-7890"; maskedTextBox.Text = "1234567890";
И, наконец, чтобы убедиться, что свойство Text, интерпретированное свойством Mask, изменено, ваш обратный вызов MaskChanged() должен инициировать принудительное приведение свойства Text, как показано здесь. Этого также достаточно, чтобы обновить отображаемый в элементе управления текст. private static void MaskChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { MaskedTextBox textBox = (MaskedTextBox)d; d.CoerceValue(TextProperty); }
Пользовательские панели Одним из распространенных типов пользовательских элементов является пользовательская панель. Как вы знаете из главы 4, панели размещают в себе один или более дочерних элементов и реализуют специфическую логику компоновки для соответствующего их расположения. Пользовательские панели — важный ингредиент, который нужен, когда вы хотите построить собственную систему “отрывных” инструментальных панелей и стыкуемых окон. Пользовательские панели часто удобны для создания составных элементов управления, которым нужна специфическая нестандартная компоновка. Например, вы можете создать пользовательскую панель как часть “ленты” в стиле Office 2007, которая динамически реорганизует и переупорядочивает свои кнопки по мере изменения объема свободного пространства. Вы уже знакомы с базовыми типами панелей, которые WPF предлагает для организации содержимого (StackPanel, DockPanel, WrapPanel, Canvas и Grid). Также вы видели, что некоторые элементы WPF используют свои собственные специализированные панели (вроде TabPanel, ToolBarOverflowPanel и VirtualizingPanel). Вы можете найти намного больше примеров пользовательских панелей в Internet. Ниже перечислены некоторые их них, достойные внимания.
• Панель RadialPanel, организующая элементы в циклической манере вокруг центральной точки (в справочной системе .NET 3.0 SDK).
• Специальный Canvas, позволяющий перетаскивать свои дочерние элементы без дополнительного кода обработки событий (http://www.codeproject.com/WPF/ DraggingElementsInCanvas.asp).
• Две панели, реализующие забавные эффекты в списке элементов (http:// www.codeproject.com/WPF/Panels.asp).
• Панель, использующая анимацию на основе кадров для трансформации одной компоновки в другую (http://wpf.netfx3.com/files/folders/controls/
entry8196.aspx).
Book_Pro_WPF-2.indb 831
19.05.2008 18:11:35
832
Глава 24
Из следующих разделов вы узнаете, как создается пользовательская панель, и мы рассмотрим два простых примера — базовый клон Canvas и расширенную версию WrapPanel.
Двухшаговый процесс компоновки Каждая панель использует один и тот же прием: двухшаговый процесс, отвечающий за изменение размеров и упорядочивание дочерних элементов. Первая стадия — измерение, когда панель определяет, насколько большими хотят быть его дочерние компоненты. Вторая стадия — компоновка, когда каждый элемент получает свои границы. Необходимы два шага, поскольку панели нужно учесть “пожелания” всех ее членов перед тем, как решить, как следует распорядиться доступным пространством. Вы добавляете логику этих двух шагов, переопределяя методы со странными именами MeasureOverride() и ArrangeOverride(), которые определены в классе FrameworkElement как часть системы компоновки WPF. Их странные имена говорят о том, что методы MeasureOverride() и ArrangeOverride() заменяют логику, содержащуюся в методах MeasureCore() и ArrangeCore(), определенных в классе UIElement. Эти методы не переопределяемы.
MeasureOverride() Первый шаг состоит в определении с помощью метода MeasureOverride() того, сколько пространства желает занять каждый дочерний элемент. Однако даже в методе MeasureOverride() дочерние элементы не получают неограниченного пространства. В качестве абсолютного минимума дочерние элементы ограничены пространством, доступном в панели. Дополнительно вы можете ограничить их более строго. Например, Grid с двумя пропорционально размещенными строками представит каждому из дочерних элементов половину доступной высоты. StackPanel выделит все доступное пространство первому элементу, затем представит то, что осталось, второму, и т.д. Каждая реализация MeasureOverride() отвечает за проход циклом по коллекции дочерних элементов и вызов метода Measure() для каждого из них. Когда вы вызываете метод Measure(), то применяете ограничивающую рамку — объект Size, определяющий максимально доступное пространство для дочернего элемента управления. К концу метода MeasureOverride() панель возвращает пространство, необходимое для отображения всех ее дочерних элементов и их желательные размеры. Ниже приведена базовая структура метода MeasureOverride(), без специфических деталей, связанных с размерами: protected override Size MeasureOverride(Size constraint) { // Проверить все дочерние элементы. foreach (UIElement element in base.InternalChildren) { // Запросить у каждого дочернего элемента желательное для него // пространство, применяя ограничение availableSize. Size availableSize = new Size(...); element.Measure(availableSize); // (Здесь можно прочесть element.DesiredSize, // чтобы получить запрошенный размер.) } // Показать, сколько места требует данная панель. // Будет использовано для установки свойства DesiredSize панели. return new Size(...); }
Book_Pro_WPF-2.indb 832
19.05.2008 18:11:36
Пользовательские элементы
833
Метод Measure() не возвращает значения. После вызова Measure() свойство DesiredSize данного элемента содержит запрошенный размер. Вы можете использовать эту информацию в своих вычислениях для будущих дочерних элементов (и определения общего размера, необходимого панели). Вы должны вызвать Measure() для каждого дочернего элемента, даже если не хотите ограничивать размер этого элемента или использовать его свойство DesiredSize. Многие элементы не отображают себя до тех пор, пока не будет вызван их метод Measure(). Если вы хотите предоставить дочернему элементу все пространство, которое он пожелает, передайте объект Size со значением Double.PositiveInfinity по обоим измерениям. (Такую стратегию использует ScrollViewer, поскольку он может обработать содержимое любого размера.) Дочерний элемент затем вернет размер пространства, необходимого его содержимому, или доступное пространство — в зависимости от того, что меньше. В конце процесса измерения контейнер компоновки должен вернуть желаемый размер. В простой панели вы можете вычислять желаемый размер панели, комбинируя желаемые размеры каждого дочернего элемента. На заметку! Вы можете просто вернуть ограничение, переданное методу MeasureOverride(), в качестве желаемого размера вашей панели. Хотя кажется разумным взять весь доступный размер, это приводит к проблемам, если контейнер принимает объект Size со значением Double.PositiveInfinity хотя бы по одному из двух измерений (что означает “возьми столько места, сколько хочешь”). Хотя бесконечный размер допустим в качестве ограничения, он не допустим в качестве результирующего значения, поскольку WPF не сможет определить, насколько большим должен быть ваш элемент. Более того, вы не должны запрашивать больше пространства, чем вам нужно на самом деле. В противном случае это приведет к появлению лишнего пустого пространства и элементы, добавленные позже, после панели компоновки, будут “толпиться” внизу окна. Внимательный читатель может отметить, что существует близкое сходство между методом Measure(), вызываемым с каждым элементом, и методом MeasureOverride(), определяющим первый шаг логики компоновки панели. Фактически метод Measure() инициирует метод MeasureOverride(). То есть, если вы поместите один контейнер компоновки внутри другого, то при вызове Measure() получите общий размер, необходимый контейнеру компоновки и всем его дочерним элементам. Совет. Одной из причин того, что процесс замера проходит в два шага (метод Measure(), инициирующий метод MeasureOverride()), является необходимость иметь дело с двумя полями. Когда вызывается Measure(), вы передаете все доступное пространство. Когда WPF вызывает MeasureOverride(), он автоматически сокращает доступное пространство, чтобы принять во внимание размер полей (если только вы не передадите бесконечный размер).
ArrangeOverride() Как только каждый элемент замерен, наступает время разместить их в пределах доступного пространства. Система компоновки вызывает метод ArrangeOverride() вашей панели, и панель вызывает метод Arrange() для каждого дочернего элемента, чтобы сообщить ему, сколько пространства ему выделено. (Как вы можете предположить, Arrange() так же инициирует метод ArrangeOverride(), как Measure() инициирует метод MeasureOverride().) При измерении элементов с помощью метода Measure() вы передаете объект Size, задающий границы доступного пространства. При размещении элемента методом Arrange() вы передаете объект System.Windows.Rect, определяющий размер и поло-
Book_Pro_WPF-2.indb 833
19.05.2008 18:11:36
834
Глава 24
жение элемента. В данный момент это похоже на то, как элемент располагается по координатам X и Y стиля Canvas, определяющим расстояние между верхним левым углом вашего контейнера компоновки и элементов. На заметку! Элементы (и панели компоновки) вольны нарушать правила и пытаться рисовать за пределами выделенных им границ. Например, в главе 13 вы видели, как Line может перекрыть соседние элементы. Однако обычные элементы должны соблюдать выделенные им границы. Вдобавок большинство контейнеров будут усекать те дочерние элементы, которые выходят за их границы. Ниже приведена базовая структура метода ArrangeOverride() без специфических деталей, связанных с вычислением размеров. protected override Size ArrangeOverride(Size arrangeSize) { // Перебрать все дочерние элементы. foreach (UIElement element in base.InternalChildren) { // Присвоить дочернему элементу его границы. Rect bounds = new Rect(...); element.Arrange(bounds); // (Теперь вы можете прочитать element.ActualHeight и // element.ActualWidth, чтобы определить его размеры.) } // Определить, сколько места займет эта панель. // Эта информация будет использована для установки свойств // ActualHeight и ActualWidth панели. return arrangeSize; }
При упорядочивании элементов вы не можете передавать бесконечные размеры. Однако вы можете дать элементу его желаемый размер, передав значение свойства DesiredSize. Вы можете также дать элементу больше пространства, чем он требует. Фактически это случается довольно часто. Например, вертикальная StackPanel предоставляет своим дочерним элементам столько высоты, сколько они требуют, но при этом выделяют им всю ширину самой панели. Аналогично Grid может использовать фиксированный или пропорциональный размер строк, который больше, чем желаемый размер находящегося внутри элемента. И даже если вы расположите элемент в контейнере “размер по содержимому” (size-to-content), этот элемент может быть увеличен, если ему будет явно установлен размер через свойства Height и Width. Когда элемент делается больше, чем его желаемый размер, то вступают в действие свойства HorizontalAlignment и VerticalAlignment. Содержимое элемента помещается где-то внутри отведенного ему пространства и его надо как-то выравнивать. Поскольку метод ArrangeOverride() всегда принимает определенный размер (не бесконечный), вы можете вернуть переданный объект Size, чтобы установить финальный размер вашей панели. Фактически многие контейнеры компоновки предпринимают этот шаг, чтобы занять все выделенное им пространство. (Здесь отсутствует опасность захватить слишком много места, которое может понадобиться другому элементу, поскольку шаг замера системы компоновки гарантирует, что не будет выдано больше места, чем необходимо, если его не хватает.)
Клон Canvas Самый быстрый способ понять работу этих двух методов — рассмотреть внутреннее устройство класса Canvas, который является простейшим контейнером компонов-
Book_Pro_WPF-2.indb 834
19.05.2008 18:11:36
Пользовательские элементы
835
ки. Чтобы создать собственную панель в стиле Canvas, вам нужно просто унаследовать класс от Panel и добавить методы MeasureOverride() и ArrangeOverride(), показанные ниже: public class CanvasClone : System.Windows.Controls.Panel { ... }
Canvas помещает дочерние элементы там, где они хотят разместиться, и выделяет им столько места, сколько им нужно. В результате нет необходимости вычислять, сколько доступного пространства следует выделить. Это делает метод MeasureOverride() чрезвычайно простым. Каждому дочернему элементу выделяется бесконечное пространство: protected override Size MeasureOverride(Size constraint) { Size size = new Size(double.PositiveInfinity, double.PositiveInfinity); foreach (UIElement element in base.InternalChildren) { element.Measure(size); } return new Size(); }
Обратите внимание, что MeasureOverride() возвращает пустой объект Size(), а это означает, что Canvas вообще не требует никакого пространства. На вас ложится задача специфицировать явно размер Canvas или поместить его в контейнер компоновки, который растянется для того, чтоб заполнить все доступное пространство. Метод ArrangeOverride() лишь не намного сложнее. Чтобы определить правильное местоположение каждого элемента, Canvas использует прикрепленные свойства (Left, Right, Top и Bottom). Как вы знаете из главы 6 (и как увидите далее на примере WrapBreakPanel), прикрепленные свойства реализованы двумя вспомогательными методами в определении класса: GetProperty() и SetProperty(). Рассматриваемый нами клон Canvas немного проще. Он имеет только два прикрепленных свойства — Left и Top (без излишних Right и Bottom). Ниже приведен код, используемый для размещения элементов. protected override Size ArrangeOverride(Size arrangeSize) { foreach (UIElement element in base.InternalChildren) { double x = 0; double y = 0; double left = Canvas.GetLeft(element); if (!DoubleUtil.IsNaN(left)) { x = left; } double top = Canvas.GetTop(element); if (!DoubleUtil.IsNaN(top)) { y = top; } element.Arrange(new Rect(new Point(x, y), element.DesiredSize)); } return arrangeSize; }
Book_Pro_WPF-2.indb 835
19.05.2008 18:11:36
836
Глава 24
Улучшенная WrapPanel Теперь, когда вы достаточно подробно изучили систему панелей, стоит попробовать создать собственный контейнер компоновки, который добавит кое-что, не предусмотренное в базовых панелях WPF. В этом разделе будет представлен пример, расширяющий возможности WrapPanel. WrapPanel выполняет простую функцию, которая оказывается весьма полезной. Она раскладывает свои дочерние элементы один за другим, переходя на следующую строку по заполнению текущей. Windows Forms включает подобный инструмент компоновки, называемый FlowLayoutPanel. В отличие от WrapPanel, FlowLayoutPanel имеет одну дополнительную способность — прикрепленное свойство, которое могут использовать дочерние элементы для принудительного немедленного перевода строки. (Технически это не было прикрепленным свойством, а свойством, добавленным поставщиком расширений, но эти две концепции являются аналогами.) Хотя WrapPanel не обеспечивает такой возможности, добавить ее нетрудно. Все, что для этого понадобится — это пользовательская панель, добавляющая необходимое прикрепленное свойство. В следующем листинге показана WrapBreakPanel с добавленным прикрепленным свойством LineBreakBeforeProperty. Установленное в true, это свойство вставляет немедленный перенос строки перед элементом. public class WrapBreakPanel : Panel { public static DependencyProperty LineBreakBeforeProperty; static WrapBreakPanel() { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.AffectsArrange = true; metadata.AffectsMeasure = true; LineBreakBeforeProperty = DependencyProperty.RegisterAttached( "LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), metadata); } ... }
Как любое свойство зависимостей, LineBreakBefore определяется как статическое поле и регистрируется в статическом конструкторе класса. Единственное отличие в том, что вы используете метод RegisterAttached() вместо Register(). Объект FrameworkPropertyMetadata для свойства LineBreakBefore специально указывает на то, что оно затрагивает процесс компоновки. В результате при каждой установке этого свойства будет инициирован новый проход компоновки. Прикрепленные свойства не помещаются в нормальные оболочки свойств, поскольку они не устанавливаются в классе, определяющем их. Вместо этого вы должны предоставить два статических метода, которые смогут использовать метод DependencyObject. SetValue() для установки этого свойства в любой произвольный элемент. Код, необходимый для свойства LineBreakBefore, выглядит так: public static void SetLineBreakBefore(UIElement element, Boolean value) { element.SetValue(LineBreakBeforeProperty, value); } public static Boolean GetLineBreakBefore(UIElement element) { return (bool)element.GetValue(LineBreakBeforeProperty); }
Book_Pro_WPF-2.indb 836
19.05.2008 18:11:36
837
Пользовательские элементы
Единственное, что остается — принимать во внимание это свойство при выполнении логики компоновки. Логика компоновки WrapBreakPanel основана на WrapPanel. Во время стадии измерения элементы располагаются по строкам, так что при необходимости панель может вычислить размер общего пространства. Каждый элемент добавляется в текущую строку, если только он не слишком велик, чтобы уместиться в ней, и не установлено свойство LineBreakBefore. Вот полный код: protected override Size MeasureOverride(Size constraint) { Size currentLineSize = new Size(); Size panelSize = new Size(); foreach (UIElement element in base.InternalChildren) { element.Measure(constraint); Size desiredSize = element.DesiredSize; if (GetLineBreakBefore(element) || currentLineSize.Width + desiredSize.Width > constraint.Width) { // Перейти на новую строку (либо потому, что элемент требует, // либо потому, что закончилось место в текущей строке). panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width); panelSize.Height += currentLineSize.Height; currentLineSize = desiredSize; // Если элемент слишком широк, чтобы поместиться в ширину строки, // просто выделить ему отдельную строку. if (desiredSize.Width > constraint.Width) { panelSize.Width = Math.Max(desiredSize.Width, panelSize.Width); panelSize.Height += desiredSize.Height; currentLineSize = new Size(); } } else { // Продолжать добавление в текущую строку. currentLineSize.Width += desiredSize.Width; // Установить высоту строки по высоте ее максимального элемента. currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height); } } // Возвратить размер, необходимый для размещения всех элементов. // Обычно это будет ширина ограничения, а высота // базируется на размерах элементов. // Однако если элемент шире, чем ширина панели, // то желаемая ширина будет шириной строки. panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width); panelSize.Height += currentLineSize.Height; return panelSize; }
Ключевая деталь приведенного кода — проверка свойства LineBreakBefore. Это реализует дополнительную логику, которая не обеспечивается обычной WrapPanel. Код ArrangeOverride() почти такой же, но лишь немного более утомительный. Отличие в том, что панели требуется определить максимальную высоту строки (которая определяется по самому высокому элементу), прежде чем начать компоновку строки. Таким образом, каждый элемент получает полный объем доступного пространства,
Book_Pro_WPF-2.indb 837
19.05.2008 18:11:36
838
Глава 24
принимая во внимание полную высоту строки. Этот тот же процесс, что используется в компоновке обычной WrapPanel. Чтобы увидеть все подробности, обратитесь к загружаемому коду для данной главы. Использовать WrapBreakPanel просто. Ниже приведен пример кода компоновки, демонстрирующего, что WrapBreakPanel корректно разделяет строки и вычисляет правильный желаемый размер на базе размеров дочерних элементов. Content above the WrapBreakPanel. No Break Here No Break Here No Break Here No Break Here Button with Break No Break Here No Break Here No Break Here No Break Here Content below the WrapBreakPanel.
На рис. 24.5 можно видеть результат.
Рис. 24.5. WrapBreakPanel в действии
Book_Pro_WPF-2.indb 838
19.05.2008 18:11:36
Пользовательские элементы
839
Рисованные элементы В предыдущем разделе мы начали раскрывать внутреннюю “кухню” элементов WPF — а именно: методы MeasureOverride() и ArrangeOverride(), позволяющие включить любой элемент в систему компоновки WPF. В этом разделе мы погрузимся еще глубже и рассмотрим, как элементы отображают сами себя. Большинство элементов WPF используют композицию для создания своего внешнего представления. Другими словами, типичный элемент строит себя из других, более основополагающих элементов. На протяжении этой главы вы уже видели, как работает эта модель. Например, вы определяете составные элементы пользовательского элемента управления с помощью кода разметки, который обрабатывается таким же образом, как XAML пользовательского окна. Вы определяете визуальное дерево пользовательского элемента управления с применением управляющего шаблона. А когда вы создаете пользовательскую панель, то вообще не нуждаетесь в определении каких-либо визуальных деталей. Составные элементы предоставляются потребителем и добавляются в коллекцию Children. Такой акцент отличается от того, что вы видели в предыдущих технологиях пользовательских интерфейсов, таких как Windows Forms. В Windows Forms некоторые элементы управления рисуют себя, используя библиотеку User32, являющуюся частью Windows API, но наиболее специализированные элементы полагаются на классы рисования GDI+ для отображения себя “с нуля”. Поскольку Windows Forms не предоставляет высокоуровневых графических примитивов, которые можно было бы добавить непосредственно к пользовательскому интерфейсу (подобно прямоугольникам, эллипсам и путям WPF), всякий элемент управления, который нуждается в нестандартном визуальном представлении, требует специального кода отображения. Конечно, только композиция может завести вас так далеко. В конечном итоге, некоторые классы должны взять на себя ответственность за рисование содержимого. В WPF этот момент находится глубоко в дереве элементов. В типичном окне отображение состоит из индивидуальных фрагментов текста, фигур и битовых карт, а не высокоуровневых элементов.
Метод OnRender() Чтобы выполнить специальное отображение, элемент должен переопределить метод OnRender(), унаследованный от базового класса UIElement. Метод OnRender() не обязательно заменяет композицию. Некоторые элементы управления применяют OnRender() для рисования визуальных деталей и используют композицию для компоновки прочих элементов на своей поверхности. Примером могут служить класс Border, который рисует свою рамку в методе OnRender(), и класс Panel, рисующий свой фон в методе OnRender(). Как Border, так и Panel поддерживают дочернее содержимое, и это содержимое отображается поверх специально отображаемых деталей. Метод OnRender() принимает объект DrawingContext, который предлагает набор полезных методов рисования содержимого. Впервые этот класс был упомянут в главе 14, когда мы использовали его для рисования содержимого объекта Visual. Ключевое отличие рисования в методе OnRender() состоит в том, что вы не создаете явно и не закрываете DrawingContext. Это связано с тем, что несколько разных методов OnRender() могут совместно использовать один и тот же DrawingContext. Например, элемент-наследник может выполнять некоторое специальное отображение и вызывать реализацию OnRender() в базовом классе для рисования дополнительного содержимого. Это работает, потому что WPF автоматически создает объект DrawingContext в начале этого процесса и закрывает его, когда он больше не нужен.
Book_Pro_WPF-2.indb 839
19.05.2008 18:11:36
840
Глава 24
На заметку! Технически метод OnRender() в действительности не рисует ваше содержимое на экране. Вместо этого он рисует его в объекте DrawingContext, а WPF затем кэширует эту информацию. WPF определяет, когда ваш элемент нуждается в перерисовке, и тогда выводит то, что вы создали в DrawingContext. В этом и состоит сущность графической системы WPF: вы определяете содержимое, а WPF управляет его рисованием и обновлением незаметно для вас. Наиболее удивительная подробность механизма отображения WPF заключается в том, что совсем немного классов в действительности выполняют его. Большинство классов построено на основе других, более простых классов, и нужно “нырнуть” достаточно глубоко в дерево элементов, чтобы найти класс, который действительно переопределяет метод OnRender(). Ниже описаны некоторые из таких классов.
• Класс TextBlock . Всякий раз, когда вы рисуете текст, применяется объект TextBlock, использующий свой метод OnRender() для его отображения.
• Класс Image. Класс Image переопределяет метод OnRender() для рисования содержимого картинки, используя метод DrawingContext.DrawImage().
• Класс MediaElement. MediaElement переопределяет OnRender() для рисования видеофрейма, если он используется для воспроизведения видеофайла.
• Классы фигур. Базовый класс Shape переопределяет OnRender() для рисования своего внутреннего объекта Geometry с помощью метода DrawingContext. DrawGeometry(). Этот объект Geometry может представлять эллипс, прямоугольник или более сложные пути, состоящие из отрезков прямых и кривых линий, в зависимости от специфичного класса-наследника Shape. Многие элементы используют фигуры для рисования небольших видеодеталей.
• “Хромированные” классы. Такие классы, как ButtonChrome и ListBoxChrome, рисуют внешнее представление общего элемента управления и помещают специфицированное вами содержимое внутрь. Многие другие классы-наследники Decorator наподобие Border также переопределяют OnRender().
• Классы панелей. Хотя содержимое панелей представлено ее дочерними элементами, метод OnRender() заботится о рисовании прямоугольника с цветом фона, если установлено свойство Background. Часто реализация OnRender() бывает обманчиво простой. Например, вот как выглядит код отображения любого класса-наследника Shape: protected override void OnRender(DrawingContext drawingContext) { this.EnsureRenderedGeometry(); if (this._renderedGeometry != Geometry.Empty) { drawingContext.DrawGeometry(this.Fill, this.GetPen(), this._renderedGeometry); } }
Напомним, что переопределение OnRender() — не единственный способ отобразить содержимое и добавить его в ваш пользовательский интерфейс. Вы также можете создать объект DrawingVisual и добавить его к UIElement с помощью метода AddVisualChild() (а также реализовать несколько других деталей, как было описано в главе 14). Вы можете затем вызвать DrawingVisual.RenderOpen(), чтобы извлечь DrawingContext и использовать его для рисования содержимого. Некоторые элементы применяют эту стратегию в WPF для отображения ряда графических деталей поверх остального содержимого элемента. Например, вы можете видеть
Book_Pro_WPF-2.indb 840
19.05.2008 18:11:37
Пользовательские элементы
841
это на примере индикатора перетаскивания, индикаторов ошибок и рамок фокуса. Во всех этих случаях подход на основе DrawingVisual позволяет элементу рисовать поверх другого содержимого, а не под ним. Но все-таки в основном все отображение происходит в выделенном методе OnRender().
Выполнение специального рисования Когда вы создаете свои собственные пользовательские элементы, вы можете предпочесть переопределить OnRender() для рисования специального содержимого. Вы можете переопределить OnRender() в элементе, включающем содержимое (чаще всего это класс-наследник Decorator), так, чтобы добавить некое графическое украшение вокруг его содержимого. Или же вы можете переопределить OnRender() в элементе, который не имеет никакого вложенного содержимого, чтобы нарисовать сразу все его визуальное представление. Например, вы можете создать собственный элемент, рисующий мелкие графические детали, который затем можно использовать в другом элементе управления через композицию. Примером в WPF может служить элемент TickBar, который рисует метки в Slider. Элемент TickBar встроен в визуальное дерево Slider через свой шаблон по умолчанию (наряду с Border и Track, которые включают два RepeatButton и Thumb). Возникает естественный вопрос: когда нужно использовать относительно низкоуровневый подход на основе OnRender(), и когда применять композицию с другими классами (такими как элементы-наследники Shape) для рисования того, что вам нужно? Чтобы решить, потребуется оценить степень сложности необходимой графики и степень интерактивности, которую следует обеспечить. Например, рассмотрим класс ButtonChrome. В реализации WPF этого класса специальный код отображения принимает во внимание различные свойства, включая RenderDefaulted, RenderMouseOver и RenderPressed. Шаблон элемента управления по умолчанию для Button использует триггеры для установки этих свойств в соответствующее время, как было показано в главе 15. Например, это когда курсор мыши перемещается над кнопкой, класс Button использует триггер для установки свойства ButtonChrome.RenderMouseOver в true. Всякий раз при изменении свойств RenderDefaulted , RenderMouseOver или RenderPressed класс ButtonChrome вызывает базовый метод InvalidateVisual(), чтобы указать, что его текущий внешний вид перестал быть актуальным. Затем WPF вызывает метод ButtonChrome.OnRender() для получения нового графического представления. Если бы класс ButtonChrome применял композицию, такое поведение было бы труднее реализовать. Достаточно легко создать стандартный внешний вид для класса ButtonChrome, используя правильные элементы, но тогда понадобится больше работы для модификации, когда состояние кнопки изменяется. Вам пришлось бы динамически изменять все вложенные элементы, составляющие класс ButtonChrome, или же — если внешний вид изменится более радикально — скрывать один элемент и показывать другой на его месте. Большинство пользовательских элементов не нуждаются в специальной визуализации. Но если вам нужно отобразить сложные визуальные вещи, которые существенно изменяются при изменении свойств или выполнении некоторых действий, то подход на основе специальной визуализации может оказаться проще в применении и более легковесным.
Book_Pro_WPF-2.indb 841
19.05.2008 18:11:37
842
Глава 24
Элемент, выполняющий специальное рисование Зная, как работает метод OnRender(), и когда его следует использовать, рассмотрим пользовательский элемент управления, который продемонстрирует его в действии. Следующий код определяет элемент по имени CustomDrawnElement, который демонстрирует простой эффект. Он рисует затененный фон, используя RadialGradientBrush. Трюк состоит в том, что самая яркая точка, с которой начинается градиент, устанавливается динамически и следует за курсором мыши. Таким образом, когда пользователь перемещает курсор мыши над элементом, точка белого блика следует за ней, как показано на рис. 24.6. CustomDrawnElement не нуждается в том, чтобы содержать в себе какое-то дочернее содержиРис. 24.6. Элемент, выполняющий мое, поэтому он унаследован непосредственно от специальное рисование FrameworkElement . Он позволяет устанавливать только одно свойство — цвет фона градиента. (Цвет переднего плана жестко закодирован как белый, хотя вы легко можете это изменить.) public class CustomDrawnElement : FrameworkElement { public static DependencyProperty BackgroundColorProperty; static CustomDrawnElement() { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(Colors.Yellow); metadata.AffectsRender = true; BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElement), metadata); } public Color BackgroundColor { get { return (Color)GetValue(BackgroundColorProperty); } set { SetValue(BackgroundColorProperty, value); } } ...
Свойство зависимостей BackgroundColor специально помечено флагом Framework PropertyMetadata.AffectRender. В результате этого WPF автоматически вызывает OnRender() всякий раз при изменении цвета. Однако вы также должны обеспечить вызов метода OnRender(), когда курсор мыши перемещается в новую позицию. Это выполняется вызовом метода InvalidateVisual() в нужные моменты: ... protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); this.InvalidateVisual(); } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); this.InvalidateVisual(); } ...
Book_Pro_WPF-2.indb 842
19.05.2008 18:11:37
Пользовательские элементы
843
Единственное, что остается — код отображения. Он использует метод
DrawingContext.DrawRectangle() для рисования фона элемента. Свойства ActualWidth и ActualHeight указывают финальные отображаемые размеры элемента. ... protected override void OnRender(DrawingContext dc) { base.OnRender(dc); Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight); dc.DrawRectangle(GetForegroundBrush(), null, bounds); } ...
И, наконец, приватный вспомогательный метод по имени GetForegroundBrush() конструирует корректную кисть RadialGradientBrush на основе текущей позиции курсора мыши. Чтобы вычислить центральную точку, вам нужно преобразовать текущую позицию мышки над элементом в относительную позицию от 0 до 1, которой ожидает RadialGradientBrush. ... private Brush GetForegroundBrush() { if (!IsMouseOver) { return new SolidColorBrush(BackgroundColor); } else { RadialGradientBrush brush = new RadialGradientBrush( Colors.White, BackgroundColor); // Получить позицию курсора мыши в независимых от устройства // единицах относительно самого элемента управления. Point absoluteGradientOrigin = Mouse.GetPosition(this); // Преобразовать координаты точки в пропорциональные (от 0 до 1) значения. Point relativeGradientOrigin = new Point( absoluteGradientOrigin.X / base.ActualWidth, absoluteGradientOrigin.Y / base.ActualHeight); // Изменить кисть. brush.GradientOrigin = relativeGradientOrigin; brush.Center = relativeGradientOrigin; return brush; } } }
На этом пример завершен.
Специальный декоратор В качестве главного правила: вы никогда не должны использовать специальное рисование в элементе управления. Если вы делаете это, то тем самым нарушаете основополагающий принцип элементов управления без внешнего вида в WPF. Проблема в том, что, однажды жестко закодировав некоторую логику рисования, вы гарантируете, что часть визуального представления вашего элемента управления невозможно будет настроить с помощью шаблона. Намного лучший подход заключается в проектировании отдельного элемента, который рисует ваше специализированное содержимое (такого как класс CustomDrawnElement из предыдущего примера), а затем применении его внутри шаблона по умолчанию для вашего элемента управления. Такой подход используется в обоих элементах управления, которые мы рассматривали в этой главе — Button и Slider.
Book_Pro_WPF-2.indb 843
19.05.2008 18:11:37
844
Глава 24
Стоит мельком взглянуть, как вы можете адаптировать предыдущий пример, чтобы он мог работать как часть шаблона элемента управления. Элементы, выполняющие рисование самих себя, обычно играют две роли в шаблоне элемента управления:
• рисуют некоторые мелкие графические детали (подобно стрелкам на кнопках прокрутки);
• предоставляют более детализированный фон или фрейм, окружающий другой элемент. Второй подход требует специального декоратора. Вы можете превратить
CustomDrawnElement в рисующий себя элемент, внеся два меленьких изменения. Для начала унаследуйте его от Decorator: public class CustomDrawnDecorator : Decorator
Затем переопределите метод OnMeasure(), чтобы специфицировать требуемый размер. Это ответственность всех декораторов — просмотреть все свои дочерние элементы и добавить дополнительное пространство для декорации, а затем вернуть суммарный размер. CustomDrawnDecorator не нуждается в дополнительном пространстве для рисования рамки. Вместо этого он просто устанавливает свой размер таким, какого требует его содержимое, используя для этого следующий код: protected override Size MeasureOverride(Size constraint) { UIElement child = this.Child; if (child != null) { child.Measure(constraint); return child.DesiredSize; } else { return new Size(); } }
Однажды создав пользовательский декоратор, вы можете применять его в шаблоне элемента управления. Например, ниже представлен шаблон кнопки, который помещает градиентный фон, отслеживающий положение мыши, за содержимым кнопки. Он использует привязку шаблона, чтобы гарантировать соответствие свойств выравнивания и дополнения.
Теперь вы можете использовать этот шаблон для изменения стиля ваших кнопок. Конечно, чтобы сделать такой декоратор более практичным, вероятно, стоит изменять его внешний вид при щелчке кнопкой мыши. Этого можно добиться с помощью триггеров, модифицирующих свойства вашего “хромированного” класса. В главе 15 предлагается исчерпывающее обсуждение такого дизайна.
Book_Pro_WPF-2.indb 844
19.05.2008 18:11:37
Пользовательские элементы
845
Резюме В настоящей главе была подробно рассмотрена разработка пользовательского элемента управления в WPF. Вы увидели, как строятся базовые пользовательские элементы управления и расширяются существующие элементы управления WPF, а также как устроен “золотой стандарт” WPF — основанные на шаблонах элементы, лишенные внешнего вида. И, наконец, вы изучили специальное рисование и применение специально рисованного содержимого с элементами без внешнего вида. Если вы планируете углубиться в мир разработки специализированных элементов управления, то найдете в Internet некоторые замечательные примеры. Хорошей начальной точкой может стать проект примеров Bag-O-Tricks, представленный Кевином Муром (Kevin Moore) (руководитель проекта в команде WPF) по адресу http://wpf.netfx3.com/ files/folders/controls/entry8196.aspx. Этот пример включает широкое разнообразие пользовательских элементов управления, среди которых элементы управления датами, текстовое поле для ввода числовой информации со стрелочками “больше–меньше”, указатель цвета и панель со встроенной анимацией.
Book_Pro_WPF-2.indb 845
19.05.2008 18:11:37
ГЛАВА
25
Взаимодействие с Windows Forms В
идеальном мире, как только разработчик освоил бы новую технологию, подобную WPF, он мог бы оставить прежнюю в прошлом. Все должно было бы разрабатываться только на основе нового, наиболее богатого инструментария, не нужно было бы беспокоиться об унаследованном коде. Конечно, этот идеальный мир не имеет ничего общего с миром реальным, в котором есть две причины, которые вынуждают разработчиков WPF в определенный момент взаимодействовать с платформой Windows Forms: чтобы сохранить инвестиции в разработку существующего кода и чтобы компенсировать средства, недостающие в WPF. В этой главе мы ознакомим вас с разными стратегиями взаимодействия Windows Forms и WPF. Мы рассмотрим использование обоих типов окон в едином приложении, а также продемонстрируем еще более эффектный трюк — смешивание содержимого из обеих платформ в одном окне. Но прежде чем погрузиться во взаимодействие WPF и Windows Forms, стоит сделать шаг назад и оценить причины, по которым вы должны (и не должны) использовать взаимодействие WPF.
Оценка способности к взаимодействию Если вы потратили последние несколько лет на программирование в Windows Forms, то, вероятно, у вас есть несколько приложений и библиотека собственного кода, на которую вы полагаетесь. На данный момент не существует инструмента для трансформации интерфейсов Windows Forms в соответствующие интерфейсы WPF (и даже если бы такой инструмент существовал, он мог бы послужить лишь начальной точкой длинного и непростого процесса миграции). Конечно, нет необходимости “трансплантировать” приложение Windows Forms в WPF в новых проектах. Однако в жизни все не так просто. Вы можете решить, что стоит добавить некоторое средство WPF (вроде трехмерной анимации) к существующему приложению Windows Forms. Или вам может понадобиться постепенный перевод приложения Windows Forms в среду WPF, причем по частям, выпуская новые обновленные версии. В любом случае поддержка взаимодействия в WPF может помочь выполнить такую миграцию постепенно, не отбрасывая в одночасье все, что было сделано до этого. Другая причина, по которой стоит рассмотреть взаимодействие — это когда нужно использовать средства, которых нет в WPF. Хотя в WPF набор средств расширен в тех областях, которых никогда не касалась платформа Windows Forms (таких как анимация, трехмерная графика и отображение форматированных документов), все же существуют некоторые средства Windows Forms, которые отсутствуют в WPF либо имеют более
Book_Pro_WPF-2.indb 846
19.05.2008 18:11:37
Взаимодействие с Windows Forms
847
зрелую реализацию в Windows Forms. Это не значит, что вы должны заполнить пробел, используя элементы управления Windows Forms — в конце концов, может быть проще перестроить эти средства, использовать альтернативы или просто подождать новых выпусков WPF, — но все же возможность взаимодействия платформ выглядит заманчиво. Прежде чем начать смешивать элементы WPF с элементами управления Windows Forms, важно осознать общие цели. Во многих ситуациях разработчики сталкиваются с необходимостью выбора между пошаговым усовершенствованием приложения Windows Forms (и постепенным переводом его в мир WPF) и заменой его заново переписанным шедевром на WPF. Очевидно, что первый подход быстрее и проще в тестировании и реализации. Однако в случае сложного приложения, которому требуются значительные “инъекции” WPF, может быть проще начать новый проект WPF и импортировать в него необходимые части старого приложения. На заметку! Как всегда, при переходе с одной платформы пользовательского интерфейса на другую вам приходится переносить именно пользовательский интерфейс. Все прочие детали, вроде кода доступа к данным, правилам проверки достоверности, доступа к файлам и т.п., должны быть абстрагированы в отдельные классы (и, возможно, даже в отдельные сборки), которые вы сможете подключить к интерфейсной части на WPF — так же просто, как к приложению Windows Forms. Конечно, подобное разбиение на компоненты не всегда возможно, а иногда некоторые детали (такие как привязка данных и стратегии проверки достоверности) вызывают необходимость в такой специализации ваших классов, которая неизбежно ограничивает их повторную используемость.
Отсутствующие средства в WPF Разрабатывая программу на WPF, вы можете использовать элемент управления из Windows Forms, который вы знаете и любите, и эквивалента которому нет в WPF. Как всегда, при этом следует тщательно взвесить возможные варианты и исследовать альтернативы, прежде чем обратиться к средствам взаимодействия платформ. В табл. 25.1 представлен обзор недостающих элементов управления и их возможных замен.
Таблица 25.1. Недостающие элементы управления и средства WPF Элемент управления Windows Forms
Ближайший эквивалент WPF
Использовать Windows Forms?
LinkLabel
Используйте встроенные Hyperlink в TextBlock. Нет Как это сделать — описано в главе 9.
MaskedTextBox
Эквивалентного элемента управления не существует (хотя вы можете построить собственный, применив для этого класс System.ComponentModel. MaskedTextProvider, как описано в главе 24).
Да
DateTimePicker и MonthCalendar
Версии этих элементов управления из Windows Forms служат оболочками элементов Win32, которые далеки от совершенства. (Например, они не всегда корректно зависят от установленных свойств и не поддерживают значений null.) Хотя соответствующие версии этих элементов для WPF не включены в .NET 3.5, вы можете загрузить их по адресу http://j832.com/BagOTricks.
Нет
Book_Pro_WPF-2.indb 847
19.05.2008 18:11:38
848
Глава 25 Продолжение табл. 25.1
Элемент управления Windows Forms
DomainUpDown и NumericUpDown CheckedListBox
Ближайший эквивалент WPF Для эмуляции этих элементов управления используйте TextBox с двумя элементами RepeatButton.
Использовать Windows Forms? Нет
Если вы не используете привязку данных, то можете Нет разместить множество элементов CheckBox внутри ScrollViewer. Если же поддержка привязки данных нужна, можно применить ListBox со специальным шаблоном элементов управления. Пример приведен в главе 18 (также и для RadioButtonList).
DataGridView
ListView и GridView предлагают разные способы для получения набора средств, подобных DataGridView, но не всех. Например, только DataGridView позволяет “заморозить” колонки, обеспечивает виртуализацию и систему многослойных стилей, позволяющую форматировать ячейки разных типов разными способами.
WebBrowser
Эквивалентного элемента управления не существует, Да но вы можете применить элемент Frame с HTMLстраницами внутри (как описано в главе 9). Однако Frame не предоставляет доступа к объектной модели страницы. Это значит, что вам понадобится использовать WebBrowser, если вы хотите взаимодействовать со страницей программно.
PropertyGrid
Эквивалентного элемента управления не существует.
ColorDialog, FolderBrowserDialog, FontDialog, PageSetupDialog
Вы можете использовать эти компоненты в WPF. Нет Однако большая часть этих часто используемых диалоговых окон легко воссоздается в WPF, без старомодного внешнего вида. При желании примеры можно найти в Internet. (А в главе 24 приведен пример базового специализированного элемента для выбора цвета.)
PrintPreviewControl и PrintPreviewDialog
Здесь возможно несколько подходов в стиле “сделай Возможно сам”. Простейший заключается в конструировании FlowDocument программно, который затем вы можете отображать в средстве просмотра документов и посылать на принтер. Хотя PrintPreviewControl и PrintPreviewDialog — более зрелые элементы, требующие меньше работы, применение их в WPF не рекомендуется. Это связано с тем, что при этом вам придется переключиться на старую модель печати Windows Forms. Конечно, если у вас есть существующий код печати, использующий библиотеки Windows Forms, взаимодействие с ним позволит избежать лишней работы.
ErrorProvider, HelpProvider
В WPF нет поддержки поставщиков расширений Да Windows Forms. Если у вас имеются формы, использующие эти средства, вы может продолжать применять их в приложении WPF посредством взаимодействия. Однако вы не можете использовать этих поставщиков для отображения сообщений об ошибках или контекстной помощи для элементов управления WPF.
Book_Pro_WPF-2.indb 848
Да
Да
19.05.2008 18:11:38
Взаимодействие с Windows Forms
849
Окончание табл. 25.1 Элемент управления Windows Forms
Ближайший эквивалент WPF
Использовать Windows Forms?
AutoComplete
Хотя WPF включает функциональность AutoComplete в ComboBox (см. главу 18) через свойство IsTextSearchingenabled, это простое средство AutoComplete, которое заполняет поле предполагаемыми значениями только из текущего списка. Оно не предоставляет полного списка предполагаемых значений, как это делает AutoComplete из Windows Forms, как и не предоставляет доступа к последним URL, записанным операционной системой. Использование Windows Forms для получения этой поддержки обычно чрезмерно — лучше отказаться от него вовсе или построить самостоятельно.
MDI
WPF не поддерживает окон MDI. Однако система Да компоновки достаточно гибка, чтобы адаптировать широкое разнообразие специальных подходов, включая самодельные окна с вкладками. Однако это потребует определенных усилий. Если же вам действительно нужно MDI-приложение, лучше построить полноценное приложение Windows Forms, а не пытаться комбинировать WPF с Windows Forms.
Возможно
На заметку! Подробную информацию о специфике Windows Forms, включая AutoComplete, его поддержку MDI, модели печати и поставщиках расширений можно найти в книге Pro .NET 2.0 Windows Forms and Custom Controls in C# (Apress, 2005г.). Как видно в табл. 25.1, некоторые элементы управления Windows Forms являются хорошими кандидатами на интеграцию, поскольку могут быть легко вставлены в окна WPF, а для воссоздания их своими силами может понадобиться много работы. К ним относятся MaskedTextBox, DataGridView, PropertyGrid и WebBrowser (если вы хотите взаимодействовать с вашими собственными элементами управления Windows Forms, они, вероятно, также должны быть включены в этот список; другими словами, их легче перенести в WPF, нежели воссоздать с нуля). Существует большой набор элементов управления, которые недоступны в WPF, но имеют подходящие (а иногда и усовершенствованные) эквиваленты. К ним относятся DateTimePicker, CheckedListBox и ImageList. И, наконец, есть некоторые средства, которые просто недоступны в WPF, т.е. они не представлены в WPF, и не существует стратегии взаимодействия для включения их туда. Если вы хотите построить или обновить приложение, которое интенсивно использует поставщика расширений (вроде ErrorProvider, HelpProvider либо специального, разработанного вами) или применяет окна MDI, то лучше все-таки оставить его на Windows Forms. Вы можете интегрировать содержимое WPF в свое приложение Windows Forms, но обратная задача — миграция в WPF — потребует больше работы.
Смешивание окон и форм Наиболее ясный способ интеграции содержимого WPF и Windows Forms состоит в помещении их в отдельные окна. Таким образом, ваше приложение будет состоять из хо-
Book_Pro_WPF-2.indb 849
19.05.2008 18:11:38
850
Глава 25
рошо инкапсулированных классов окон, каждый из которых имеет дело только с одной технологией. Любые подробности взаимодействия обрабатываются в коде “клея” — логике, создающей и отображающей окна.
Добавление форм к приложению WPF Простейший подход к смешиванию окон и форм заключается в добавлении одной или более форм (из набора инструментов Windows Forms) к обычному в остальных отношениях приложению WPF. Visual Studio позволяет это сделать легко — просто выполните щелчок правой кнопкой мыши на имени проекта в Solution Explorer и выберите в контекстном меню команду AddNew Item (ДобавитьНовый элемент). После этого выберите слева категорию Windows Forms, затем — Windows Form template. И, наконец, присвойте вашей форме имя файла и щелкните на кнопке Add (Добавить). При первом добавлении формы Visual Studio добавит ссылки на все необходимые сборки Windows Forms, включая System.Windows.Forms.dll и System.Drawing.dll. Вы можете спроектировать форму в проекте WPF точно так же, как делаете это в проекте Windows Forms. Когда вы открываете форму, Visual Studio загружает обычный визуальный конструктор форм Windows Forms и наполняет панель инструментов элементами управления Windows Forms. Когда вы открываете файл XAML для окна WPF, то получаете вместо этого знакомую среду конструктора WPF. Совет. Для лучшего разделения между содержимым WPF и Windows Forms вы можете предпочесть поместить “внешнее” содержимое в отдельную сборку библиотеки классов. Например, приложение Windows Forms может использовать окна WPF, определенные в отдельной сборке. Такой подход особенно оправдан, если вы планируете многократно использовать некоторые из этих окон как в приложениях Windows Forms, так и в WPF.
Добавление окон WPF в приложение Windows Forms Обратный ход несколько сложнее. Visual Studio не позволяет напрямую создать новое окно WPF в приложении Windows Forms. (Другими словами, вы не увидите его в качестве одного из доступных шаблонов при щелчке правой кнопкой мыши на имени проекта и выборе в контекстном меню команды AddNew Item (ДобавитьНовый элемент).) Однако вы можете добавить существующие файлы .cs и .xaml, которые определяют окно WPF, из другого проекта. Чтобы сделать это, щелкните правой кнопкой мыши на имени проекта в Solution Explorer, выберите в контекстном меню команду AddExisting Item (ДобавитьСуществующий элемент) и найдите оба эти файла. Вам также понадобится добавить ссылки на центральные сборки WPF (PresentationCore.dll, PresentationFramework.dll и WindowsBase.dll). Совет. Можно упростить добавление всех необходимых вам ссылок WPF. Вы можете добавить пользовательский элемент управления WPF (который Visual Studio поддерживает), что вынудит Visual Studio добавить все ссылки автоматически. Вы можете затем удалить этот пользовательский элемент управления из проекта. Чтобы добавить такой элемент WPF, выполните щелчок правой кнопкой мыши на имени проекта, выберите в контекстном меню команду AddNew Item (ДобавитьНовый элемент), укажите в качестве категории WPF и затем выберите шаблон User Control (WPF) termplate. Если добавить окно WPF к приложению Windows Forms, все будет правильно. Когда вы откроете его, то сможете использовать дизайнер WPF для его модификации. При построении проекта будет скомпилирован XAML и автоматически сгенерированный код
Book_Pro_WPF-2.indb 850
19.05.2008 18:11:38
Взаимодействие с Windows Forms
851
будет объединен с вашим классом отделенного кода, как если бы это было полноценным WPF-приложением. Создание проекта, использующего формы и окна, несложно. Однако есть несколько дополнительных нюансов, которые следует учитывать, когда вы отображаете эти формы и окна во время выполнения. Если вам нужно показать окно или форму модально (как это делается с диалоговыми окнами), то такая задача довольно проста и не требует изменения кода. Но если вы хотите показывать окна в немодальном режиме, то вам понадобится разработать дополнительный код, чтобы обеспечить корректную поддержку клавиатуры, что будет продемонстрировано в последующих разделах.
Отображение модальных окон и форм Отображение модальной формы в приложении WPF не требует усилий. Вы используете в точности тот же код, что и в проекте Windows Forms. Например, если у вас есть класс формы по имени Form1, то для его модального отображения вы применяете код, подобный следующему: Form1 frm = new Form1(); if (frm.ShowDialog() == System.Windows.Forms.DialogResult.OK) { MessageBox.Show("Вы щелкнули на OK в форме Windows Forms."); }
Вы заметите, что метод Form.ShowDialog() работает немного иначе, чем WPF-метод Window.ShowDialog() (описанный в главе 8). В то время как Window.ShowDialog() возвращает true, false или null, метод Form.ShowDialog() возвращает значение из перечисления DialogResult. Обратная задача — отображение окна WPF из формы — столь же проста. Вы просто взаимодействуете с общедоступным интерфейсом вашего класса окна, а WPF позаботится об остальном: Window1 win = new Window1(); if (win.ShowDialog() == true) { MessageBox.Show("Вы щелкнули на OK в окне WPF."); }
Отображение немодальных окон и форм Если вы хотите отображать окна или формы в немодальном режиме, то здесь все не так просто. Сложность состоит в том, что клавиатурный ввод принимается корневым приложением и должен быть доставлен в соответствующее окно. Чтобы это работало между содержимым WPF и Windows Forms, необходим какой-то способ передачи этих сообщений нужному окну или форме. Если вы хотите показывать окно WPF в немодальном режиме изнутри приложения Windows Forms, то должны для этого использовать статический метод ElementHost. EnableModelessKeyboardInterop() . Вам также понадобится ссылка на сборку WindowsFormsIntegration.dll, которая определяет класс ElementHost в пространстве имен System.Windows.FormsIntegration. (Подробнее о классе ElementHost будет сказано далее в этой главе). Метод EnableModelessKeyboardInterop() вызывается после создания окна, но перед его отображением. Вызывая этот метод, передайте ему ссылку на новое окно WPF, как показано ниже: Window1 win = new Window1(); ElementHost.EnableModelessKeyboardInterop(win); win.Show();
Book_Pro_WPF-2.indb 851
19.05.2008 18:11:38
852
Глава 25
При вызове EnableModelessKeyboardInterop() объект ElementHost добавляет фильтр сообщений в приложение Windows Forms. Этот фильтр сообщений перехватывает клавиатурные сообщения, когда активно ваше окно WPF, и пересылает их ему. Без этого ваши элементы управления WPF просто не получат клавиатурного ввода. Если нужно отобразить немодальное приложение Windows Forms внутри приложения WPF, используйте аналогичный метод WindowsFormsHost.EnableWindowsFormsInterop(). Однако в этом случае не нужно передавать ссылку на форму, которую вы хотите показать. Вместо этого вы просто вызываете этот метод однажды — перед отображением любой формы. (Хорошим решением будет вызвать его при запуске приложения.) WindowsFormsHost.EnableWindowsFormsInterop();
Теперь вы можете просто показать форму в немодальном режиме, безо всяких дополнительных усилий: Form1 frm = new Form1(); frm.Show();
Без вызова EnableWindowsFormsInterop() ваша форма будет отображаться, но не распознает никакого клавиатурного ввода. Например, вы не сможете использовать клавишу для перехода от одного элемента управления к другому. Этот процесс можно расширить на несколько уровней. Например, вы можете создать окно WPF, отображающее форму (модально или не модально), а эта форма, в свою очередь, может показать окно WPF. Хотя это не придется делать очень часто, все же это лучший подход, чем поддержка взаимодействия между разнородными элементами, о которой мы поговорим ниже. Такая поддержка позволяет интегрировать разные типы содержимого в одном окне, но не позволяет вкладывать на более чем один уровень вглубь (например, создать окно WPF, содержащее элемент управления Windows Forms, который, в свою очередь, содержит в себе элемент управления WPF).
Визуальные стили элементов управления Windows Forms Когда вы отображаете форму в приложении WPF, такая форма использует старомодные (предшествующие Windows XP) стили кнопок и других распространенных элементов управления. Это связано с тем, что поддержка новых стилей должна быть явно включена вызовом метода Application.EnableVisualStyles(). Обычно Visual Studio добавляет эту строку кода в метод Main() каждого нового приложения Windows Forms. Однако когда вы создаете приложение WPF, эта деталь не включается. Чтобы решить эту проблему, просто однажды вызовите метод EnableVisualStyles() перед отображением любого содержимого Windows Forms. Подходящее место для этого — при запуске приложения, как показано ниже: public partial class App : System.Windows.Application { protected override void OnStartup(StartupEventArgs e) { // Инициирует событие Startup. base.OnStartup(e); System.Windows.Forms.Application.EnableVisualStyles(); } }
Отметим, что метод EnableVisualStyles() определен в классе System.Windows. Forms.Application, а не в System.Windows.Application.
Book_Pro_WPF-2.indb 852
19.05.2008 18:11:38
Взаимодействие с Windows Forms
853
Классы Windows Forms, которые не нуждаются во взаимодействии с WPF Как вы знаете, элементы управления Windows Forms имеют другую иерархию наследования, отличающуюся от элементов WPF. Эти элементы не могут использоваться в окне WPF без взаимодействия. Однако есть некоторые компоненты Windows Forms, которые не имеют такого ограничения. Если у вас есть ссылка на необходимую сборку (обычно System.Windows.Forms.dll), то вы можете использовать эти типы без какихлибо специальных усилий. Например, вы можете применять классы диалогов (ColorDialog , FontDialog , PageSetupDialog и т.п.) непосредственно. На практике это не совсем удобно, потому что эти диалоги несколько устарели и упаковывают структуры, являющиеся частью Windows Forms, а не WPF. Например, если вы используете ColorDialog, то получаете объект System.Drawing.Color, вместо действительно нужного вам объекта System. Windows.Media.Color. То же касается применения FontDialog и PrintPreviewDialog, которые предназначены для работы с более старой моделью печати Windows Forms. Фактически, единственный диалог Windows Forms, без которого не обойтись, поскольку он не имеет эквивалента в WPF — это FolderBrowserDialog из пространства имен Microsoft.Win32, который позволяет пользователю выбрать папку. Более полезные компоненты Windows Forms включают SoundPlayer (описанный в главе 22), который вы можете использовать как легковесный эквивалент MediaPlayer и MediaElement из WPF; BackgroundWorker (описанный в главе 3), который можно применять для безопасного управления асинхронными задачами; NotifyIcon (описанный ниже), позволяющий отображать пиктограмму в системном лотке. Единственным недостатком использования NotifyIcon в окне WPF является отсутствие поддержки во время проектирования. То есть вам придется вручную создать NotifyIcon, присоединить обработчики событий и т.д. Применив пиктограмму через свойство Icon, и установив Visible равным true, вы увидите эту пиктограмму в системном лотке (как показано на рис. 25.1). По завершении вашего приложения вы должны вызвать Dispose() для NotifyIcon, чтобы немедленно удалить ее из системного лотка.
Рис. 25.1. Пиктограмма системного лотка
NotifyIcon использует некоторые части Windows Forms, например, контекстное меню Windows Forms, являющееся экземпляром класса System.Windows.Forms. ContextMenuStrip. Таким образом, даже если вы используете NotifyIcon с приложением WPF, вы должны определить его контекстное меню с применением модели Windows Forms. Создание всех объектов для меню в коде и присоединение к нему обработчиков событий — не такое уж незначительное неудобство. К счастью, имеется более простое решение для построения приложения WPF, использующего NotifyIcon. Вы можете создать класс компонента. Класс компонента — это пользовательский класс, унасле-
Book_Pro_WPF-2.indb 853
19.05.2008 18:11:38
854
Глава 25
дованный от System.ComponentModel.Component. Он обеспечивает два средства, которых не хватает обычным классам: поддержку детерминировано реализованных ресурсов (когда вызывается его метод Dispose()) и поддержку во время проектирования в Visual Studio. Каждый компонент получает поверхность дизайна (технически известную как лоток компонента), где вы можете перетаскивать и конфигурировать другие классы, реализующие интерфейс IComponent, включая Windows Forms. Другими словами, вы можете использовать лоток компонента для построения и конфигурирования NotifyIcon, дополненного контекстным меню и обработчиками событий. Ниже описаны шаги, которые нужно проделать, чтобы построить пользовательский компонент, упаковывающий экземпляр NotifyIcon и включающий контекстное меню. 1. Откройте или создайте проект WPF. 2. Выполните щелчок правой кнопкой мыши на имени проекта в Solution Explorer и выберите в контекстном меню команду AddNew Item (ДобавитьНовый элемент). Выберите шаблон Component Class (Класс компонента), укажите имя класса компонента и щелкните на кнопке Add (Добавить). 3. Поместите NotifyIcon на поверхность дизайна вашего компонента. (Вы найдете NotifyIcon в разделе Common Controls (Общие элементы управления) панели инструментов.) 4. В этот момент Visual Studio добавит необходимую ссылку на сборку System. Windows.Forms.dll. Однако ему может не удастся добавить ссылку на пространство имен System.Drawing.dll, которое содержит основные типы Windows Forms. Тогда вы должны добавить эту ссылку вручную. 5. Поместите ContextMenuStrip на поверхность дизайна вашего компонента (из раздела Menus & Toolbars (Меню и панели инструментов) панели инструментов). Это предоставит контекстное меню для NotifyIcon. На рис. 25.2 показаны оба ингредиента в Visual Studio.
Рис. 25.2. Поверхность дизайна компонента
Book_Pro_WPF-2.indb 854
19.05.2008 18:11:39
Взаимодействие с Windows Forms
855
6. Выберите NotifyIcon и сконфигурируйте его, используя окно свойств. Вам понадобится установить следующие свойства: Text (текст подсказки, всплывающий при наведении курсора мыши на NotifyIcon), Icon (пиктограмма, которая появится в системном лотке) и ContextMenuStrip (контекстное меню, добавленное на шаге 5). 7. Чтобы построить контекстное меню, щелкните правой кнопкой мыши на ContextMenuStrip и выберите в появившемся контекстном меню команду Edit Items (Редактировать элементы). Вы увидите редактор коллекции, с помощью которого можно добавлять пункты меню (которые следует поместить после корневого пункта меню). Присвойте им нужные имена, поскольку обработчики событий нужно будет присоединять самостоятельно. 8. Чтобы увидеть код вашего класса компонента, щелкните правой кнопкой мыши на компоненте в Solution Explorer и выберите в появившемся контекстном меню команду View Code (Показать код). (Не открывайте файл .Designer.cs. Он содержит код, сгенерированный Visual Studio автоматически, который комбинируется с остальной частью кода компонента посредством частичных классов.) 9. Добавьте код обработчиков событий меню. Ниже приведен пример добавления обработчиков событий для двух пунктов меню — Close и ShowWindow. public partial class NotifyIconWrapper : Component { public NotifyIconWrapper() { InitializeComponent(); // Присоединить обработчики событий. cmdClose.Click += cmdClose_Click; cmdShowWindow.Click += cmdShowWindow_Click; } // Использовать только один экземпляр окна. private Window1 win = new Window1(); private void cmdShowWindow_Click(object sender, EventArgs e) { // Показать окно (и перенести его на передний план, если оно уже видимо). if (win.WindowState == System.Windows.WindowState.Minimized) win.WindowState = System.Windows.WindowState.Normal; win.Show(); win.Activate(); } private void cmdClose_Click(object sender, EventArgs e) { System.Windows.Application.Current.Shutdown(); } // Очистка этого компонента реализуется освобождением // всех содержащихся компонентов (включая NotifyIcon). protected override void Dispose(bool disposing) { if (disposing && (components != null)) components.Dispose(); base.Dispose(disposing); } // (Код визуального конструктора опущен.) }
Построив класс пользовательского компонента, вам нужно просто создать его экземпляр, когда понадобится показать NotifyIcon. Это инициирует дизайнерский код вашего компонента, который создаст объект NotifyIcon, сделав его видимым в системном лотке.
Book_Pro_WPF-2.indb 855
19.05.2008 18:11:39
856
Глава 25
Удаление пиктограммы из системного лотка так же просто — для этого вам нужно вызвать метод Dispose() вашего компонента. Это заставит его вызвать Dispose() для всех составляющих его компонентов, включая NotifyIcon. Рассмотрим пример класса приложения, отображающего пиктограмму при запуске и удаляющего его при завершении приложения: public partial class App : System.Windows.Application { private NotifyIconWrapper component; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); this.ShutdownMode = ShutdownMode.OnExplicitShutdown; component = new NotifyIconWrapper(); } protected override void OnExit(ExitEventArgs e) { base.OnExit(e); component.Dispose(); } }
Чтобы завершить этот пример, убедитесь, что атрибут StartupUri удален из файла App.xaml. Таким образом, при запуске приложение покажет NotifyIcon, но не отобразит никакого дополнительного окна до тех пор, пока пользователь не выберет соответствующий пункт из меню. Это пример полагается еще на один трюк. Единственное главное окно остается “живым” для всего приложения и отображается всякий раз, когда пользователь выбирает из меню пункт Show Window (Показать окно). Однако при закрытии пользователем окна возникает проблема. Есть два возможных пути ее разрешения: вы можете пересоздавать окно при необходимости, когда пользователь в следующий раз щелкнет на Show Window, или же вы можете перехватить событие Window.Closing и молча скрыть окно вместо его уничтожения. Вот как это делается: private void window_Closing(object sender, CancelEventArgs e) { e.Cancel = true; this.WindowState = WindowState.Minimized; this.ShowInTaskbar = false; }
Обратите внимание, что этот код не изменяет свойства Visibility окна и не вызывает метод Hide(), поскольку ни то, ни другое при закрытии окна невозможно. Вместо этого окно минимизируется и удаляется из панели задач. При восстановлении окна вам нужно проверить состояние окна и вернуть в его нормальное состояние вместе с кнопкой в панели задач.
Создание окон со смешанным содержимым В некоторых случаях разделение Windows Forms и WPF по разным окнам не подходит. Например, может потребоваться поместить содержимое WPF в существующую форму рядом с содержимым Windows Forms. Хотя эта модель концептуально запутана, WPF справляется с ней достаточно успешно. Фактически включение содержимого Windows Forms в приложение WPF (или наоборот) сделать проще, чем добавить содержимое ActiveX к приложению Windows Forms. В последнем случае Visual Studio должен генерировать класс-оболочку, который служит
Book_Pro_WPF-2.indb 856
19.05.2008 18:11:39
Взаимодействие с Windows Forms
857
посредником между элементом управления ActiveX и вашим кодом, занимаясь передачей от управляемого к неуправляемому коду. Этот класс-оболочка специфичен для компонента, т.е. каждый используемый вами элемент управления ActiveX требует отдельного класса-оболочки. А из-за причуд COM интерфейс, предоставляемый оболочкой, может не соответствовать в точности интерфейсу лежащего в основе компонента. При интеграции содержимого Windows Forms и WPF вам не нужны классы-оболочки. Вместо этого вы используете один из небольшого набора контейнеров, в зависимости от конкретного сценария. Эти контейнеры работают с любым классом, так что шаг генерации кода исключается. Такая упрощенная модель возможна, поскольку даже несмотря на значительное отличие технологий Windows Forms и WPF, обе они базируются на управляемом коде. Наиболее важное преимущество такого дизайна заключается в том, что в своем коде вы непосредственно можете взаимодействовать с элементами управления Windows Forms и элементами WPF. Слой взаимодействия вступает в действие, только когда выполняется визуализация содержимого окна. Это происходит автоматически, не требуя вмешательства разработчика. Вам также не приходится беспокоиться об обработке событий клавиатуры в немодальных окнах, поскольку используемые для организации взаимодействия классы (ElementHost и WindowsFormsHost) делают это автоматически.
Зазор между WPF и Windows Forms Для того чтобы интегрировать WPF и Windows Forms в одном окне, вам нужно какимто образом выделить часть вашего окна для “чужого” содержимого. Например, совершенно разумно вставить трехмерную графику в приложение Windows Forms, поскольку вы можете поместить ее в отдельную область окна (или даже занять ею все окно). Однако вряд ли стоит перерисовать все кнопки вашего приложения Windows Forms, сделав их элементами WPF, поскольку вам придется создавать отдельную область WPF для каждой такой кнопки. Наряду с соображениями сложности есть еще некоторые вещи, которые невозможны при взаимодействии с WPF. Например, вы не можете комбинировать содержимое WPF и Windows Forms путем их перекрытия. Это значит, что вы не можете заставить анимацию WPF запустить летающий элемент над областью, за отображение которой отвечает Windows Forms. Соответственно, вы не можете перекрыть частично прозрачное содержимое Windows Forms над областью WPF, чтобы смешать их вместе. И то, и другое является нарушением правила зазора (airspace rule), которое требует, чтобы WPF и Windows Forms всегда находились в отдельных областях окна, которыми они управляют исключительно. На рис. 25.3 показано, что допускается, а что — нет. Технически правило зазора является результатом того факта, что в окне, включающем содержимое WPF и Windows Forms, обе области имеют свои отдельные дескрипторы окна — hwnd. Каждый hwnd управляется, отображается и обновляется отдельно. Дескрипторы окна управляются операционной системой Windows. В классических приложениях Windows каждый элемент управления представляет собой отдельное окно, а это значит, что каждый элемент управления владеет отдельной частью экрана. Очевидно, что “окна” такого рода не являются окнами верхнего уровня, которые перемещаются по вашему экрану. Это просто самодостаточные области (прямоугольные или другие). В WPF принята совершенно другая модель — там есть один hwnd высшего уровня, и механизм WPF отвечает за отображение целого окна, что делает возможным более симпатичную визуализацию (например, такие эффекты, как динамическое сглаживание) и обеспечивает намного более высокую степень гибкости (например, визуальные элементы, которые рисуют свое содержимое, выходящее за их границы).
Book_Pro_WPF-2.indb 857
19.05.2008 18:11:39
858
Глава 25 Разрешено Окно Windows Forms или WPF
Содержимое Windows Forms
Содержимое WPF
Не разрешено Окно Windows Forms или WPF
Содержимое WPF
Содержимое Windows Forms
Рис. 25.3. Правило зазора
На заметку! Есть несколько элементов WPF, которые используют отдельные дескрипторы окна. К ним относятся меню, всплывающие подсказки и выпадающие части комбинированных окон списков — всем им требуется возможность расширяться за пределы окна. Реализация правила зазора очень проста. Если вы поместите содержимое Windows Forms поверх содержимого WPF, то обнаружите, что оно всегда будет находиться сверху, независимо от того, как оно объявлено в коде разметки, или от того, какой контейнер компоновки вы используете. Это потому, что содержимое WPF — это одно окно, а содержимое Windows Forms реализуется в виде отдельных окон, отображающихся поверх части окна WPF. Если же вы поместите содержимое WPF в форму Windows Forms, результат будет несколько другим. Каждый элемент управления Windows Forms представляет собой отдельное окно, а потому имеет собственный hwnd. Поэтому содержимое WPF может быть размещено где угодно по отношению к другим элементам управления Windows Forms, в зависимости от z-индекса (z-индекс определяется порядком добавления элементов в коллекцию Controls родителя, так что элементы, добавленные позже, появляются поверх добавленных раньше). Однако содержимое WPF имеет свою собственную отдельную область. Это значит, что вы не можете применять прозрачность или другую технику для частичного перекрытия или комбинирования вашего элемента с содержимым Windows Forms. Вместо этого содержимое WPF остается в своей собственной области.
Размещение элементов управления Windows Forms в WPF Чтобы показать элемент управления Windows Forms в окне WPF, вы используете класс WindowsFormsHost из пространства имен System.Windows.Forms.Integration. Класс WindowsFormsHost — это элемент WPF (унаследованный от FrameworkElement),
Book_Pro_WPF-2.indb 858
19.05.2008 18:11:39
Взаимодействие с Windows Forms
859
который способен содержать в себе ровно один элемент управления Windows Forms, указываемый в его свойстве Child. Достаточно легко создавать и использовать WindowsFormsHost программно. Однако в большинстве случаев проще сделать это декларативно в коде разметки XAML. Единственный недостаток состоит в том, что Visual Studio не предоставляет достаточной поддержки визуального конструирования для элемента управления WindowsFormsHost. Хотя вы можете перетаскивать его на поверхность окна, наполнять его содержимым (и отображать нужное пространство имен) вам придется вручную. Первый шаг — отобразить пространство имен System.Windows.Forms, чтобы можно было обратиться к нужному элементу управления Windows.Forms:
После этого вы сможете создать WindowsFormsHost и элемент управления внутри него — точно так же, как делаете это с любым другим элементом WPF. Ниже приведен пример применения MaskedTextBox из Windows Forms.
На заметку! WindowsFormsHost может содержать в себе любой элемент управления Windows Forms (т.е. любой класс, унаследованный от System.Windows.Forms.Control). Он не может содержать компонентов Windows Forms, не являющихся элементами управления, вроде HelpProvider или NotifyIcon. На рис. 25.4 можно видеть MaskedTextBox в окне WPF. Большую часть свойств MaskedTextBox можно установить прямо в коде разметки. Это связано с тем, что Windows Forms использует ту же инфраструктуру TypeConverter (о которой мы говорили в главе 2) для преобразования строк в значения свойств определенного типа. Это всегда удобно — скажем, строковое представление типа может быть трудно ввести вручную, но обычно можно конфигурировать элементы управления Windows Forms без обращения к коду. Например, ниже приведен MaskedTextBox, оснащенный маской, которая формирует пользовательский ввод семизначного телефонного номера с необязательным кодом региона:
Рис. 25.4. Маскированное текстовое поле для ввода телефонного номера
Вы также можете применять расширения разметки XAML для заполнения null-значений, использовать статические свойства, создавать объекты типа или использовать объекты, определенные в коллекции Resources окна. Приведем пример, использующий расширение типа для установки свойства MaskedTextBox.ValidatingType. Он специ-
Book_Pro_WPF-2.indb 859
19.05.2008 18:11:39
860
Глава 25
фицирует, что MaskedTextBox должен преобразовывать ввод (строку телефонного номера) в Int32, когда читается свойство Text или изменяется фокус.
Есть одно расширение разметки, которое не будет работать — это выражение привязки данных, поскольку оно требует свойства зависимости. (Элементы управления Windows Forms конструируются из обычных свойств .NET.) Если вы хотите привязать свойство элемента управления Windows Forms к свойству элемента WPF, для этого существует несложный обходной маневр — просто установить свойство зависимости в элемент WPF и поправить при необходимости его BindingDirection (подробности читайте в главе 16). И, наконец, важно отметить, что вы можете привязать события к вашему элементу управления Windows Forms, используя знакомый синтаксис XAML. Ниже приведен пример, подключающий обработчик к событию MaskInputRejected, которое происходит, когда нажатие клавиши отклоняется из-за несоответствия маске:
Очевидно, что это — не маршрутизируемые события, так что вы не можете определить их на высших уровнях иерархии элементов. Когда случается событие, ваш обработчик отвечает на него, показывая сообщение об ошибке в другом элементе. В данном случае этим элементом будет метка WPF, расположенная где-то на форме: private void maskedTextBox_MaskInputRejected(object sender, System.Windows.Forms.MaskInputRejectedEventArgs e) { lblErrorText.Content = "Error: " + e.RejectionHint.ToString(); }
Совет. Не импортируйте пространства имен Windows Forms (такие как System.Windows.Forms) в файл кода, который уже использует пространства имен WPF (подобные System.Windows. Controls). Классы Windows Forms и классы WPF имеют немало совпадающих имен. Базовые ингредиенты (вроде Brush, Pen, Font, Color, Size и Point) и часто используемые элементы (Button, TextBox и т.п.) присутствуют в обеих библиотеках. Чтобы предотвратить конфликты имен, лучше в окно импортировать только один набор пространств имен (пространства имен WPF — для окна WPF, пространства Windows Forms — для форм), а для доступа к остальным использовать полные квалифицированные имена. Этот пример иллюстрирует самое замечательное свойство взаимодействия WPF и Windows Forms: оно никак не затрагивает ваш код. Манипулируете ли вы элементом управления Windows Forms или элементом WPF — вы всегда используете знакомый интерфейс класса для этого объекта. Слой взаимодействия — это просто волшебство, которое позволяет обоим ингредиентам сосуществовать в одном окне. Никакого дополнительного кода не требуется. На заметку! Для того чтобы элементы управления Windows Forms могли работать с более современными стилями, представленными в Windows XP, при запуске приложения вы можете вызвать EnableVisualStyles(), как было описано выше в разделе “Визуальные стили элементов управления Windows Forms” настоящей главы. Содержимое Windows Forms отображается механизмом Windows Forms, а не WPF. Поэтому свойства контейнера WindowsFormsHost, касающиеся отображения (такие
Book_Pro_WPF-2.indb 860
19.05.2008 18:11:39
Взаимодействие с Windows Forms
861
свойства, как Transform, Clip и Opacity), не оказывают влияния на то, что находится внутри него. Это значит, что даже если вы установите трансформацию вращения, область отсечения, и сделаете ваше содержимое полупрозрачным, то не увидите никаких изменений. К тому же Windows Forms использует другую систему координат для установки размеров элементов управления, используя физические пиксели. В результате этого, если вы увеличите системные установки DPI на своем компьютере, то содержимое WPF изменится на более детализированное, а компоненты Windows Forms — нет.
WPF и пользовательские элементы управления Windows Forms Одним из наиболее существенных ограничений элемента WindowsFormsHost является то, что он может содержать в себе только один элемент управления Windows Forms. Чтобы компенсировать этот недостаток, вы можете использовать элемент-контейнер Windows Forms. К сожалению, контейнер Windows Forms не поддерживает модели содержимого XAML, так что вам придется наполнять его программно. Намного лучший подход заключается в применении пользовательского элемента управления Windows Forms. Такой пользовательский элемент управления может быть определен в отдельной сборке, на которую вы будете ссылаться, или же вы можете добавить его непосредственно в проект WPF (используя знакомую команду AddNew Item). Это позволит совместить преимущества обоих миров — вы получите полную поддержку времени проектирования для построения пользовательского элемента управления наряду с простым способом интеграции его в окно WPF. Фактически пользовательский элемент управления предоставляет дополнительный уровень абстракции, подобный использованию отдельного окна. Это объясняется тем, что содержащее его окно WPF не может получить доступ к индивидуальным элементам, составляющим ваш пользовательский элемент управления. Вместо этого оно взаимодействует с высокоуровневыми свойствами, добавляемыми к пользовательскому элементу, которые могут модифицировать его изнутри. Это повышает степень инкапсуляции вашего кода и упрощает его, поскольку ограничивает точки взаимодействия между окном WPF и содержимым Windows Forms. Это также облегчает переход к чистому решению на основе WPF в будущем, посредством простой замены WindowsFormsHost пользовательским элементом управления WPF, имеющим те же свойства. (И, опять-таки, вы можете далее совершенствовать дизайн и повышать гибкость вашего приложения, перемещая пользовательский элемент управления в отдельную сборку библиотеки классов.) На заметку! Технически ваше окно WPF может обращаться к элементам внутри пользовательского элемента управления через его коллекцию Controls. Однако для того, чтобы использовать такой “черный ход”, вам придется написать чреватый ошибками код поиска определенного элемента по его строчному имени. Это всегда будет плохой идеей. Разрабатывая пользовательский элемент управления, имеет смысл максимально приблизить его поведение к содержимому WPF, чтобы было легко интегрировать его в вашу компоновку окна WPF. Например, вы можете решить воспользоваться контейнерными элементами управления FlowLayoutPanel и TableLayoutPanel, чтобы содержимое внутри вашего пользовательского элемента управления приспосабливалось к его размерам. Просто добавьте соответствующий элемент управления и установите его свойство Dock в DockStyle.Fill. Затем поместите в него нужные элементы управления. Подробнее об использовании элементов управления компоновкой Windows Forms (которые слегка отличаются от панелей компоновки WPF) читайте в моей книге Pro .NET 2.0 Windows Forms and Custom Controls in C# (Apress, 2005 г.).
Book_Pro_WPF-2.indb 861
19.05.2008 18:11:40
862
Глава 25
Взаимодействие с ActiveX WPF не обеспечивает прямую поддержку совместимости с ActiveX. Однако Windows Forms имеет такую поддержку в форме вызываемых оболочек времени выполнения (runtime callable wrappers — RCW), динамически генерируемых классов, позволяющих управляемому приложению Windows Forms размещать в себе компоненты ActiveX. Хотя существуют некоторые странности взаимодействия .NET c COM, которые могут разрушить некоторые элементы управления, такой подход работает достаточно хорошо в большинстве случаев, и работает незаметно, если тот, кто создает компонент, также предоставит сборку первичного взаимодействия (primary interop assembly), представляющую собой сделанную вручную, тонко настроенную RCW, которая исключает проблемы при взаимодействии. Но как это поможет, если вам нужно разрабатывать приложение WPF, использующее элемент управления ActiveX? В этом случае вам потребуется создать два слоя взаимодействия. Сначала вы помещаете элемент управления ActiveX в пользовательский элемент управления или форму Windows Forms. Затем вы помещаете пользовательский элемент управления в окно WPF или отображаете форму из приложения WPF.
Размещение элементов управления WPF в Windows Forms Обратный подход — размещение содержимого WPF в форме Windows Forms — столь же прост. В этой ситуации вам не нужен класс WindowsFormsHost. Вместо этого используется класс System.Windows.Forms.Integration.ElementHost — часть сборки WindowsFormsIntegration.dll. Класс ElementHost обладает способностью упаковывать любой элемент WPF.Однако ElementHost — действительный элемент управления Windows Forms, а это значит, что вы можете помещать его в вашу форму наряду с прочим содержимым Windows Forms. В некоторых отношениях ElementHost более прямолинеен, чем WindowsFormsHost, поскольку каждый элемент управления в Windows Forms отображается как отдельное окно со своим hwnd. Так что нет ничего сложного в том, что одно из этих окон будет отображаться механизмом WPF, а не User32/GDI+. Visual Studio предоставляет некоторую поддержку времени проектирования для элемента управления ElementHost, но только если вы поместите ваше содержимое WPF в пользовательский элемент управления WPF. Ниже описано, как это сделать. 1. Выполнить щелчок правой кнопкой на имени проекта в Solution Explorer и выбрать в контекстном меню команду AddNew Item (ДобавитьНовый элемент). Выбрать шаблон User Control (WPF), указать имя вашего класса компонента и щелкнуть на кнопке Add (Добавить). На заметку! Этот пример предполагает, что вы помещаете пользовательский элемент управления WPF в проект Windows Forms. Если у вас есть сложный пользовательский элемент управления, вы должны придерживаться более структурированного подхода к размещению его в отдельной сборке библиотеки классов. 2. Добавить нужные вам элементы управления WPF к новому пользовательскому элементу WPF. Visual Studio предоставляет обычный уровень поддержки времени проектирования для этого шага, так что вы можете перетаскивать элементы WPF из панели инструментов (Toolbox), конфигурировать их в окне Properties (Свойства) и т.п. 3. Закончив, перестройте ваш проект (выбрав в меню команду BuildBuild Solution (СборкаСборка решения)). Вы не можете использовать ваш пользовательский элемент управления WPF в форме до тех пор, пока не скомпилируете его.
Book_Pro_WPF-2.indb 862
19.05.2008 18:11:40
Взаимодействие с Windows Forms
863
4. Открыть форму Windows Forms, где хотите добавить ваш пользовательский элемент управления WPF (или создать новую форму щелчком правой кнопки на имени проекта в Solution Explorer и выбором в контекстном меню команды AddWindows Form (ДобавитьWindows-форма)). 5. Чтобы поместить пользовательский элемент WPF в форму, вам понадобится помощь элемента управления ElementHost. Элемент ElementHost появляется на вкладке WPF Interoperability панели инструментов (Toolbox). Перетащите его на вашу форму и установите нужные размеры. Совет. Для лучшего разделения будет хорошей идеей добавить ElementHost к определенному контейнеру вместо того, чтобы добавлять непосредственно в форму. Это облегчит отделение содержимого WPF от остальной части окна. Обычно вы будете использовать Panel, FlowLayoutPanel или TableLayoutPanel. 6. Чтобы выбрать содержимое ElementHost, используйте контекстную метку (смарттег). Если смарт-тег невидим, вы можете отобразить его, выбрав ElementHost и щелкнув на стрелке в правом верхнем углу. В этом смарт-теге вы найдете выпадающий список по имени Select Hosted Content (Выбрать содержимое). Используя этот список, вы можете выбрать необходимый пользовательский элемент управления WPF, как показано на рис. 25.5.
Рис. 25.5. Выбор WPF-содержимого для ElementHost 7. Хотя пользовательский элемент управления WPF появится в вашей форме, вы не сможете там редактировать его содержимое. Чтобы “перепрыгнуть” к соответствующему XAML-файлу, щелкните на ссылке Edit Hosted Content (Редактировать содержимое) в смарт-теге ElementHost. С технической точки зрения, ElementHost может содержать элемент WPF любого типа. Однако смарт-тег ElementHost ожидает, что вы выберете пользовательский элемент управления, имеющийся в вашем проекте (или сборке, на которую есть ссылка). Если вы хотите использовать элемент управления другого типа, вам понадобится написать код, который добавит его к ElementHost программно.
Book_Pro_WPF-2.indb 863
19.05.2008 18:11:40
864
Глава 25
Ключи доступа, мнемоники и фокус Взаимодействие WPF и Windows Forms работает, потому что оба типа содержимого могут быть тщательно разделены. Каждая область выполняет собственное отображение и обновление, и взаимодействует с мышью независимо. Однако такая изоляция не всегда желательна. Например, это приводит к потенциальным проблемам обработки клавиатуры, которая иногда должна выполняться глобально для всей формы в целом. Ниже перечислены некоторые примеры.
• Когда клавишей вы переходите от последнего элемента управления в одной области, то ожидаете, что фокус перейдет к первому элементу управления в следующей области.
• Когда вы используете горячую клавишу для обращения к элементу управления (такому как кнопка), то ожидаете, что он отреагирует, независимо от того, в какой области окна находится.
• Когда вы используете мнемонику метки, то ожидаете, что фокус перейдет к связанному с ней элементу управления.
• Аналогично, если вы подавляете нажатие клавиши, используя событие preview, то вам нужно, чтобы это работало в каждой области, независимо от того, какой элемент в данный момент находится в фокусе. Хорошей новостью является то, что такого поведения можно добиться без особых усилий. Например, рассмотрим окно WPF, представленное на рис. 25.6. Оно включает две кнопки WPF (верхнюю и нижнюю), а также одну кнопку Windows Forms (среднюю). Вот код разметки: Use Alt+_A Рис. 25.6. Три кнопки с горячими Use Alt+_C
На заметку! Синтаксис для идентификации клавиш-акселераторов в WPF слегка отличается (здесь используются подчеркивания) от принятого в Windows Forms (использующего символ &, который в XML должен быть представлен как &, потому что это специальный символ). Когда это окно появляется впервые, текст на всех кнопках нормальный. Когда пользователь нажимает и удерживает клавишу , все три символа активных клавиш подчеркиваются. Пользователь затем может инициировать нажатие любой из трех кнопок, нажав на клавиатуре , или (продолжая удерживать ). Тот же прием работает и с мнемониками, которые позволяют меткам передавать фокус ближайшему элементу управления (обычно — текстовому полю). Вы можете также использовать для перехода по трем кнопкам окна, как если бы они все были элементами управления WPF — сверху вниз. И, наконец, тот же пример продолжает
Book_Pro_WPF-2.indb 864
19.05.2008 18:11:40
Взаимодействие с Windows Forms
865
работать, если вы поместите комбинацию содержимого Windows Forms и WPF в форму Windows Forms. Поддержка клавиатуры не всегда так хороша, и вы можете столкнуться с некоторыми причудами при передаче фокуса. Ниже представлен список возможных проблем.
• Хотя WPF поддерживает систему передачи нажатий клавиш, чтобы каждый элемент управления имел шанс обработать клавиатурный ввод, модели обработки событий клавиатуры в WPF и Windows Forms отличаются. По этой причине вы не сможете принимать события клавиатуры от WindowsFormsHost, когда фокус находится внутри содержимого Windows Forms. Аналогично, если пользователь переходит от одного элемента управления к другому внутри WindowsFormsHost, вы не сможете получать события GotFocus и LostFocus из WindowsFormsHost. На заметку! Кстати, то же самое справедливо и в отношении событий мыши WPF. Например, событие MouseMove не будет возбуждено для WindowsFormsHost, пока вы перемещаете мышь в пределах его границ.
• Проверка достоверности Windows Forms не срабатывает, когда вы перемещаете фокус от элемента управления внутри WindowsFormsHost к элементу, находящемуся вне его. Вместо этого она инициируется, только когда вы переместитесь от одного элемента управления к другому внутри WindowsFormsHost. (Если вспомнить, что содержимое WPF и содержимое Windows Forms — по сути, отдельные окна, это выглядит совершенно оправданным, поскольку именно такого поведения можно ожидать при переключении между разными приложениями.)
• Если окно минимизируется в то время, когда фокус находится где-то в пределах WindowsFormsHost, то фокус может не быть восстановлен при восстановлении окна.
Отображение свойств Одной из наиболее неудобных деталей взаимодействия между WPF и Windows Forms является то, что они используют похожие, но отличающиеся свойства. Например, элементы управления WPF имеют свойство Background, позволяющее применять кисти для рисования фона. Элементы управления Windows Forms используют похожее свойство BackColor, заполняющее фон цветом на основе значения ARGB. Очевидно несоответствие между этими двумя свойствами, несмотря на то, что они часто применяются для установки одного и того же аспекта внешнего вида. По большей части это не представляет собой проблемы. Как разработчик, вы просто должны переключаться между двумя API-интерфейсами, в зависимости от того объекта, с которым работаете. Однако WPF немного облегчает вашу задачу, благодаря средству, называемому транслятором свойств. Трансляторы свойств не позволят вам писать код разметки в стиле WPF и заставлять его работать с элементами управления Windows Forms. Фактически трансляторы свойств несколько примитивнее. Они просто преобразуют несколько базовых свойств WindowsFormsHost (или ElementHost) из одной системы в другую, так чтобы их можно было применять к дочернему элементу управления. Например, если вы устанавливаете свойство WindowsFormsHost.IsEnabled, то свойство Enabled элемента управления, находящегося внутри, модифицируется соответствующим образом. Это не обязательное средство (вы можете сделать то же самое, модифицируя свойство Enabled дочернего элемента непосредственно, вместо того, чтобы работать со свойством IsEnabled контейнера), но часто это позволяет сделать ваш код яснее.
Book_Pro_WPF-2.indb 865
19.05.2008 18:11:40
866
Глава 25
Чтобы выполнить эту работу, классы WindowsFormsHost и ElementHost включают коллекцию PropertyMap, отвечающую за ассоциирование имени свойства с делегатом, который идентифицирует метод, выполняющий преобразование. Используя этот метод, система отображения свойств способна обработать такие преобразования, как BackColor в Background и наоборот. По умолчанию каждая такая коллекция наполняется стандартным набором ассоциаций (вы вольны создавать собственные или заменять существующие, но обычно такая возня на низком уровне не имеет особого смысла). В табл. 25.2 перечислены стандартные преобразования при отображении свойств, которые предоставляют классы WindowsFormsHost и ElementHost.
Таблица 25.2. Отображения свойств Свойство WPF
Свойство Windows Forms
Комментарии
Foreground
ForeColor
Преобразует любую кисть ColorBrush в соответствующий объект Color. В случае GradientBrush используется цвет GradientStop с минимальным значением смещения. Для любого другого типа кисти цвет ForeColor не изменяется, а применяется установленный по умолчанию.
Background
BackColor или BackgroundImage
Преобразует любую кисть SolidColorBrush в соответствующий объект Color. Прозрачность не поддерживается. Если используется какая-то более экзотичная кисть, то WindowsFormsHost создает битовую карту и присваивает ее вместо этого свойству BackgroundImage.
Cursor
Cursor
FlowDirection
RightToLeft
FontFamily, FontSize, FontStretch, FontStyle, FontWeight
Font
IsEnabled
Enabled
Padding
Padding
Visibility
Visible
Book_Pro_WPF-2.indb 866
Преобразует значение из перечисления Visibility в булевское значение. Если Visibility равно Hidden, то свойство Visible устанавливается в true, чтобы размер содержимого можно было использовать для вычислений компоновки, однако само содержимое WindowsFormsHost не рисует. Если Visibility равно Collapsed, то свойство Visible не изменяется (остается в текущем установленном значении или значении по умолчанию) и WindowsFormsHost содержимое не рисует.
19.05.2008 18:11:41
Взаимодействие с Windows Forms
867
На заметку! Отображения свойств работают динамически. Например, если свойство WindowsFormsHost.FontFamily изменяется, то конструируется объект Font и применяется к свойству Font дочернего элемента управления.
Взаимодействие с Win32 В настоящее время, когда Windows Forms достиг периода упадка, и никаких существенных усовершенствований для него не планируется, трудно поверить, что эта технология родилась на свет всего несколько лет назад. WPF не ограничивается взаимодействием только с приложениями Windows Forms — если вы хотите работать с Win32 API или помещать содержимое WPF в приложение C++ MFC, вы также можете это сделать. Вы можете разместить Win32 в WPF, используя класс System.Windows.Interop.HwndHost, работающий аналогично классу WindowsFormsHost. Те же ограничения, которые присущи WindowsFormsHost, характерны и для HwndHost (например, правило зазора, причуды передачи фокуса и т.п.). На самом деле WindowsFormsHost наследуется от HwndHost.
HwndHost — это ворота в традиционный мир приложений C++ и MFC. Однако он также позволяет вам интегрировать управляемое содержимое DirectX. В настоящее время WPF не включает средств взаимодействия с DirectX, и вы не можете использовать библиотеки DirectX для отображения содержимого в окне WPF. Однако можно применить DirectX для построения отдельного окна и затем поместить его внутри окна WPF с помощью HwndHost. Хотя рассмотрение DirectX выходит за рамки настоящей книги (и в связи с важностью для нас более сложного программирования в WPF), вы можете загрузить управляемые библиотеки DirectX по адресу http://msdn.microsoft.com/directx. Дополнением класса HwndHost является класс HwndSource. В то время как HwndHost позволяет поместить любой hwnd в окно WPF, HwndSource упаковывает любой визуальный компонент или элемент WPF в hwnd, так что его можно вставить в приложение на базе Win32 вроде приложения MFC. Единственное условие — ваше приложение должно иметь доступ к библиотекам WPF, состоящим из управляемого кода .NET. Это нетривиальная задача. Если вы используете приложение C++, то простейшим подходом будет применение Managed Extensions for C++ (Управляемые расширения C++). Тогда вы можете создавать содержимое WPF, упаковывать его в HwndSource, устанавливать свойство HwndHost.RootVisual на элемент верхнего уровня и затем помещать HwndSource в окно. При желании вы можете найти все необходимое для построения сложных интегрированных проектов и адаптации унаследованного кода в Internet, а также в справочной системе Visual Studio.
Резюме В этой главе была рассмотрена поддержка взаимодействия, позволяющая приложениям WPF отображать содержимое Windows Forms (и наоборот). Затем вы ознакомились с элементом WindowsFormsHost, позволяющим встраивать элемент управления Windows Forms в окно WPF, а также с ElementHost, позволяющим вставлять элемент WPF в форму. Оба эти класса предоставляют простой и эффективный способ управления переходом от Windows Forms к WPF.
Book_Pro_WPF-2.indb 867
19.05.2008 18:11:41
ГЛАВА
26
Многопоточность и дополнения К
ак вам известно из предыдущих глав, каркас WPF революционизировал почти все основы программирования под Windows. Он предложил новый подход ко всему — от определения содержимого окна до отображения трехмерной графики. WPF даже представил новые концепции, которые не являются очевидно сосредоточенными на пользовательском интерфейсе, такие как свойства зависимостей и маршрутизируемые события. Конечно, огромное количество задач кодирования выходят за рамки программирования пользовательского интерфейса, а потому они не подверглись изменениям в мире WPF. Например, приложения WPF используют те же классы, что и другие приложения .NET, при подключении к базам данных, манипуляциях с файлами и выполнении диагностики. Также несколько средств попадают в область, лежащую где-то между традиционным программированием .NET и WPF. Эти средства не ограничены строго рамками WPF-приложений, но имеют некоторые специфичные для WPF особенности. В этой главе вы увидите два замечательных примера. Для начала, мы поговорим о многопоточности, которая позволяет вашему приложению WPF выполнять фоновую работу, сохраняя отзывчивый пользовательский интерфейс. Чтобы спроектировать безопасное и стабильное многопоточное приложение, вам нужно понимать правила многопоточности WFP. Затем вы познакомитесь с новой моделью дополнений (add-in model), которая позволит вашему WPF-приложению динамически загружать и использовать отдельно компилированные компоненты с полезными кусочками функциональности.
На заметку! Как многопоточность, так и модель дополнений сами по себе являются обширными темами, которым можно посвящать целые книги; поэтому в этой главе мы не станем слишком углубляться в эти средства. Однако вы получите некоторый базовый фундамент, которые пригодится вам для их применения с WPF, и который послужит надежной основой для дальнейшего изучения.
Многопоточность Многопоточность — это искусство выполнения более чем одного куска кода одновременно. Целью многопоточности обычно является создание более отзывчивого интерфейса — такого, который не “замораживается” посреди работы, — хотя вы можете применять многопоточность также для того, чтобы полнее задействовать преимущества двухядерного процессора при выполнении ресурсоемких алгоритмов или другой работы
Book_Pro_WPF-2.indb 868
19.05.2008 18:11:41
Многопоточность и дополнения
869
одновременно с некоторой длительной операцией (например, чтобы выполнять некоторые вычисления в процессе ожидания ответа от Web-службы). В самом начале проектирования WPF его разработчики предусмотрели новую модель многопоточности. Эта модель, называемая арендой потоков (thread rental), позволила обращаться к объектам пользовательского интерфейса из любого потока. Чтобы сократить стоимость блокировки, группы взаимосвязанных объектов могут объединяться под одной блокировкой (называемой контекстом). К сожалению, такой дизайн усложнил однопоточные приложения (которые должны учитывать контекст) и затруднил взаимодействие с унаследованным кодом (вроде Win32 API). В конечном итоге от этого подхода пришлось отказаться. В результате теперь WPF поддерживает модель однопоточного апартамента (single-threaded apartment), которая очень похожа на ту, что используется в приложениях Windows Forms. С этой моделью связано несколько основных правил.
• Элементы WPF обладают потоковым родством (thread affinity). Поток, который создает их, владеет ими, и другие потоки не могут взаимодействовать с ними напрямую. (Элемент — это объект WPF, отображаемый в окне.)
• Объекты WPF, обладающие потоковым родством, наследуются от DispatcherObject в некоторой точке их иерархии классов. DispatcherObject включает небольшой набор членов, которые позволяют верифицировать, выполняется ли код в правильном потоке, чтобы использовать определенный объект, и (если нет) переключаться на другой поток.
• На практике один поток выполняет все ваше приложение и владеет объектами WPF. Хотя вы можете использовать отдельные потоки, чтобы отображать отдельные окна, такой дизайн встречается редко. В следующих разделах вы познакомитесь с классом DispatcherObject и изучите простейший способ выполнения асинхронных операций в приложении WPF.
Класс Dispatcher Диспетчер управляет работой, происходящей в приложении WPF. Диспетчер владеет потоком приложения и управляет очередью элементов работы. При работе вашего приложения диспетчер принимает новые запросы работы и выполняет их по одному. Технически диспетчер создается при первоначальном создании в новом потоке экземпляра класса, который наследуется от DispatcherObject. Если вы создаете отдельные потоки и используете их для отображения отдельных окон, то получаете более одного диспетчера. Однако большинство приложений не усложняют картину и обходятся одним потоком пользовательского интерфейса и одним диспетчером. Затем они используют многопоточность для управления операциями с данными и другими фоновыми задачами. На заметку! Диспетчер — это экземпляр класса System.Windows.Threading.Dispatcher. Все связанные с диспетчером объекты также находятся в пространстве имен System. Windows.Threading, которое является новым для WPF. (Центральные классы для организации потоков, которые существуют со времен .NET 1.0, находятся в пространстве System. Threading.) Вы можете получить диспетчер для текущего потока, используя статическое свойство Dispatcher.CurrentDispatcher. Используя объект Dispatcher, вы можете присоединить обработчики событий, которые отвечают за необработанные исключения или реагируют на выключение диспетчера. Вы можете также получить ссылку на поток
Book_Pro_WPF-2.indb 869
19.05.2008 18:11:41
870
Глава 26
System.Threading.Thread, которым управляет диспетчер, остановить диспетчер или направить код на выполнение правильному потоку (прием, который будет показан в следующем разделе).
Класс DispatcherObject Большую часть времени вы не будете взаимодействовать с диспетчером напрямую. Однако вам придется немало времени тратить на использование экземпляров DispatcherObject, потому что каждый визуальный объект WPF наследуется от этого класса. DispatcherObject — это просто объект, привязанный к диспетчеру. Другими словами — объект, привязанный к потоку диспетчера. DispatcherObject имеет всего три члена, которые перечислены в табл. 26.1.
Таблица 26.1. Члены класса DispatherObject Наименование
Описание
Dispather
Возвращает диспетчер, управляющий данным объектом.
CheckAccess()
Возвращает true, если код находится в правильном потоке для использования этого объекта; в противном случае возвращает false.
VerifyAccess()
Ничего не делает, если код находится в правильном потоке для использования этого объекта; в противном случае генерирует исключение InvalidOperationException.
Объекты WPF часто вызывают VerifyAccess(), чтобы защитить себя. Они не вызывают VerifyAccess() в ответ на каждую операцию (потому что это было бы слишком накладно по производительности), но вызывают этот метод достаточно часто, чтобы было маловероятно долго использовать объект из неверного потока. Например, следующий код реагирует на щелчок кнопки, создавая новый объект System.Threading.Thread. Затем он использует этот поток для вызова небольшого кусочка кода, который изменяет текстовое поле в текущем окне. private void cmdBreakRules_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(UpdateTextWrong); thread.Start(); } private void UpdateTextWrong() { // Эмулирует некоторую работу посредством пятисекундной задержки. Thread.Sleep(TimeSpan.FromSeconds(5)); txt.Text = "Here is some new text."; }
Этот код специально задуман так, чтобы выдать сбой. Метод UpdateTextWrong() будет выполнен в новом потоке, которому не разрешен доступ к объектам WPF. В этом случае объект TextBox перехватывает нарушение вызовом VerifyAccess(), при этом генерируя исключение InvalidOperationException. Чтобы исправить этот код, вы должны получить ссылку на диспетчер, который владеет объектом TextBox (тот же диспетчер, что владеет окном и всеми прочими объектами WPF в приложении). Получив доступ к этому диспетчеру, вы можете вызывать Dispatcher.BeginInvoke(), чтобы маршализировать некоторый код потоку диспетчера. По сути, BeginInvoke() планирует ваш код в качестве задачи для диспетчера. Затем диспетчер выполняет этот код.
Book_Pro_WPF-2.indb 870
19.05.2008 18:11:41
Многопоточность и дополнения
871
Ниже показан корректный код: private void cmdFollowRules_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(UpdateTextRight); thread.Start(); } private void UpdateTextRight() { // Симулирует некоторую работу, происходящую с пятисекундной задержкой. Thread.Sleep(TimeSpan.FromSeconds(5)); // Получить диспетчер от текущего окна и использовать // его для вызова кода обновления. this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart) delegate() { txt.Text = "Here is some new text."; } ); }
Метод Dispatcher.BeginInvoke() принимает два параметра. Первый указывает свойство задачи. В большинстве случаев вы будете применять DispatcherPriority. Normal, но вы можете также использовать более низкий приоритет, если у вас есть задача, которая не обязательно должна быть завершена немедленно, и которую можно отложить до того момента, когда диспетчеру нечего будет делать. Например, это может иметь смысл, если вам нужно отобразить сообщение о состоянии длительно выполняющейся операции где-то в вашем пользовательском интерфейсе. Вы можете использовать DispatcherPriority.ApplicationIdle, чтобы подождать, когда приложение завершит всю прочую работу, либо еще более “сдержанный” метод DispatcherPriority. SystemIdle, чтобы подождать, пока вся система не придет в состояние ожидания, и центральный процессор не будет простаивать. Вы можете также использовать пониженный приоритет, чтобы отвлечь внимание диспетчера на что-то другое. Однако рекомендуется оставлять высокие приоритеты для событий ввода (таких как нажатия клавиш). Они должны обрабатываться почти постоянно, или же возникнет впечатление, что приложение несколько медлительно. С другой стороны, добавление нескольких миллисекунд ко времени выполнения фоновой операции не будет заметно, так что приоритет DispatcherPriority.Normal более оправдан в такой ситуации. Второй параметр BeginInvoke() — это делегат, указывающий на метод с кодом, который вы хотите выполнить. Этот метод может быть где-то в другом месте вашего кода, или же вы можете определить его встроенным (как в этом примере). Подход на основе встроенного кода хорош для простых операций, таких как обновление в одной строке. Однако если вам нужно использовать более сложный процесс для обновления пользовательского интерфейса, лучше будет вынести такой код в отдельный метод. На заметку! Метод BeginInvoke() также возвращает значение, которое в данном примере не используется. BeginInvoke() возвращает объект DispatcherOperation, который позволяет вам получить состояние вашей операции маршализации и определить, когда ваш код действительно было выполнен. Однако DispatcherOperation используется редко, потому что код, который вы передаете BeginInvoke(), должен занимать очень небольшое время. Помните, если вы выполняете длительную фоновую операцию, то вам нужно выполнять ее в отдельном потоке, а затем маршализировать результат потоку диспетчера (и в этот момент вы обновите пользовательский интерфейс, чтобы изменить разделяемый
Book_Pro_WPF-2.indb 871
19.05.2008 18:11:41
872
Глава 26
объект). Не имеет смысла выполнять ваш длительно работающий код в методе, который вы передаете BeginInvoke(). Например, приведенный ниже слегка реорганизованный код работает, но менее практичен: private void UpdateTextRight() { // Получить диспетчер от текущего окна. this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart) delegate() { // Симуляция некоторой длительной работы. Thread.Sleep(TimeSpan.FromSeconds(5)); txt.Text = "Here is some new text."; } ); }
Здесь проблема заключается в том, что вся работа происходит в потоке диспетчера. Это значит, что код займет диспетчер примерно так же, как это происходило бы в немногопоточном приложении. На заметку! Диспетчер также предоставляет метод Invoke(). Подобно BeginInvoke(), он маршализирует специфицированный вами код потоку диспетчера. Но в отличие от BeginInvoke(), Invoke() останавливает ваш поток до тех пор, пока диспетчер выполняет ваш код. Вы можете использовать Invoke(), если вам нужно приостановить асинхронную операцию до тех пор, пока какой-то отклик не поступит от пользователя. Например, вы можете вызвать Invoke() для запуска кусочка кода, отображающего диалоговое окно OK/Cancel. После того, как пользователь щелкнет на кнопке и ваш маршализируемый код завершится, Invoke() вернет управление, и вы сможете продолжить работу в соответствии с ответом пользователя.
Класс BackgroundWorker Вы можете выполнять асинхронные операции разными способами. Вы уже видели один бесхитростный подход — создание нового объекта System.Threading.Thread вручную, применение вашего асинхронного кода и запуск его методом Thread.Start(). Это мощный подход, потому что объект Thread не удерживает никакого возврата. Вы можете создавать десятки потоков, устанавливать их приоритеты, контролировать их состояние (например, приостанавливать, возобновлять или прерывать их) и т.д. Однако этот подход также представляет некоторую опасность. Если вы обращаетесь к разделяемым данным, вам нужно применять блокировку для предотвращения тонких ошибок. Если вы создаете потоки часто или в большом количестве, то тем самым вызываете дополнительные излишние накладные расходы. Приемы написания хорошего многопоточного кода и используемые при этом классы .NET не являются специфичными для WPF. Если вы пишете многопоточный код в приложении Windows Forms, то можете использовать ту же технику и в мире WPF. В оставшейся части этой главы мы с вами рассмотрим один из наиболее простых и безопасных подходов: компонент System.ComponentModel.BacgroundWorker. Совет. Чтобы увидеть разные подходы — от простейшего до наиболее сложного, обратитесь к моей книге Programming .NET 2.0 Windows Forms and Custom Controls in C# (Apress, 2005 г.).
Book_Pro_WPF-2.indb 872
19.05.2008 18:11:41
Многопоточность и дополнения
873
Класс BackgroundWorker был представлен в .NET 2.0 для упрощения работы с потоками в приложениях Windows Forms. Однако BackgroundWorker в той же мере применим и в WPF. Компонент BackgroundWorker предоставляет вам почти идеальный способ запуска длительно выполняющихся задач в отдельном потоке. Он использует диспетчер “за кулисами” и абстрагирует сложности маршализации посредством модели событий. Как вы убедитесь, BackgroundWorker также поддерживает два дополнительных удобства: события продвижения и сообщения отмены. В обоих случаях детали многопоточности скрыты, что облегчает кодирование. На заметку! BackgroundWorker незаменим, если у вас есть единственная асинхронная задача, которая выполняется в фоновом режиме от начала до конца (с необязательной поддержкой извещения о продвижении и возможностью отмены). Если же вы имеете в виду что-то еще, например, асинхронную задачу, которая работает на протяжении всей жизни вашего приложения, или асинхронную задачу, взаимодействующую с вашим приложением, пока оно выполняет свою работу, то вам придется спроектировать специальное решение, воспользовавшись поддержкой многопоточности .NET.
Простая асинхронная операция Чтобы попробовать BackgroundWorker в действии, стоит рассмотреть пример приложения. Базовый ингредиент любого испытания — длительно выполняющийся процесс. В следующем примере используется распространенный алгоритм нахождения простых чисел в заданном диапазоне, называемый решетом Эратосфена, который был изобретен Эратосфеном примерно в 240 г. до нашей эры. В соответствие с этим алгоритмом, вы начинаете с составления списка всех целых числе в заданном диапазоне. Затем вычеркиваете все числа, кратные всем простым числам, меньшим или равным квадратному корню из максимального числа. Оставшиеся числа и будут простыми. В этом примере я не хочу углубляться в теорию, которая доказывает работоспособность решета Эратосфена, либо показывать тривиальный код, реализующий его. (Также не беспокойтесь об оптимизации или сравнении с другими приемами.) Однако вы увидите, как асинхронно выполнить алгоритм решета Эратосфена. Полный код доступен в онлайновых примерах, сопровождающих эту главу. Он принимает следующую форму: public class Worker { public static int[] FindPrimes(int fromNumber, int toNumber) { // Найти простые числа между fromNumber и toNumber, // и вернуть их в виде массива целых. } }
Метод FindPrimes() принимает два параметра, которые ограничивают диапазон чисел. Затем код возвращает массив целых чисел, содержащий все простые числа из заданного диапазона. На рис. 26.1 показан пример, который мы строим. Окно позволяет пользователю выбрать диапазон чисел. Когда пользователь щелкает на кнопке Find Primes (Найти простые числа), поиск начинается, но происходит в фоновом режиме. По завершении поиска перечень простых чисел появляется в окне списка.
Book_Pro_WPF-2.indb 873
19.05.2008 18:11:42
874
Глава 26
Рис. 26.1. Завершенный поиск простых чисел
Создание BackgroundWorker Чтобы использовать BackgroundWorker, начните с создания его экземпляра. При этом у вас есть два выбора.
• Вы можете создать BackgroundWorker в вашем коде и присоединить программно все обработчики событий.
• Вы можете объявить BackgroundWorker в вашем XAML-коде. Преимущество такого подхода в том, что вы можете присоединить обработчики событий, используя атрибуты. Поскольку BackgroundWorker не является видимым элементом WPF, вы не можете поместить его куда угодно. Вместо этого вам нужно объявить его в качестве ресурса для вашего окна. (О ресурсах читайте в главе 11.) Оба подхода эквивалентны. В загружаемом примере для этой главы используется второй подход. Первый шаг — обеспечить доступ к пространству имен System. ComponentModel в вашем документе XAML через импорт. Чтобы сделать это, вам нужно использовать технику отображения пространства имен, знакомую по главе 2:
Теперь вы можете создать экземпляр BackgroundWorker в коллекции Windows. Resources. Делая это, вы должны указывать ключевое имя, чтобы можно было позднее извлечь этот объект. В данном примере ключевое имя — backgroundWorker:
Преимущество объявления BackgroundWorker в разделе Window.Resources заключается в том, что вы можете установить его свойства и присоединить обработчики событий посредством атрибутов. Например, ниже приведен дескриптор BackgroundWorker, который у вас получится в конце примера, включающий поддержку уведомления о про-
Book_Pro_WPF-2.indb 874
19.05.2008 18:11:42
Многопоточность и дополнения
875
хождении и возможности отмены, и присоединяющий обработчики событий к DoWork, ProgressChanged и RunWorkerCompleted.
Чтобы получить доступ к этому ресурсу в вашем коде, вам нужно поместить его в коллекцию Resources. В данном примере окно выполняет этот шаг в его конструкторе, так что весь ваш код обработки событий легко доступен: public partial class BackgroundWorkerTest : Window { private BackgroundWorker backgroundWorker; public BackgroundWorkerTest() { InitializeComponent(); backgroundWorker = ((BackgroundWorker)this.FindResource("backgroundWorker")); } ... }
На заметку! Вы узнаете больше о коллекции Resources из главы 11.
Запуск BackgroundWorker Первый шаг к использованию BackgroundWorker с примером поиска простых чисел состоит в создании специального класса, который позволит вам передать входные параметры BackgroundWorker. Когда вы вызываете BackgroundWorker.RunWorkerAsync(), то можете указать любой объект, который будет доставлен событию DoWork. Однако вы можете указать только один объект, поэтому вам придется упаковать числа from и to в один класс, как показано ниже: public class FindPrimesInput { public int From { get; set; } public int To { get; set; } public FindPrimesInput(int from, int to) { From = from; To = to; } }
Чтобы запустить сам BackgroundWorker, вам нужно вызвать метод Background Worker.RunWorkerAsync() и передать объект FindPrimesInput. Приведем код, который делает это, когда пользователь щелкает кнопку Find Primes: private void cmdFind_Click(object sender, RoutedEventArgs e) { // Сделать недоступной эту кнопку и очистить предыдущие результаты. cmdFind.IsEnabled = false; cmdCancel.IsEnabled = true; lstPrimes.Items.Clear();
Book_Pro_WPF-2.indb 875
19.05.2008 18:11:42
876
Глава 26 // Получить диапазон поиска. int from, to; if (!Int32.TryParse(txtFrom.Text, out from)) { MessageBox.Show("Invalid From value."); // неверное значение From return; } if (!Int32.TryParse(txtTo.Text, out to)) { MessageBox.Show("Invalid To value."); // неверное значение To return; } // Начать поиск простых чисел в другом потоке. FindPrimesInput input = new FindPrimesInput(from, to); backgroundWorker.RunWorkerAsync(input);
}
Когда ваш BackgroundWorker начинает работу, он захватывает свободный поток из пула потоков CLR и затем инициирует событие DoWork из этого потока. Вы обрабатываете событие DoWork и запускаете вашу длительную задачу. Однако вам следует быть осторожным и не обращаться к разделяемым данным (таким как поля вашего класса окна) или объектам пользовательского интерфейса. Как только работа будет завершена, BackgroundWorker инициирует событие RunWorkerCompleted, чтобы известить ваше приложение. Это событие инициируется в потоке диспетчера, что позволит вам обратиться к разделяемым данным и вашему пользовательскому интерфейсу, не порождая никаких проблем. Как только BackgroundWorker захватывает поток, он инициирует событие DoWork. Вы можете обработать это событие, вызвав метод Worker.FindPrimes(). Событие DoWork представляет объект DoWorkEventArgs, который является ключевым ингредиентом при извлечении и возврате информации. Вы извлекаете входной объект через свойство DoWorkEventArgs.Argument и возвращаете результат, устанавливая свойство DoWorkEventArgs.Result. private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // Получить входные значения. FindPrimesInput input = (FindPrimesInput)e.Argument; // Запустить поиск простых чисел и ждать. // Это длительная часть работы, но она не подвешивает // пользовательский интерфейс, поскольку выполняется в другом потоке. int[] primes = Worker.FindPrimes(input.From, input.To); // Вернуть результат. e.Result = primes; }
По завершении метода BackgroundWorker инициирует RunWorkerCompletedEventArgs в потоке диспетчера. В этой точке вы можете извлечь результат из свойства RunWorker CompletedEventArgs.Result. Затем вы можете обновить интерфейс и обратиться к переменным уровня окна без опаски. private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { // Ошибка была сгенерирована обработчиком события DoWork. MessageBox.Show(e.Error.Message, "An Error Occurred"); }
Book_Pro_WPF-2.indb 876
19.05.2008 18:11:42
Многопоточность и дополнения
877
else { int[] primes = (int[])e.Result; foreach (int prime in primes) { lstPrimes.Items.Add(prime); } } cmdFind.IsEnabled = true; cmdCancel.IsEnabled = false; progressBar.Value = 0; }
Обратите внимание, что вам не понадобился никакой код блокировки, и не было необходимости в использовании метода Dispatcher.BeginInvoke(). Объект BackgroundWorker обо всем позаботился сам. “За кулисами” BackgroundWorker использует несколько многопоточных классов, которые были представлены в .NET 2.0, включая AsyncOperationManager , AsyncOperation и SynchronizationContext. По сути, BackgroundWorker применяет AsyncOperationManager для управления фоновой задачей. AsyncOperationManager обладает встроенным интеллектом, а именно: он способен получить контекст синхронизации для текущего потока. В приложении Windows Forms AsyncOperationManager получает объект WindowsFormsSynchronizationContext, в то время как приложение WPF получает объект DispatcherSynchronizationContext. Концептуально эти классы выполняют одинаковую работу, но их внутреннее устройство отличается.
Отслеживание продвижения BackgroundWorker также предоставляет встроенную поддержку первоначальной установки свойства BackgroundWorker.WorkerReportsProgress в true. На самом деле предоставление и отображение информации о продвижении — двухшаговый процесс. Вопервых, коду обработки события DoWork необходимо вызвать метод BackgroundWorker. ReportProgress() и показать предполагаемый процент готовности (от 0% до 100%). Вы можете делать это редко или же часто — как вам нравится. При каждом вызове ReportProgress() объект BackgroundWorker инициирует событие ProgressChanged. Вы можете отреагировать на это событие, чтобы прочитать процент готовности и обновить пользовательский интерфейс. Поскольку событие ProgressChanged инициировано в потоке пользовательского интерфейса, в применении Dispatcher.BeginInvoke() нет необходимости. Метод FindPrimes() сообщает о продвижении с приращением 1%, используя код вроде следующего: int iteration = list.Length / 100; for (int i = 0; i < list.Length; i++) { ... // Сообщить о продвижении, только если есть изменение в 1%. // Также не нужно выполнять вычисление, если нет // BackgroundWorker или если он не поддерживает // уведомления о продвижении. if ((i % iteration == 0) && (backgroundWorker != null) && backgroundWorker.WorkerReportsProgress) { backgroundWorker.ReportProgress(i / iteration); } }
Book_Pro_WPF-2.indb 877
19.05.2008 18:11:42
878
Глава 26
Как только вы установите свойство BackgroundWorker.WorkerReportsProgress, с этого момента можете реагировать на уведомления о продвижении, обрабатывая событие ProgressChanged. В этом примере индикатор продвижения обновляется соответствующим образом: private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar.Value = e.ProgressPercentage; }
На рис. 26.2 показан индикатор продвижения.
Рис. 26.2. Отслеживание продвижения асинхронной задачи
Поддержка отмены Столь же просто добавить поддержку отмены длительно выполняющейся задачи посредством BackgroundWorker. Первый шаг — установка свойства BackgroundWorker. WorkerSupportsCancellation. Чтобы запросить отмену, ваш код должен вызвать метод BackgroundWorker. CancelAsync(). В этом примере отмена запрашивается при щелчке по кнопке Cancel (Отмена): private void cmdCancel_Click(object sender, RoutedEventArgs e) { backgroundWorker.CancelAsync(); }
Когда вы вызываете CancelAsync(), ничего автоматически не происходит. Вместо этого код, выполняющий задачу, должен явно проверить запрос на отмену, выполнить необходимую очистку и вернуть управление. Ниже приведен код метода FindPrimes(), который проверяет запросы на отмену непосредственно перед сообщением о продвижении: for (int i = 0; i < list.Length; i++) { ... if ((i % iteration) && (backgroundWorker != null))
Book_Pro_WPF-2.indb 878
19.05.2008 18:11:42
Многопоточность и дополнения
879
{ if (backgroundWorker.CancellationPending) { // Вернуть управления, не делая более никаой работы. return; } if (backgroundWorker.WorkerReportsProgress) { backgroundWorker.ReportProgress(i / iteration); } } }
Код в вашем обработчике события DoWork также должен явно установить свойство DoWorkEventArgs.Cancel в true, чтобы завершить отмену. Затем вы возвращаете управление из метода, не пытаясь более строить строку простых чисел. private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { FindPrimesInput input = (FindPrimesInput)e.Argument; int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker); if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } // Вернуть результат. e.Result = primes; }
Даже когда вы отменяете операцию, событие RunWorkerCompleted все равно инициируется. В этой точке вы можете проверить, отменена ли задача, и обрабатывать его соответственно. private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) { MessageBox.Show("Search cancelled."); } else if (e.Error != null) { // Обработчиком события DoWork была сгенерирована ошибка. MessageBox.Show(e.Error.Message, "An Error Occurred"); } else { int[] primes = (int[])e.Result; foreach (int prime in primes) { lstPrimes.Items.Add(prime); } } cmdFind.IsEnabled = true; cmdCancel.IsEnabled = false; progressBar.Value = 0; }
Book_Pro_WPF-2.indb 879
19.05.2008 18:11:42
880
Глава 26
Теперь компонент BackgroundWorker позволяет запускать поиск и останавливать его принудительно.
Дополнения приложений Дополнения (add-ins, также называемые plug-ins (подключаемые модули)) — это отдельно компилируемые компоненты, которые ваше приложение может находить, загружать и использовать динамически. Часто приложение спроектировано с учетом использования дополнений, так что может быть расширено в будущем без необходимости в его модификации, перекомпиляции и повторном тестировании. Дополнения также дают вам гибкость в настройке отдельных экземпляров приложения для определенного рынка или клиента. Но наиболее частой причиной для использования модели дополнений является необходимость позволить независимым разработчикам расширять функциональность вашего приложения. Например, дополнения к Adobe Photoshop представляют широкий набор эффектов обработки изображений. Дополнения к Firefox предоставляют расширенные средства Web-серфинга и совершенно новую функциональность. В обоих случаях такие дополнения создаются независимыми разработчиками. Со времен .NET 1.0 разработчики были вынуждены разрабатывать собственные системы дополнений. Два базовых ингредиента таких систем — это интерфейсы (которые позволяют вам определять контракты, через которые приложение взаимодействует с дополнением, а дополнение — с приложением) и рефлексия (которая позволяет вашему приложению динамически исследовать и загружать дополнительные типы из отдельной сборки). Однако построение системы дополнений с нуля требует значительного объема работы. Вам нужно отделить способ нахождения дополнений, и вам нужно гарантировать правильное управление ими (другими словами, что они выполняются в ограниченном контексте безопасности и могут быть выгружены при необходимости). Версия .NET 3.5 представила встроенную модель дополнений, использующую ту же инфраструктуру интерфейсов и рефлексии. Ключевое преимущество этой модели дополнений состоит в том, что вам не придется самостоятельно реализовывать такие вещи, как исследование (discovery). Ключевой же недостаток данной модели — ее сложность. Проектировщики .NET позаботились о том, чтобы сделать модель дополнений достаточно гибкой, чтобы обрабатывать огромный диапазон сценариев поддержки версий и размещения. В конечном итоге получилось, что вы должны создавать как минимум семь (!) отдельных компонентов, чтобы реализовать модель дополнений в приложении, даже если вы и не собираетесь использовать ее наиболее изощренные средства.
Канал дополнений Сердцем модели дополнений является канал (pipeline) дополнений, представляющий собой цепь компонентов, которые позволяют принимающему (хост) приложению взаимодействовать с дополнением (рис. 26.3). На одном конце этого канала находится принимающее приложение. На другом конце — дополнение. Между ними находятся пять компонентов, отвечающих за взаимодействие. На первый взгляд эта модель кажется несколько перегруженной. Более простой сценарий мог бы помещать единственный слой (контракт) между приложением и дополнением. Однако дополнительные слои (представления и адаптеры) позволяют модели дополнений быть более гибкой в определенных ситуациях (как описано во врезке “Более развитые адаптеры”).
Book_Pro_WPF-2.indb 880
19.05.2008 18:11:43
Многопоточность и дополнения
881
Граница домена приложения
Приложение\ хост (EXE)
Адаптер стороны хоста (DLL)
Представ\ ление хоста (DLL)
Адаптер стороны дополнения (DLL)
Дополнение (DLL)
Представление дополнения (DLL)
Контракт (DLL)
Рис. 26.3. Взаимодействие по каналу дополнений
Как работает канал Контракт — это краеугольный камень канала дополнений. Он включает в себя один или более интерфейсов, которые определяют, как принимающее приложение может взаимодействовать с дополнениями и как дополнения могут взаимодействовать с принимающим приложением. Сборка контракта может также включать специальные сериализуемые типы, которые вы планируете использовать для передачи данных между принимающим приложением и дополнением. Канал дополнений спроектирован с учетом необходимой расширяемости и гибкости. И по этой причине принимающее приложение и дополнения не используют контракт напрямую. Вместо этого они применяют свои собственные соответствующие версии контракта, называемые представлениями (views). Принимающее приложение использует представление хоста, в то время как дополнение — представление дополнения. Обычно представление включает абстрактные классы, которые соответствуют интерфейсам в контракте. Хотя обычно они достаточно похожи, контракты и представления полностью независимы. Соединить эти две части вместе — задача адаптеров. Адаптеры выполняют это соединение, представляя классы, которые одновременно наследуются от классов представлений и реализуют интерфейсы контракта. На рис. 26.4 показан этот дизайн.
Класс адаптера стороны хоста
Принимающее приложение
наследует
Абстрактный класс представления хоста
Класс адаптера стороны дополнения
реализует
Класс дополнения
реализует
Интерфейс контракта
наследует
Абстрактный класс представления дополнения
Рис. 26.4. Отношения классов в канале
Book_Pro_WPF-2.indb 881
19.05.2008 18:11:43
882
Глава 26
По сути, адаптеры заполняют пробел между представлениями и интерфейсом контракта. Они отображают вызовы представления на вызовы интерфейса контракта. Также они отображают вызовы интерфейса контракта на соответствующий метод представления. Это несколько усложняет дизайн, но добавляет дополнительный уровень гибкости. Чтобы понять, как работают адаптеры, рассмотрим, что происходит, когда приложение использует дополнение. Во-первых, принимающее приложение вызывает один из методов в представлении вида. Но помните, что представление хоста — абстрактный класс. “За кулисами” приложение на самом деле вызывает метод хост-адаптера через представление хоста. (Это возможно, поскольку класс адаптера хоста наследуется от класса представления хоста.) Хост-адаптер затем вызывает соответствующий метод в интерфейсе контракта, который реализован адаптером дополнения. И, наконец, адаптер дополнения вызывает метод в представлении дополнения. Этот метод реализован дополнением, которое и выполняет реальную работу.
Более развитые адаптеры Если у вас нет особых потребностей в поддержке версий или хостинге, то адаптеры будут совершенно прямолинейны. Они просто передают работу по каналу. Однако адаптеры также являются важной точкой расширяемости для более изощренных сценариев. Примером может служить поддержка версий. Очевидно, что вы можете независимо обновлять приложение или его дополнения, не меняя способа их взаимодействия — до тех пор, пока используете одни и те же интерфейсы в контракте. Однако в некоторых случаях вам может понадобиться изменить интерфейсы, чтобы ввести новые средства. Это представляет некоторую проблему, потому что старые интерфейсы должны поддерживаться для обратной совместимости со старыми дополнениями. После нескольких ревизий вы получите сложную смесь похожих, но разных интерфейсов, и предложению придется распознавать и поддерживать их все. С моделью дополнений вы можете использовать другой подход к обратной совместимости. Вместо ввода множества интерфейсов вы можете использовать единственный интерфейс в вашем контракте и применять адаптеры для создания различных представлений. Например, дополнение версии 1 может работать с приложением версии 2 (которое предоставляет контракт версии 2) до тех пор, пока у вас есть адаптер дополнения, заполняющий пробел. Аналогично, если вы разрабатываете дополнение, которое использует контракт версии 2, вы можете применять его с оригинальной версией 1 приложения (и версией 1 контракта), используя различные адаптеры дополнений. То же самое “шаманство” можно применить, когда у вас есть специальные нужды хостинга. Например, вы можете использовать адаптеры для загрузки дополнений с разными уровнями изоляции, или даже разделять их между приложениями. Принимающее приложение и дополнение не обязаны знать об этих деталях, потому что адаптеры урегулируют их. Даже если вам не нужно создавать специальные адаптеры для реализации специализированных стратегий поддержки версий и хостинга, вам все равно нужно включать эти компоненты. Однако все ваши дополнения могут использовать одинаковые представления и адаптерные компоненты. Другими словами, как только вы решите проблему установки полноценного канала для одного дополнения, то сможете потом добавлять новые дополнения, не прикладывая дополнительных усилий, как показано на рис. 26.5. В следующих разделах вы узнаете, как реализовать канал дополнений для приложения WPF.
Book_Pro_WPF-2.indb 882
19.05.2008 18:11:43
Многопоточность и дополнения
883
Класс дополнения Класс адаптера стороны дополнения
Класс адаптера стороны хоста
Принимающее приложение
наследует
реализует
Абстрактный класс представления хоста
реализует
Интерфейс контракта
Класс дополнения Класс дополнения
наследует
Абстрактный класс представления дополнения
Рис. 26.5. Множество дополнений, использующих один и тот же канал
Структура папок дополнений Для использования канала дополнений вы должны придерживаться строгой структуры каталогов. Эта структура каталогов отдельна от приложения. Другими словами, вполне допустимо расположить ваше приложение в одном месте, а все дополнения и компоненты канала — в другом. Однако компоненты дополнений должны быть организованы в специально именованных подкаталогах относительно друг друга. Например, если ваша система дополнений использует корневой каталог c:\MyApp, вам понадобятся следующие подкаталоги:
c:\MyApp\AddInSideAdapters c:\MyApp\AddInViews c:\MyApp\Contracts c:\MyApp\HostSideAdapters c:\MyApp\AddIns И, наконец, подкаталог AddIns (последний в списке) должен иметь отдельный подкаталог для каждого дополнения, используемого вашим приложением, вроде c:\MyApp\ AddIns\MyFirstAddIn, c:\MyApp\AddIns\MySecondAddIn и т.д. В этом примере предполагается, что исполняемый модуль приложения развернут в каталоге c:\MyApp. Другими словами, один и тот же каталог выполняет двойную функцию — как папка приложения, и как корень дополнений. Это распространенный вариант развертывания, хотя и не обязательный. На заметку! Если вы внимательно рассмотрели диаграммы каналов, то должны были заметить, что существует подкаталог для каждого компонента кроме представлений стороны хоста. Это объясняется тем, что представления хоста используются напрямую принимающим приложениемхостом, поэтому они развертываются вместе с исполняемым приложением. (В данном примере это означает, что они находятся в c:\MyApp.) Представления дополнений так не развертываются, потому что существует вероятность того, что несколько дополнений будут использовать одно и то же представление дополнений. Благодаря выделенной папке AddInViews, вам нужно развернуть (и обновить) только одну копию каждой сборки представления дополнения.
Book_Pro_WPF-2.indb 883
19.05.2008 18:11:43
884
Глава 26
Подготовка решения, использующего модель дополнений Структура папок дополнения обязательна. Если вы пропустите один из подкаталогов, перечисленных в предыдущем разделе, то получите исключение времени выполнения при поиске дополнений. В настоящее время Visual Studio не имеет шаблона для создания приложений, использующих дополнения. Поэтому на вас ложится обязанность создать эти папки и настроить ваш проект Visual Studio для их использования. Ниже описан простейший подход. 1. Создать каталог верхнего уровня, который будет содержать все проекты, которые вы собираетесь создать. Например, вы можете назвать его c:\AddInTest. 2. Создать новый проект WPF для принимающего приложения-хоста в этом каталоге. Неважно, как вы назовете этот проект, но вы должны поместить его в каталоге верхнего уровня, который был создан на шаге 1 (например, c:\AddInTest\ HostApplication). 3. Добавить новый проект библиотеки классов для каждого компонента канала, и поместить их все в одно и то же решение. Как минимум, вы должны создать проект для одного дополнения (например, c:\AddInTest\MyAddIn), одного представления дополнения (c:\AddInTest\ MyAddInView ), одного адаптера стороны дополнения (c:\AddInTest\MyAddInAdapter ), одного представления хоста (c:\AddInTest\ HostView) и одного адаптера стороны хоста (c:\ AddInTest\HostAdapter). На рис. 26.6 показан пример из загружаемого кода для этой главы, который мы рассмотрим в последующих разделах. Он включает приложение (под названием HostApplication) и два дополнения (по имени FadeImageAddIn и NegativeImageAddIn).
Рис. 26.6. Решение, использующее канал дополнений
На заметку! Технически не имеет значения, как вы назовете проекты и каталоги при создании компонентов канала. Необходимая структура папок, о которой вы узнали в предыдущем разделе, будет создана, когда вы строите приложение (если правильно установлены настройки проекта, как описано в следующих двух шагах). Однако чтобы упростить процесс конфигурирования, настоятельно рекомендуется, чтобы вы создавали все каталоги проекта в каталоги верхнего уровня, установленном на шаге 1. 4. Теперь вам нужно создать каталог сборки (build) внутри каталога верхнего уровня. Именно здесь будут размещены все компоненты вашего приложения и канала после компиляции. Обычно принято называть этот каталог Output (например, c:\AddInTest\Output). 5. По мере проектирования различных компонентов канала вы будете модифицировать путь сборки каждого из них, чтобы компонент помещался в правильном подкаталоге. Например, ваш адаптер дополнения должен быть скомпилирован в каталоге вроде c:\AddInTest\Output\AddInSideAdapters. Чтобы модифицировать путь сборки, выполните двойной щелчок на узле Properties (Свойства) в Solution Explorer. Затем перейдите на вкладку Build (Сборка). В разделе Output (внизу) вы найдете текстовое поле по имени Output Path (Выходной путь). Вы долж-
Book_Pro_WPF-2.indb 884
19.05.2008 18:11:43
Многопоточность и дополнения
885
ны использовать относительный выходной путь, находящийся на один уровень выше дерева каталогов, и затем использующий каталог Output. Например, выходным путем для адаптера дополнения должен быть ..\Output\ AddInSideAdapters. По мере построения каждого компонента в следующих разделах вы узнаете, какой путь сборки нужно использовать. На рис. 26.7 показано, как выглядит финальный результат, на основе решения, представленного на рис. 26.6. Есть еще одно обстоятельство, которое нужно учитывать при разработке модели дополнений в Visual Studio. Имеются в виду ссылки. Некоторые компоненты канала нуждаются в ссылках на другие компоненты канала. Однако вы не хотите копировать ссылаемые сборки туда, где находятся сборки, содержащие ссылки на них. Вместо этого вы полагаетесь на сисРис. 26.7. Структура тему каталогов модели дополнений. папок для решения, Чтобы предотвратить копирование ссылаемых сборок, вы использующего канал должны выделить сборку в Solution Explorer (которая появля- дополнений ется под узлом References (Ссылки)). Затем в окне Properties потребуется установить Copy Local (Копировать локально) в False. При построении каждого компонента в следующих разделах вы узнаете, какие ссылки нужно добавить. Совет. Корректная конфигурация проекта дополнений требует некоторых усилий. Чтобы приступить к ней, вы можете использовать пример дополнений, который мы обсуждаем в этой главе и который доступен в составе загружаемого кода.
Приложение, использующее дополнения В следующих разделах будет создано приложение, использующее модель дополнений для поддержки различных способов обработки изображения (рис. 26.8). Когда приложение стартует, оно перечислит все дополнения, представленные в данный момент. Пользователь сможет выбрать одно из них из списка и использовать его для модификации текущего изображения.
Рис. 26.8. Приложение, использующее дополнения для манипуляций изображением
Book_Pro_WPF-2.indb 885
19.05.2008 18:11:43
886
Глава 26
Контракт Начальная точка определения канала дополнений для вашего приложения — создание сборки контракта. Сборка контракта определяет две вещи.
• Интерфейсы, определяющие, как хост взаимодействует с дополнением, и как дополнение взаимодействует с хостом.
• Специальные типы, которые вы используете для обмена информацией между хостом и дополнением. Эти типы должны быть сериализуемыми. Пример, показанный на рис. 26.8, использует чрезвычайно простой контракт. Дополнение предоставляет метод по имени ProcessImageBytes(), который принимает байтовый массив с данными изображения, модифицирует его и возвращает модифицированный байтовый массив. Вот контракт, определяющий этот метод: [AddInContract] public interface IImageProcessorContract : IContract { byte[] ProcessImageBytes(byte[] pixels); }
При создании контракта вы должны наследовать его от интерфейса IContract и также должны оснастить его атрибутом AddInContract. Как интерфейс, так и атрибут находятся в пространстве имен System.AddIn.Contract. Чтобы иметь доступ к ним в вашей сборке контракта, вы должны добавить ссылку на сборку System.AddIn. Contract.dll. Поскольку пример с обработкой изображения не использует специальных типов для передачи данных (а только обычные байтовые массивы), никаких типов не определено в сборке контракта. Байтовые массивы могут быть переданы между приложением-хостом и дополнением, потому что массивы и байты являются сериализуемыми. Единственный дополнительный шаг, который вам нужно предпринять — это конфигурирование каталога сборки. Сборка контракта должна быть помещена в подкаталог Contracts корня дополнений, а это означает, что вы можете использовать выходной путь ..\Output\Contracts в текущем примере. На заметку! В этом примере интерфейсы предельно просты, чтобы избежать замутнения кода дополнительными деталями. В более реалистичных сценариях обработки изображений вы можете включить метод, возвращающий список конфигурируемых параметров, которые влияют на то, как дополнение обрабатывает изображение. Каждое дополнение должно иметь свои собственные параметры. Например, фильтр, затемняющий изображение, может иметь настройку интенсивности, фильтр, выполняющий наклон изображения, — установку угла наклона и т.д. Принимающее приложение (хост) может затем применять эти параметры, вызывая метод ProcessImageBytes().
Представление дополнения Представление дополнения (add-in view) предлагает абстрактный класс, отражающий сборку контракта, и используется на стороне дополнения. Создать этот класс просто: [AddInBase] public abstract class ImageProcessorAddInView { public abstract byte[] ProcessImageBytes(byte[] pixels); }
Обратите внимание, что класс представления дополнения должен быть оснащен атрибутом AddInBase. Этот атрибут находится в пространстве имен System.AddIn.Pipeline.
Book_Pro_WPF-2.indb 886
19.05.2008 18:11:44
Многопоточность и дополнения
887
Сборка представления дополнения требует ссылки на сборку System.AddIn.dll, чтобы принять ее. Сборка представления дополнения должна быть помещена в подкаталоги AddInViews корня дополнения, а это значит, что вы можете использовать в данном примере выходной путь ..\Output\AddInViews.
Дополнение Представление дополнения — это абстрактный класс, который не реализует никакой функциональности. Чтобы создать полезное дополнение, вам нужен конкретный класс — наследник абстрактного класса представления. Этот класс может затем добавлять код, который выполняет реальную работу (в нашем случае — обрабатывает изображение). Следующее дополнение инвертирует значения цвета, чтобы создать эффект, подобный негативу. Вот его полный код: [AddIn("Negative Image Processor", Version = "1.0.0.0", Publisher = "Imaginomics", Description = "Inverts colors to look like a photo negative")] public class NegativeImageProcessor : AddInView.ImageProcessorAddInView { public override byte[] ProcessImageBytes(byte[] pixels) { for (int i = 0; i < pixels.Length - 2; i++) { //Предполагается 24-битный цвет — каждый пиксель описан тремя байтами данных. pixels[i] = (byte)(255 - pixels[i]); pixels[i + 1] = (byte)(255 - pixels[i + 1]); pixels[i + 2] = (byte)(255 - pixels[i + 2]); } return pixels; } }
На заметку! В этом примере байтовый массив передается методу ProcessImageBytes() через параметр, модифицированный непосредственно, и затем передается обратно вызывающему коду в качестве возвращаемого значения. Однако когда вы вызываете ProcessImageBytes() из другого домена приложения, это поведение не так просто, как кажется. Инфраструктура дополнений в действительности делает копию исходного байтового массива и передает эту копию в прикладной домен дополнения. Как только байтовый массив модифицирован и возвращен из метода, инфраструктура дополнений копирует его обратно в домен приложения-хоста. Если бы ProcessImageBytes() не возвращал модифицированного подобным образом массива байтов, то хост никогда бы не увидел измененных данных изображения. Чтобы создать дополнение, вам нужно просто унаследовать класс от абстрактного класса представления и оснастить его атрибутом AddIn. Вдобавок вы можете использовать свойства атрибута AddIn для применения имени, версии, издателя дополнения и его описания, как сделано здесь. Эта информация становится доступной хосту при исследовании им доступных дополнений. Сборка дополнения требует двух ссылок: одну на сборку System.AddIn.dll и еще одну — на проект представления дополнения. Однако вы можете установить свойство Copy Local ссылки на представление дополнения в False (как было описано ранее в разделе “Подготовка решения, использующего модель дополнений”). Это потому, что представление дополнения не развертывается с самим дополнением. Вместо этого она помещается в выделенный подкаталог AddInViews.
Book_Pro_WPF-2.indb 887
19.05.2008 18:11:44
888
Глава 26
Дополнение должно быть помещено в собственный подкаталог внутри подкаталога
AddIns корня дополнения. В текущем примере вы должны применять выходной путь вроде ..\Output\AddIns\NegativeImageAddIn.
Адаптер дополнения Текущий пример обладает всей необходимой функциональностью дополнения, но все-таки еще остается пробел между дополнением и контрактом. Хотя представление дополнения моделируется по контракту, оно не реализует интерфейс контракта, используемого для взаимодействия между приложением и дополнением. Недостающий ингредиент — адаптер дополнения. Он реализует интерфейс контракта. Когда вызывается метод на интерфейсе контракта, он вызывает соответствующий метод в представлении дополнения. Ниже приведен код наиболее простого адаптера дополнения, который вы можете создать. [AddInAdapter] public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract { private AddInView.ImageProcessorAddInView view; public ImageProcessorViewToContractAdapter( AddInView.ImageProcessorAddInView view) { this.view = view; } public byte[] ProcessImageBytes(byte[] pixels) { return view.ProcessImageBytes(pixels); } }
Все адаптеры дополнений должны наследоваться от ContractBase (из пространства имен System.AddIn.Pipeline ). Класс ContractBase унаследован от MarshalByRefObject, который позволяет адаптеру вызываться через границы домена приложения. Все адаптеры дополнения также должны быть оснащены атрибутом AddInAdapter (из пространства имен System.AddIn.Pipeline). Более того, адаптер дополнения должен включать конструктор, принимающий в качестве аргумента экземпляр соответствующего представления. Когда инфраструктура дополнения создает адаптер дополнения, она автоматически использует этот конструктор и передает ему само дополнение. (Напомним, что дополнение наследуется от абстрактного класса представления дополнения, ожидаемого конструктором.) Ваш код просто должен сохранить это представление для последующего использования. Адаптер дополнения требует трех ссылок: одну на System.AddIn.dll , одну на System.AddIn.Contract.dll и одну на проект контракта. Вы должны установить свойство Copy Local ссылки контракта в False (как описано в разделе “Подготовка решения, использующего модель дополнений”). Сборка адаптера дополнения должна быть помещена в подкаталог AddInSideAdapters корня дополнения, а это означает, что вы можете использовать в данном примере выходной путь ..\Output\AddInSideAdapters.
Представление хоста Следующий шаг — построение стороны хоста канала дополнения. Хост взаимодействует с представлением хоста. Подобно представлению дополнения, представление хоста — это абстрактный класс, который тесно отражает интерфейс контракта. Единственное отличие в том, что он не требует никаких атрибутов.
Book_Pro_WPF-2.indb 888
19.05.2008 18:11:44
Многопоточность и дополнения
889
public abstract class ImageProcessorHostView { public abstract byte[] ProcessImageBytes(byte[] pixels); }
Сборка хоста представления должна быть развернута вместе с приложением-хостом. Вы можете вручную поправить выходной путь (например, чтобы сборка представления хоста была размещена в папке ..\Output в текущем примере). Или же, когда вы добавите ссылку на представление хоста в приложение-хост, вы можете оставить свойство Copy Local равным True. Таким образом, представление хоста будет скопировано автоматически в тот же выходной каталог, что и приложение-хост.
Адаптер хоста Адаптер стороны хоста наследуется от представления хоста. Он принимает объект, реализующий контракт, который может затем использовать при вызове своих методов. Этот тот же процесс пересылки (продвижения), что использует и адаптер дополнения, но наоборот. В данном примере, когда приложение-хост вызывает метод ProcessImageBytes() представления хоста, оно в действительности вызывает ProcessImageBytes() в адаптере хоста. Адаптер хоста вызывает ProcessImageBytes() на интерфейсе контракта (и этот вызов переправляется через границы приложения, трансформируясь в вызов метода на адаптере дополнения). Ниже приведен полный код адаптера хоста. [HostAdapter] public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView { private Contract.IImageProcessorContract contract; private ContractHandle contractHandle; public ImageProcessorContractToViewHostAdapter( Contract.IImageProcessorContract contract) { this.contract = contract; contractHandle = new ContractHandle(contract); } public override byte[] ProcessImageBytes(byte[] pixels) { return contract.ProcessImageBytes(pixels); } }
Вы заметите, что адаптер хоста на самом деле использует два поля-члена. Он сохраняет ссылку на текущий объект контракта и также сохраняет ссылку на объект System. AddIns.Pipeline.ContractHandle. Объект ContractHandle управляет жизненным циклом дополнения. Если адаптер хоста не создает объект ContractHandle (и сохраняет ссылку на него), то дополнение будет освобождено немедленно по завершении кода конструктора. Когда приложение-хост попытается использовать дополнение, то получит исключение AppDomainUnloadedException. Проект адаптера хоста нуждается в ссылках на System.Add.dll и System. AddIn.Contract.dll . Ему также нужны ссылки на сборку контракта и сборку представления хоста (у обеих Copy Local должно быть установлено в False ). Выходной путь — подкаталог HostSideAdapters в корне дополнения (в данном примере — ..\Output\HostSideAdapters).
Book_Pro_WPF-2.indb 889
19.05.2008 18:11:44
890
Глава 26
Хост Теперь, имея всю готовую инфраструктуру, последний шаг заключается в создании приложения, которое использует модель дополнений. Хотя хостом может служить исполняемое приложение .NET любого типа, в данном примере мы применяем приложение WPF. Хост нуждается только в одной ссылке, которая указывает на проект представления хоста. Представление хоста — это точка входа в канал дополнений. Фактически теперь, когда вы завершили тяжкий труд по реализации канала, хост может не беспокоиться о том, как он управляется. Ему нужно только найти доступные дополнения, активизировать те, что он желает использовать, и затем вызывать методы представления хоста. Первый шаг — нахождение доступных дополнений — называется исследованием (discovery). Он осуществляется через статические методы класса System.AddIn.Hosting. AddInStore. Чтобы загрузить дополнения, вы просто указываете путь к корню дополнений и вызываете AddInStore.Update(), как показано ниже: //В этом примере путь, из которого запускается приложение, также является корнем дополнений string path = Environment.CurrentDirectory; AddInStore.Update(path);
После вызова Update(), система дополнений создаст два файла с кэшированной информацией. Файл по имени PipelineSegments.store будет помещен в корневой каталог дополнений. Этот файл включает информацию о различных представлениях и адаптерах. Файл по имени AddIns.store будет помещен в подкаталог AddIns, и он содержит информацию о доступных дополнениях. При добавлении новых представлений, адаптеров или дополнений вы можете обновлять эти файлы повторным вызовом AddInStore.Update(). (Этот метод быстро возвратит управление, если никаких новых дополнений или компонентов канала не обнаружит.) Если есть причины ожидать проблем с существующими файлами дополнений, вы можете вместо этого вызывать AddInStore.Rebuild(), что всегда воссоздаст файлы дополнений заново. Как только вы создадите файлы кэшей, то сможете выполнять поиск определенных дополнений. Вы можете использовать метод FindAddIn(), чтобы найти одно определенное дополнение, или же метод FindAddIns(), чтобы найти все дополнения, которые соответствуют указанному представлению хоста. Метод FindAddIns() возвращает коллекцию маркеров (tokens), каждый из которых представляет собой экземпляр класса System.AddIn.Hosting.AddInToken. IList tokens = AddInStore.FindAddIns( typeof(HostView.ImageProcessorHostView), path); lstAddIns.ItemsSource = tokens;
Вы можете получить информацию о дополнении через несколько ключевых свойств (Name, Description, Publisher и Version). В приложении, обрабатывающем изображения (показанном на рис. 26.8), список маркеров привязан к элементу управления ListBox, и некоторая базовая информация отображается о каждом дополнении, используя следующий шаблон данных:
Book_Pro_WPF-2.indb 890
19.05.2008 18:11:44
Многопоточность и дополнения
891
Вы можете создать экземпляр дополнения, вызывая метод AddInToken.Activate. В текущем приложении пользователь щелкает на кнопке Go (Запуск) для активизации дополнения. Затем извлекается информация текущего изображения (показанного в окне), которая затем передается методу ProcessImageBytes() представления хоста. Вот как это работает: private void cmdProcessImage_Click(object sender, RoutedEventArgs e) { // Скопировать информацию изображения в байтовый массив. BitmapSource source = (BitmapSource)img.Source; int stride = source.PixelWidth * source.Format.BitsPerPixel/8; stride = stride + (stride % 4) * 4; int arraySize = stride * source.PixelHeight * source.Format.BitsPerPixel / 8; byte[] originalPixels = new byte[arraySize]; source.CopyPixels(originalPixels, stride, 0); // Получить выбранный маркер дополнения. AddInToken token = (AddInToken)lstAddIns.SelectedItem; // Получить представление хоста. HostView.ImageProcessorHostView addin = token.Activate( AddInSecurityLevel.Internet); // Использовать дополнение. byte[] changedPixels = addin.ProcessImageBytes(originalPixels); // Создать новый BitmapSource с данными измененного изображения и отобразить его. BitmapSource newSource = BitmapSource.Create(source.PixelWidth, source.PixelHeight, source.DpiX, source.DpiY, source.Format, source.Palette, changedPixels, stride); img.Source = newSource; }
Когда вы вызываете метод AddInToken.Activate, “за кулисами” происходит всего несколько шагов. 1. Создается новый домен приложения для дополнения. Альтернативно вы можете загрузить дополнение в домен приложения-хоста или в совершенно отдельный процесс. Однако по умолчанию оно размещается в отдельном домене приложения в пределах текущего процесса, что обычно обеспечивает оптимальный компромисс между стабильностью и производительностью. Вы можете также выбрать уровень привилегий, предоставляемых новому домену приложения. (В данном примере они ограничены набором привилегий Internet — существенно ограниченным набором прав, которые выдаются коду, выполняемому из Web.) 2. Сборка дополнения загружается в новый домен приложения. Затем создается экземпляр дополнения посредством рефлексии, используя конструктор без аргументов. Как вы уже видели, дополнение наследуется от абстрактного класса в сборке представления дополнения. В результате загрузка дополнения также загружает в новый домен приложения сборку представления дополнения. 3. Создается экземпляр адаптера дополнения в новом домене приложения. Дополнение передается адаптеру дополнения в качестве аргумента конструктора. (Дополнение указывается как представление дополнения.) 4. Адаптер дополнения делается доступным домену приложения-хоста (через удаленный прокси). Однако он указывается как реализованный им контракт. 5. В домене приложения-хоста создается экземпляр адаптера хоста. Адаптер дополнения передается адаптеру хоста через его конструктор.
Book_Pro_WPF-2.indb 891
19.05.2008 18:11:44
892
Глава 26
6. Адаптер хоста возвращается приложению-хосту (указанному через представление хоста). Приложение теперь может вызывать методы представления хоста, чтобы взаимодействовать с дополнением через канал. Существуют и другие перегрузки метода Activate, которые позволяют применять специальный набор привилегий (для тонкой настройки безопасности), определенный домен приложения (что удобно, если вы хотите запускать несколько дополнений в пределах одного домена приложения) и внешний процесс (что позволяет вам разместить дополнение в совершенно отдельном EXE приложении для обеспечения еще более высокой степени изоляции). Все эти примеры проиллюстрированы в системе помощи Visual Studio. Этот код завершает пример. Приложение-хост теперь может исследовать доступные дополнения, активизировать их и взаимодействовать с ними через представление хоста.
Жизненный цикл дополнения Вам не нужно вручную управлять жизненным циклом дополнения. Вместо этого система дополнений автоматически освобождает дополнения и останавливает домен приложения. В предыдущем примере дополнение освобождается, когда переменная, ссылающаяся на представление хоста, выходит из области видимости. Если вы хотите сохранять одно и то же дополнение активным в течение длительного времени, вы можете присвоить его переменной-члену класса окна. В некоторых ситуациях вам может понадобиться более высокая степень управляемости жизненным циклом дополнения. Модель дополнений предоставляет приложению-хосту возможность останавливать дополнение автоматически, используя класс AddInController (из пространства имен System.AddIn.Hosting), который отслеживает активные в настоящий момент дополнения. AddInController предоставляет статический метод по имени GetAddInCintrioller(), который принимает представление хоста и возвращает AddInController для дополнения. Вы можете затем использовать метод AddInController.ShutDown() для завершения его работы, как показано ниже: AddInController controller = AddInController.GetAddInController(addin); controller.Shutdown();
В этой точке все адаптеры уничтожаются, дополнение освобождается и домен приложения этого дополнения останавливается, если только он не содержит других дополнений.
Добавление новых дополнений Используя одно и то же представление дополнения, можно создавать неограниченное число отдельных дополнений. В этом примере их два, и они обрабатывают изображения двумя разными способами. Второе дополнение использует грубый алгоритм затемнения картинки посредством удаления части цвета из случайных пикселей. [AddIn("Fade Image Processor", Version = "1.0.0.0", Publisher = "SupraImage", Description = "Darkens the picture")] public class FadeImageProcessor : AddInView.ImageProcessorAddInView { public override byte[] ProcessImageBytes(byte[] pixels) { Random rand = new Random(); int offset = rand.Next(0, 10); for (int i = 0; i < pixels.Length - 1 - offset; i++) { if ((i + offset) % 5 == 0) {
Book_Pro_WPF-2.indb 892
19.05.2008 18:11:45
Многопоточность и дополнения
893
pixels[i] = 0; } } return pixels; } }
В данном примере это дополнение строит выходной путь ..\Output\AddIns\ FadeImageAddIn. Здесь нет необходимости создавать дополнительные представления или адаптеры. Как только вы развернете это дополнение (а затем вызовете метод Rebuild() или Update() класса AddInStore), ваше приложение-хост найдет оба дополнения.
Взаимодействие с хостом В текущем примере хост полностью управляет дополнением. Однако отношения часто меняются. Распространенный пример касается дополнения, которое управляет некоторой областью функциональности приложения. Это особенно присуще визуальным дополнениям (тема следующего раздела), таким как специальные панели инструментов. Часто этот процесс, когда дополнению разрешается вызывать хост, называется автоматизацией. С концептуальной точки зрения автоматизация достаточно проста. Дополнению просто нужна ссылка на объект в домене приложения-хоста, которым оно может манипулировать через отдельный интерфейс. Однако упор системы дополнений на гибкости поддержки версий делает реализацию этой техники несколько более сложной. Единственного интерфейса хоста недостаточно, потому что он тесно связывает вместе хост и дополнение. Вместо этого вам нужно реализовать канал с представлениями и адаптерами. Чтобы увидеть, в чем состоит вся сложность, рассмотрим слегка измененную версию приложения обработки изображений, которая показана на рис. 26.9. Она оснащена полосой продвижения в нижней части окна, обновляемой по мере обработки данных дополнением.
Рис. 26.9. Дополнение, сообщающее о продвижении
Book_Pro_WPF-2.indb 893
19.05.2008 18:11:45
894
Глава 26
Совет. Остаток этого раздела рассматривает изменения, которые вы должны внести в процессор изображений, чтобы поддержать автоматизацию хоста. Чтобы увидеть, как эти части сочетаются вместе, и посмотреть на полный код, загрузите примеры кода для этой главы. Чтобы это приложение работало, дополнению нужен какой-то способ передачи информации о продвижении приложению-хосту во время его работы. Первый шаг к реализации такого решения заключается в создании интерфейса, определяющего, как дополнение может взаимодействовать с хостом. Этот интерфейс должен быть помещен в сборку контракта (или отдельную сборку в папке Contracts). Приведем интерфейс, описывающий, как должно быть применено дополнение, чтобы сообщать о продвижении, посредством вызова метода по имени ReportProgress() в приложении-хосте: public interface IHostObjectContract : IContract { void ReportProgress(int progressPercent); }
Как и интерфейс дополнения, интерфейс хоста должен наследоваться от IContract. В отличие от интерфейса дополнения интерфейс хоста не использует атрибут AddInContract, поскольку не реализуется дополнением. Следующий шаг — создание представления дополнения и представления хоста. Как и при проектировании дополнения, вам нужен просто абстрактный класс, который близко соответствует используемому вами интерфейсу. Чтобы использовать интерфейс IHostObjectContract, показанный ранее, вам нужно просто добавить следующее определение класса — как в проект представления дополнения, так и в проект представления хоста. public abstract class HostObject { public abstract void ReportProgress(int progressPercent); }
Обратите внимание, что определение класса не использует атрибут AddInBase ни в одном из проектов. Действительная реализация метода ReportProgress() находится в приложении-хосте. Ему нужен класс, наследуемый от класса HostObject (в сборке представления хоста). Ниже приведен слегка упрощенный пример, использующий процент для обновления элемента управления ProgressBar. public class AutomationHost : HostView.HostObject { private ProgressBar progressBar; public Host(ProgressBar progressBar) { this.progressBar = progressBar; } public override void ReportProgress(int progressPercent) { progressBar.Value = progressPercent; } }
Теперь у вас есть механизм, который может использовать дополнение для пересылки информации о продвижении приложению-хосту. Однако остается одна проблема — дополнение не имеет никакой возможности получить ссылку на HostObject. Эта проблема не возникает, когда приложение-хост использует дополнение, потому что у него есть
Book_Pro_WPF-2.indb 894
19.05.2008 18:11:45
Многопоточность и дополнения
895
средство исследования, с помощью которого можно искать дополнения. Однако не существует удобной службы, позволяющей дополнениям находить свой хост. Решение заключается в том, чтобы приложение-хост передало ссылку HostObject дополнению. Обычно этот шаг будет выполнен при первоначальной активизации дополнения. По соглашению метод, используемый приложением-хостом для передачи этой ссылки, часто называется Initialize(). Так выглядит обновленный контракт для дополнений процессора изображений: [AddInContract] public interface IImageProcessorContract : IContract { byte[] ProcessImageBytes(byte[] pixels); void Initialize(IHostObjectContract hostObj); }
При вызове Initialize() дополнение просто сохраняет ссылку для последующего использования. Затем оно может вызывать метод ReportProgress(), когда это понадобится, как показано ниже. [AddIn] public class NegativeImageProcessor : AddInView.ImageProcessorAddInView { private AddInView.HostObject host; public override void Initialize(AddInView.HostObject hostObj) { host = hostObj; } public override byte[] ProcessImageBytes(byte[] pixels) { int iteration = pixels.Length / 100; for (int i = 0; i < pixels.Length - 2; i++) { pixels[i] = (byte)(255 - pixels[i]); pixels[i + 1] = (byte)(255 - pixels[i + 1]); pixels[i + 2] = (byte)(255 - pixels[i + 2]); if (i % iteration == 0) host.ReportProgress(i / iteration); } return pixels; } }
До сих пор код не представлял никаких серьезных сложностей. Однако последний фрагмент — адаптеры — несколько сложнее всего, с чем вы имели дело ранее. Теперь, когда к контракту дополнения добавлен метод Initialize(), также нужно добавить его в представления хоста и дополнения. Однако сигнатура метода не может соответствовать интерфейсу контракта. Дело в том, что метод Initialize() в интерфейсе ожидает в качестве аргумента IHostObjectContract. Представления, которые никак не связаны с контрактом, не имеют никакого понятия о IHostObjectContract. Вместо этого они используют описанный ранее абстрактный класс HostObject: public abstract class ImageProcessorHostView { public abstract byte[] ProcessImageBytes(byte[] pixels); public abstract void Initialize(HostObject host); }
Book_Pro_WPF-2.indb 895
19.05.2008 18:11:45
896
Глава 26
Адаптеры представляют собой сложную часть системы. Они призваны заполнить пробел между абстрактным представлением HostObject и интерфейсом IHostObjectContract. Например, рассмотрим ImageProcessorContractToViewHostAdapter на стороне хоста. Он унаследован от абстрактного класса ImageProcessorHostView, в результате чего реализует версию Initialize(), принятую от экземпляра HostObject. Этот метод Initialize() нуждается в преобразовании этого представления в контракт, после чего вызывает метод IHostObjectContract.Initialize(). Сложность заключается в создании адаптера, который выполняет эту трансформацию (подобно адаптеру, который выполняет ту же трансформацию с представлением дополнения и интерфейсом дополнения). Ниже показан новый HostObjectViewToContractHostAdapter, который выполняет работу, и метод Initialize(), использующий его для выполнения “прыжка” от класса представления к интерфейсу контракта. public class HostObjectViewToContractHostAdapter : ContractBase, Contract.IHostObjectContract { private HostView.HostObject view; public HostObjectViewToContractHostAdapter(HostView.HostObject view) { this.view = view; } public void ReportProgress(int progressPercent) { view.ReportProgress(progressPercent); } } [HostAdapter] public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView { private Contract.IImageProcessorContract contract; private ContractHandle contractHandle; ... public override void Initialize(HostView.HostObject host) { HostObjectViewToContractHostAdapter hostAdapter = new HostObjectViewToContractHostAdapter(host); contract.Initialize(hostAdapter); } }
Аналогичная трансформация имеет место в адаптере дополнения, но в обратном направлении. Здесь ImageProcessorViewToContractAdapter реализует интерфейс IImageProcessorContract. Ему нужно взять объект IHostObjectContract, который он получает в своей версии метода Initialize(), и затем преобразовать контракт в представление. Затем он может передавать вызов наряду с вызовом метода Initialize() в представлении. Вот этот код: [AddInAdapter] public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract { private AddInView.ImageProcessorAddInView view; ... public void Initialize(Contract.IHostObjectContract hostObj) {
Book_Pro_WPF-2.indb 896
19.05.2008 18:11:45
Многопоточность и дополнения
897
view.Initialize(new HostObjectContractToViewAddInAdapter(hostObj)); } } public class HostObjectContractToViewAddInAdapter : AddInView.HostObject { private Contract.IHostObjectContract contract; private ContractHandle handle; public HostObjectContractToViewAddInAdapter( Contract.IHostObjectContract contract) { this.contract = contract; this.handle = new ContractHandle(contract); } public override void ReportProgress(int progressPercent) { contract.ReportProgress(progressPercent); } }
Теперь, когда хост вызывает Initialize() на дополнении, он может пройти через адаптер хоста (ImageProcessorContractToViewHostAdapter) и адаптер дополнения (ImageProcessorViewToContractAdapter), прежде чем быть вызванным на самом дополнении. Когда дополнение вызывает метод ReportProgress(), он проходит те же этапы, но в обратном порядке. Сначала он проходит через адаптер дополнения (HostObjectContractToViewAddInAdapter), а затем переходит к адаптеру хоста (HostObjectViewToContractHostAdapter). Эта прогулка завершает пример — отчасти. Проблема в том, что приложение-хост вызывает метол ProcessImageBytes() в главном потоке пользовательского интерфейса. В результате пользовательский интерфейс блокируется. Хотя вызовы ReportProgress() обрабатываются, и полоса продвижения обновляется, все же окно не обновляется до самого завершения процесса. Намного лучший подход состоит в выполнении затратного по времени вызова ProcessImageBytes() в фоновом потоке — либо за счет создания объекта Thread вручную, либо посредством использования BackgroundWorker. Затем, когда пользовательский интерфейс должен быть обновлен (при вызове ReportProgress() и возврате окончательного изображения), вы должны использовать метод Dispatcher.BeginInvoke() для обратной маршализации вызова в поток пользовательского интерфейса. Все эти приемы уже демонстрировались в настоящей главе. Чтобы увидеть работу с потоками в действии в этом примере, обратитесь к загружаемому коду примеров к этой главе.
Визуальные дополнения Учитывая тот факт, что WPF является визуальной технологией, вас, наверное, интересует вопрос — как заставить дополнения генерировать пользовательский интерфейс? Это не такая простая задача. Проблема в том, что элементы пользовательского интерфейса в WPF не сериализуемы. Поэтому они не могут передаваться между приложением-хостом и дополнением. К счастью, проектировщики системы дополнений предусмотрели изощренный обходной путь. Решение заключается в том, чтобы позволить приложениям WPF отображать содержимое пользовательского интерфейса, размещенного в разных доменах приложений. Другими словами, ваше приложение-хост может отображать элементы управления, которые на самом деле работают в домене дополнения. Если вы взаимодействуете с этими элементами управления (щелкая на них, вводя в них текст и т.п.), то инициируются события в домене дополнения. Если вам нужно
Book_Pro_WPF-2.indb 897
19.05.2008 18:11:45
898
Глава 26
передавать информацию из дополнения в приложение и обратно, вы используете интерфейсы контрактов, как было показано в предыдущих разделах. На рис. 26.10 показана эта техника в действии в модифицированной версии приложения обработки изображений. Когда выбрано дополнение, приложение-хост запрашивает у дополнения предоставления элемента управления с соответствующим содержимым. Этот элемент управления затем отображается в нижней части окна.
Рис. 26.10. Визуальное дополнение В этом примере выбрано дополнение, превращающее изображение в негатив. Оно представляет пользовательский элемент управления, который упаковывает элемент Image (с предварительным просмотром эффекта) и элемент Slider. По мере перемещения бегунка Slider меняется интенсивность эффекта, и картинка предварительного просмотра изменяется. (Процесс обновления довольно медлительный, из-за плохой оптимизации кода обработки изображения. Можно было бы использовать намного более совершенные алгоритмы, возможно, включающие небезопасные блоки кода, для достижения максимальной производительности.) Хотя механизм, выполняющий эту работу, довольно сложный, использовать его неожиданно легко. Ключевой ингредиент заключен в интерфейсе INativeHandleContract из пространства имен System.AddIn.Contract. Он позволяет передавать дескриптор окна между дополнением и приложением-хостом. Приведем пересмотренный IImageProcessorContract из сборки контракта. Он заменяет метод ProcessImageBytes() методом GetVisual(), принимающим те же данные изображения, но возвращающим кусочек пользовательского интерфейса: [AddInContract] public interface IImageProcessorContract : IContract { INativeHandleContract GetVisual(Stream imageStream); }
Book_Pro_WPF-2.indb 898
19.05.2008 18:11:45
Многопоточность и дополнения
899
Вы не используете INativeHandleContract в визуальных классах, потому что он не является непосредственно используемым в вашем приложении WPF. Вместо этого вы используете тот тип, который можно было ожидать — FrameworkElement. Вот представление хоста: public abstract class ImageProcessorHostView { public abstract FrameworkElement GetVisual(Stream imageStream); }
А это почти идентичное представление дополнения: [AddInBase] public abstract class ImageProcessorAddInView { public abstract FrameworkElement GetVisual(Stream imageStream); }
Этот пример неожиданно похож на вариант с автоматизацией из предыдущего раздела. Здесь вы передаете в контракт другой тип, нежели тот, что используется в представлениях. Опять-таки, вам нужно использовать адаптеры для выполнения преобразований контракт-представление и представление-контракт. Однако на этот раз работу для вас выполняет специализированный класс по имени FrameworkElementAdapter. FrameworkElementAdapter находится в пространстве имен System.AddIn.Pipeline, но не является частью WPF, а входит в сборку System.Windows.Presentation.dll. Класс FrameworkElementAdapter предоставляет два статических метода, выполняющих работу по преобразованию: ContractToViewAdapter() и ViewToContractAdapter(). Вот как метод FrameworkElementAdapters.ContractToViewAdapter() заполняет пробел в адаптере хоста: [HostAdapter] public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView { private Contract.IImageProcessorContract contract; private ContractHandle contractHandle; ... public override FrameworkElement GetVisual(Stream imageStream) { return FrameworkElementAdapters.ContractToViewAdapter( contract.GetVisual(imageStream)); } }
А так метод FrameworkElementAdapters.ViewToContractAdapter() заполняет пробел в адаптере дополнения: [AddInAdapter] public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract { private AddInView.ImageProcessorAddInView view; ... public INativeHandleContract GetVisual(Stream imageStream) { return FrameworkElementAdapters.ViewToContractAdapter( view.GetVisual(imageStream)); } }
Book_Pro_WPF-2.indb 899
19.05.2008 18:11:45
900
Глава 26
И последний штрих заключается в реализации метода GetVisual() в дополнении. В процессоре негативного изображения создается новый элемент управления по имени ImagePreview. Данные изображения передаются этому элементу, который устанавливает картинку предварительного просмотра и обработки щелчков на бегунке. (Код пользовательского элемента управления не включен в этот пример, но все детали вы можете найти в загружаемых примерах для этой главы.) [AddIn] public class NegativeImageProcessor : AddInView.ImageProcessorAddInView { public override FrameworkElement GetVisual(System.IO.Stream imageStream) { return new ImagePreview(imageStream); } }
Теперь, когда вы знаете, как вернуть объект пользовательского интерфейса из дополнения, ничто не ограничивает вас в типах содержимого, которое можно генерировать. Базовая инфраструктура — интерфейс INativeHandleContract и класс Framework ElementAdapters — остаются неизменными.
Резюме В этой главе вы ознакомились с двумя расширенными темами, которые сами по себе могли бы занимать целые книги. Для начала, были рассмотрены особенности многопоточности приложений WPF (которые, по сути, едины для всех типов приложений Windows), и вы узнали, как безопасно обновлять элементы управления из других потоков и как упростить реализацию многопоточности с помощью BackgroundWorker. Далее вы погрузились в многоуровневую модель дополнений. Вы узнали о том, как работает канал дополнений, почему он работает именно так, и как создать базовое дополнение, поддерживающее автоматизацию хоста и предоставляющее визуальное содержимое. О модели дополнений можно было бы сказать еще кое-что. Если вы планируете сделать дополнения ключевой частью профессиональных приложений, вам стоит присмотреться к специализированным сценариям поддержки версий и хостинга при развертывании, изучить передовые приемы обращения с необработанными исключениями дополнений, а также узнать о том, как организовать более сложные взаимодействия между хостом и дополнением, а также между разными дополнениями. Некоторую дополнительную информацию вы можете найти в справочной системе Visual Studio (загляните в ее раздел “add-ins [.NET Framework]”), хотя вы и не найдете там наиболее развернутой информации. Чтобы изучить все детали, вам стоит посетить блог команды разработчиков Microsoft, которые создавали систему дополнений, доступный по адресу http:// blogs.msdn.com/clraddins. Также вас может заинтересовать блок Джейсона Хи (Jason He) на http://blogs.msdn.com/zifengh. Он является членом команды разработчиков, который описал свой опыт адаптации Paint.NET к применению модели дополнений.
Book_Pro_WPF-2.indb 900
19.05.2008 18:11:46
ГЛАВА
27
Развертывание ClickOnce Р
ано или поздно вы отпустите свое приложение WPF в “свободное плаванье”. Хотя существуют десятки разных способов передать готовое приложение с вашего компьютера разработчика на настольный компьютер конечного пользователя, большинство приложений WPF используют одну из описанных ниже стратегий развертывания.
• Запуск в браузере. Если вы создали WPF-приложение, состоящее из Web-страниц, вы можете запускать его в браузере. Вам не нужно ничего инсталлировать. Однако ваше приложение должно быть готово к тому, чтобы функционировать с очень небольшим набором привилегий. (Например, вы не сможете получить доступ к произвольным файлам, работать с системным реестром Windows, отображать всплывающие окна и т.д.) Такой подход был изложен в главе 9.
• Развертывание через браузер. WPF-приложения тесно интегрированы с средством установки ClickOnce (“однократный щелчок”), которое позволяет пользователю запустить программу инсталляции со страницы браузера. Лучше всего то, что приложения, инсталлированные средством ClickOnce, могут быть сконфигурированы так, чтобы автоматически проверять наличие обновлений. Отрицательной стороной является то, что у вас ограничены возможности по настройке вашей установки, и нет никаких возможностей выполнить задачи системного конфигурирования (вроде регистрации типов файлов, создания базы данных и т.п.).
• Развертывание с помощью традиционной программы инсталляции. Этот подход все еще живет в мире WPF. Если вы выбираете этот вариант, то вам решать — хотите вы создавать полноценный инсталляционный пакет Microsoft Installer (MSI) или же обратиться к более простой (но и ограниченной) установке ClickOnce. Построив инсталляционный пакет, вы можете распространять его на компактдиске, во вложении сообщения электронной почты, через общедоступный сетевой ресурс и т.д. В настоящей главе мы рассмотрим второй из перечисленных подходов: развертывание приложения посредством модели ClickOnce.
Развертывание приложения Хотя технически вполне возможно перемещать приложение .NET с одного компьютера на другой простым копированием папки, содержащей его, все же профессиональные приложения часто требуют несколько большего. Например, вам может понадобиться добавить ссылки на него в меню Start (Пуск) или на рабочий стол, зарегистрировать
Book_Pro_WPF-2.indb 901
19.05.2008 18:11:46
902
Глава 27
типы файлов и установить дополнительные ресурсы (такие как специальный журнал регистрации событий или база данных). Чтобы получить эти средства, вы должны создать специальную программу инсталляции. Существует много вариантов создания программ установки. Вы можете использовать коммерчески распространяемый продукт типа InstallShield либо создать инсталляцию MSI, используя шаблон Setup Project (Установочный проект) в Visual Studio. Традиционные программы установки предлагают пользователю хорошо знакомый мастер (wizard) инсталляции, с изобилием средств для передачи файлов и выполнения разнообразных конфигурационных действий. Другой вариант — воспользоваться системой развертывания ClickOnce, тесно интегрированной с WPF. Система ClickOnce имеет ряд ограничений (большинство из них связано с дизайном), но дает два существенных преимущества.
• Поддержка обновлений автоматически загружаемых из Web. • Поддержка инсталляции и выполнения приложения в сценариях с ограниченным доверием. Это средство доступно, только если вы создаете приложение XAML для браузера (XBAP), как описано в главе 9. В ближайшее время этих двух средств явно недостаточно для того, чтобы соблазнить разработчиков предпочесть их полноценной программе инсталляции. Но в будущем, по мере того, как станет более привычной работа в Windows от имени пользовательской учетной записи с ограниченным набором привилегий (например, в Windows Vista), и с более широким распространением приложений WPF, базирующихся на браузере, значение ClickOnce будет возрастать. И даже если браузерные приложения WPF придут на смену сегодняшнему поколению Web-приложений, ClickOnce станет ключевым звеном в мозаике. Если вы работали с Windows Forms в .NET 2.0, то заметите, что в WPF средство ClickOnce совершило шаг назад. В .NET 2.0 ClickOnce было предпочтительным способом развертывания приложений через Web и единственным способом конкурировать с традиционными Web-сайтами. В WPF браузерные приложения предлагают более эффективный способ создания традиционных Web-приложений на основе WPF, и их не нужно явно развертывать. В WPF вы применяете ClickOnce для развертывания только автономных приложений. Есть еще одно изменение. В .NET 2.0 приложение Windows Forms может быть сконфигурировано для использования частичного доверия и затем развернуто с помощью ClickOnce. В WPF это невозможно, поскольку требуются привилегии неуправляемого кода для создания окна WPF. Чтобы иметь такие привилегии, ваше приложение должно запускаться с полным доверием. Это означает, что инсталляция автономного WPF-приложения с помощью ClickOnce представляет те же сложности, связанные с безопасностью, что и инсталляция приложений любого типа из Web, т.е. Internet Explorer выдаст предупреждение об угрозе безопасности. Если пользователь продолжит процесс, то инсталлированное приложение будет иметь возможность делать все, что может делать текущий пользователь.
Что такое ClickOnce Хотя ClickOnce допускает некоторую настройку, все же определенные детали никогда не меняются. Прежде чем приступить к использованию ClickOnce, важно составить представление о базовой модели и ее ограничениях. При проектировании ClickOnce имелось в виду простое, прямолинейное приложение. Оно, в частности, подходит для специализированных отраслевых (line-of-business) приложений и программного обеспечения для внутреннего потребления компании.
Book_Pro_WPF-2.indb 902
19.05.2008 18:11:46
Развертывание ClickOnce
903
Обычно такие приложения выполняют свою работу, опираясь на данные и службы, предоставляемые серверами приложений среднего уровня. Как следствие, им не требуется привилегированный доступ к локальному компьютеру. Эти приложения также развертываются в среде масштаба предприятия, которое может включать тысячи рабочих станций. В таких средах стоимость развертывания приложений и их обновления довольно значительна, особенно если все это должен выполнять администратор. В результате более важно предложить простой и прямолинейный процесс, нежели богатый пакет средств. ClickOnce также может иметь смысл для прикладных программ, которые распространяются через Web, в частности, если эти приложения требуют частого обновления и не предъявляют строгих инсталляционных требований. Однако ограничения ClickOnce (такие как недостаток гибкости в настройке мастера инсталляции) делают его непрактичным для сложных потребительских приложений, которые выдвигают детализированные требования к установке или нуждаются во взаимодействии с пользователем при выполнении ряда тонких конфигурационных шагов. В этих случаях вам понадобится более изощренное инсталляционное приложение, которое вы можете создать, используя MSI. На заметку! Чтобы инсталлировать приложение WPF с помощью ClickOnce, на компьютере уже должна быть инсталлирована исполняющая система .NET Framework 3.0 или 3.5, в зависимости от версии, на которую вы ориентированы (как описано в главе 1). Когда вы впервые запускаете установку ClickOnce, это требование проверяется. Если исполняющая система .NET Framework не инсталлирована, отображается окно сообщения, которое поясняет суть проблемы и приглашает инсталлировать исполняющую систему .NET Framework с Web-сайта Microsoft.
Инсталляционная модель ClickOnce Хотя ClickOnce поддерживает несколько типов развертывания, общая модель разработана так, чтобы сделать Web-развертывание практичным и легким. Вот как это работает. Вы используете Visual Studio для публикации приложения ClickOnce на Web-сервере. Затем пользователь переходит на автоматически сгенерированную страницу (по имени publish.htm), которая предлагает ссылку для инсталляции приложения. Когда пользователь щелкает на этой ссылке, приложение загружается, инсталлируется и добавляется в меню Start. На рис. 27.1 показан этот процесс. Хотя ClickOnce — идеальный выбор для Web-развертывания, та же базовая модель подходит и для других сценариев, включая следующие:
• развертывание приложения из общедоступного сетевого ресурса; • развертывание приложения с CD- или DVD-диска; • развертывание приложения на Web-сервере или общедоступном сетевом ресурсе с последующей отправкой по электронной почте ссылки на программу инсталляции. publish.html Visual Studio
Публикация
Загрузка
Локальная копия приложения
Проверка обновлений
Компьютер разработчика
Сервер разработчика
Клиентский компьютер
Рис. 27.1. Инсталляция приложения ClickOnce
Book_Pro_WPF-2.indb 903
19.05.2008 18:11:46
904
Глава 27
Инсталляционная Web-страница не создается на общедоступном сетевом ресурсе, CD- или DVD-диске. Вместо этого пользователи инсталлируют приложение непосредственным запуском программы setup.exe. На заметку! Эти способы не столь неотразимы, как подход “развертывание из Web”. В конце концов, если вы поставляете CD-диск или принуждаете пользователя запускать специальную программу установки, можно предположить, что они решили доверять вашему приложению. В этом случае, возможно, имеет больше смысла применять полноценную программу инсталляции, которая предоставляет больше возможностей. Однако, тем не менее, вы все равно можете отдать предпочтение ClickOnce, если развертываете приложение более чем одним способом (включая развертывание из Web), если требования к инсталляции относительно скромны, или же если вы хотите использовать средство автоматического обновления. Наиболее интересная часть развертывания ClickOnce заключается в его способе поддержки обновления. По сути дела, вы (как разработчик) управляете несколькими установками обновления. Например, вы можете сконфигурировать приложение для автоматической проверки наличия обновлений через определенные интервалы. Когда пользователь запускает ваше приложение, в действительности он сначала запускает тонкую прослойку, которая проверяет наличие новой версии и предлагает пользователю загрузить ее. Вы даже можете сконфигурировать приложение для использования Web-подобного режима “только онлайн”. В этом случае приложение должно запускаться со специальной Web-страницы ClickOnce. Приложение по-прежнему кэшируется локально для достижения оптимальной производительности, но пользователи не смогут запустить его до тех пор, пока не подключатся к сайту, на котором это приложение опубликовано. Это гарантирует, что они всегда будут запускать последнюю, самую современную версию вашего приложения. Совет. Вам не обязательно создавать приложение ClickOnce, чтобы получить средство автоматического обновления. Вы можете построить подобное средство самостоятельно. Простейший способ — адаптировать код к Application Updater Component, предоставленному командой разработчиков Windows Forms по адресу http://windowsforms.net/articles/ appupdater.aspx. Другой вариант — воспользоваться более гибким (но несколько замысловатым) Application Updater Starter Block от группы Practices and Guidance Group из Microsoft. Это средство можно найти по адресу http://www.microsoft.com/downloads, указав в критерии поиска “Application Updater Block”.
Ограничения ClickOnce ClickOnce спроектирован как более легкий способ инсталляции, чем установки на базе MSI. В результате развертывание ClickOnce не предусматривает значительного конфигурирования. Многие аспекты его поведения жестко фиксированы — либо чтобы гарантировать согласованное восприятие пользователем, либо для обеспечения политик безопасности подходящих для предприятий. Ниже описаны ограничения ClickOnce.
• Приложения ClickOnce инсталлируются для единственного пользователя. Вы не можете инсталлировать приложение для всех пользователей рабочей станции.
• Приложения ClickOnce всегда инсталлируются в управляемой системой, специфичной для пользователя папке. Вы не можете изменить или повлиять на выбор папки, куда инсталлируется приложение.
Book_Pro_WPF-2.indb 904
19.05.2008 18:11:46
Развертывание ClickOnce
905
• Если приложения ClickOnce инсталлируются в меню Start, они отображаются в виде единственной ссылки в форме [Наименование издателя][Наименование продукта]. Вы не можете изменить этого, как не можете и добавить других ссылок, подобных ссылкам на справочный файл, связанный Web-сайт или средство деинсталляции. Аналогично, вы не можете добавить приложение ClickOnce в группу Startup (Автозагрузка), меню Favorites (Избранное) и т.п.
• Вы не можете изменить пользовательский интерфейс или мастер инсталляции. Это значит, что вы не можете добавлять новые диалоговые окна, изменять текст в существующих окнах и т.д.
• Вы не можете изменить инсталляционную страницу, сгенерированную ClickOnce. Однако вы можете отредактировать HTML вручную после его генерации.
• Инсталляция ClickOnce не может устанавливать распределенные компоненты в кэше глобальных сборок (GAC).
• Инсталляция ClickOnce не может выполнять специализированных действий (таких как создание базы данных, регистрация типов файлов или конфигурирование настроек реестра). Некоторые из этих ограничений можно обойти. Например, вы можете сконфигурировать ваше приложение на регистрацию определенных типов файлов или установку стандартных настроек реестра при его первом запуске на новом компьютере. Однако если предъявлены сложные требования к инсталляции, то намного лучше обратиться к созданию полноценной программы инсталляции MSI. Вы можете использовать инструменты от независимых разработчиков или же создать проект Setup в Visual Studio. Оба эти варианта выходят за рамки материала настоящей книги.
Простая публикация ClickOnce Простейший путь публикации приложения через ClickOnce заключается в выборе пункта BuildPublish [Имя проекта] (СборкаПубликовать [Имя проекта]) из меню Visual Studio, что запустит краткий мастер. Этот мастер не даст вам доступа ко всем средствам ClickOnce, о которых вы узнаете в настоящей главе, однако это самый быстрый способ начать.
Создание инсталляции ClickOnce в среде Windows Vista Прежде чем приступить, отметим некоторые дополнительные обстоятельства, касающиеся пользователей Windows Vista, которые желают публиковать приложения на локальном Web-сервере. Если вы публикуете свое приложение в виртуальном каталоге локального компьютера, вам следует убедиться, что инсталлированы службы Internet Information Services (IIS) 7 через элемент Programs and Features (Программы и средства) панели управления, который позволяет включать и отключать отдельные средства Windows. Если выбрана инсталляция IIS 7, не забудьте включить опции .NET Extensibility и IIS 6 Management Compatibility (которая позволит Visual Studio взаимодействовать с IIS). Если вы публикуете в виртуальный каталог в Visual Studio, вам понадобятся административные привилегии. Однако средство безопасности User Account Control (UAC) ограничивает административные привилегии административных учетных записей, если только они не запрошены специально. Для получения административных привилегий в Visual Studio, чтобы иметь доступ к IIS, вы должны запустить Visual Studio от имени администратора. Простейший способ сделать это — щелкнуть правой кнопкой мыши на ссылке Microsoft Visual Studio 2005 в меню Start и выбрать в появившемся контекстном меню команду Run As Administrator (Запуск от имени администратора). Также вы можете сконфигурировать свой компьютер на постоянный запуск Visual Studio от
Book_Pro_WPF-2.indb 905
19.05.2008 18:11:46
906
Глава 27
имени администратора, что представляет собой компромисс между удобством и безопасностью, необходимость которого нужно тщательно взвесить. Чтобы сделать это, щелкните правой кнопкой мыши на ссылке Visual Studio, выберите в появившемся контекстном меню команду Properties (Свойства), затем перейдите на вкладку Compatibility (Совместимость) и там отметьте флажок Run This Program As Administrator (Запускать эту программу от имени администратора). UAC также может вызвать некоторые проблемы при инсталляции приложения ClickOnce. Читайте ниже врезку “Установки ClickOnce и UAC”. Первым выбором, с которым вы столкнетесь в мастере публикации, будет выбор местоположения, куда вы публикуете приложение (рис. 27.2).
Рис. 27.2. Выбор местоположения публикации В выборе местоположения первой публикации вашего приложения нет ничего особо важного, поскольку нет необходимости выбирать то же место, которое вы позже используете для размещения инсталляционных файлов. Другими словами, вы можете публиковать в локальном каталоге и затем передать файлы на Web-сервер. Единственный нюанс — вы должны знать конечное место расположения ваших файлов, когда запускаете мастер публикации, поскольку придется указывать эту информацию. Без этого средство автоматического обновления не будет работать. Конечно, вы можете решить публиковать свое приложение непосредственно в его конечное расположение, но это не обязательно. Фактически, локальное построение инсталляции часто представляет собой простейший путь.
Выбор местоположения Чтобы лучше представить, как это работает, начните с выбора локального пути (например, C:\Temp\ClickOnceApp). Затем щелкните на кнопке Next (Далее). После этого вы столкнетесь с реальным вопросом — куда пойдут пользователи, чтобы инсталлировать это приложение (рис. 24.3). Это важно, потому что повлияет на вашу стратегию обновления. Сделанный вами выбор будет зафиксирован в файле манифеста, поставляемом вместе с приложением.
Book_Pro_WPF-2.indb 906
19.05.2008 18:11:47
Развертывание ClickOnce
907
На заметку! Есть один случай, когда вы не увидите диалогового окна, показанного на рис. 27.3. Если вы введете виртуальный каталог на Web-сервере в качестве местоположения публикации (другими словами, URL, начинающийся с http://), то мастер предположит, что это окончательное местоположение инсталляции. На рис. 27.3 можно видеть три доступных выбора. Можно создать инсталляцию на сетевом файловом ресурсе, на Web-сервере или же на CD- либо DVD-диске. В последующих разделах рассматриваются все подходы.
Рис. 27.3. Выбор типа инсталляции
Публикация на сетевом файловом ресурсе В этом случае все пользователи вашей сети получат доступ к инсталляции, перейдя по определенному пути UNC и запустив находящийся там файл под названием setup.exe. Пусть UNC — это сетевой путь в форме \\ИмяКомпьютера\ИмяОбщегоРесурса. Вы не можете использовать сетевой диск, поскольку он зависит от настроек системы (так что разные пользователи могут иметь разные настройки сетевых дисков). Чтобы обеспечить автоматические обновления, инфраструктура ClickOnce должна знать точно, где она сможет найти инсталляционные файлы, и это будет то же место, где будут развернуты обновления.
Публикация для Web-сервера Вы можете создать инсталляцию для Web-сервера в локальной корпоративной сети или в Internet. Visual Studio сгенерирует HTML-файл по имени publish.htm, который упростит процесс. Пользователь запросит эту страницу в своем браузере и щелкнет на ссылке для загрузки и инсталляции приложения. Существует несколько способов передачи ваших файлов на Web-сервер. Если вы хотите использовать двухшаговый подход (опубликовать файлы локально, а затем передать их в нужное место), то вам просто следует скопировать файлы из локального каталога на Web-сервер, используя соответствующий механизм (вроде FTP). Убедитесь, что вы сохраняете при этом структуру каталогов.
Book_Pro_WPF-2.indb 907
19.05.2008 18:11:47
908
Глава 27
Если вы хотите опубликовать свои файлы непосредственно на Web-сервере, минуя предварительное тестирование, у вас есть два выбора. Если вы применяете IIS, и текущая учетная запись пользователя обладает достаточными привилегиями для создания нового виртуального каталога на Web-сервере (или загрузки файлов в существующий виртуальный каталог), вы можете опубликовать файлы непосредственно на Web-сервер. Просто укажите путь к виртуальному каталогу на первом шаге мастера. Например, вы можете использовать в качестве местоположения публикации http://ИмяКомпьютера/ ИмяВиртуальногоКаталога (в случае корпоративной сети) или http://ДоменноеИмя/ ИмяВиртуальногоКаталога (для сервера, находящегося в Internet). Также вы можете публиковать непосредственно на Web-сервер через FTP. В Internet это часто бывает обязательным требованием (в отличие от корпоративной сети). В этом случае Visual Studio установит соединение с вашим Web-сервером и передаст туда файлы ClickOnce по FTP. При этом для установки соединения у вас будет запрошено имя пользователя и пароль. На заметку! FTP применяется для передачи файлов. Этот протокол не используется непосредственно в инсталляционном процессе. Идея состоит в том, чтобы загружаемые вами файлы стали видимыми на некотором Web-сервере, и пользователи могли инсталлировать приложение из файла publish.htm на этом Web-сервере. В результате, когда вы используете путь FTP на первом шаге мастера (рис. 27.2), вам все равно нужно указать соответствующий URL на втором шаге (рис. 27.3). Это важно, потому что публикация ClickOnce должна возвратиться в это местоположение для выполнения автоматических обновлений.
Публикация для CD- и DVD-диска Если вы выбираете публикацию на инсталляционный носитель вроде CD- или DVDдиска, то все равно должны решить, планируете ли вы осуществлять автоматические обновления. Некоторые организации используют исключительно развертывание на базе CD-диска, в то время как другие применяют его в дополнение к развертыванию на основе Web или сетевых ресурсов. На третьем шаге мастера вы выбираете опции для этого конкретного случая (рис. 27.4).
Рис. 27.4. Поддержка автоматических обновлений
Book_Pro_WPF-2.indb 908
19.05.2008 18:11:47
Развертывание ClickOnce
909
Здесь перед вами встает выбор. Вы можете применить URL или путь UNC, по которому приложение будет искать обновления. Это предполагает, что вы планируете публиковать приложение в этом месте. Альтернативно вы можете пропустить эту информацию и тем самым отключить средство автоматического обновления вовсе. На заметку! Мастер публикации не дает возможности устанавливать частоту проверок обновлений. По умолчанию приложения ClickOnce проверяют наличие обновлений при каждом запуске. Если новая версия найдена, .NET приглашает пользователя инсталлировать ее перед запуском приложения. Позднее в этой главе вы узнаете, как изменить эту настройку.
Онлайн или оффлайн Если вы создаете развертывание для Web-сервера или сетевого ресурса, то получаете дополнительную опцию, показанную на рис. 27.5.
Рис. 27.5. Поддержка использования оффлайн Выбор по умолчанию заключается в создании онлайнового/оффлайнового приложения, которое запускается независимо от того, может пользователь подключиться к месту публикации или нет. В этом случае ярлык для запуска приложения добавляется в меню Start. Если вы выберете создание только онлайнового приложения, то пользователю нужно будет всякий раз обращаться к месту публикации, чтобы запустить приложение. (Чтобы прояснить это, Web-страница publish.htm отобразит кнопку Run (Запустить) вместо Install (Инсталлировать).) Это исключит вероятность запуска старых версий вашего приложения после “наложения” обновления. Эта часть модели развертывания аналогична Web-приложениям. Когда вы создаете только онлайновое приложение, оно по-прежнему будет загружаться (в локально кэшируемое местоположение) при первом запуске. Таким образом, хотя первоначальное время запуска может быть длиннее (из-за первоначальной загрузки), приложение будет работать так же быстро, как любое обычное инсталлированное приложение Windows. Однако приложение не может быть запущено, когда пользователь не подключен к сети или Internet, что делает его неподходящим для мобильных пользо-
Book_Pro_WPF-2.indb 909
19.05.2008 18:11:47
910
Глава 27
вателей (например, пользователей ноутбуков, которым не всегда доступно подключение к Internet). Если вы выберете создание приложения, которое поддерживает оффлайновую работу, то программа инсталляции добавит ярлык в меню Start. Пользователь может запустить приложение с помощью этого ярлыка, независимо от того, подключен компьютер к сети или нет. Если компьютер находится в онлайновом режиме, то приложение проверит наличие новых версий в месте, где опубликовано приложение. Если обновления есть, приложение предложит пользователю инсталлировать их. Позднее вы узнаете, как конфигурировать эту политику. На заметку! Если вы выбираете публикацию для инсталляции с CD-диска, у вас нет выбора для создания только онлайнового приложения. Это — последний выбор в мастере публикации. Щелкните на кнопке Next, чтобы увидеть итоговую информацию и затем на кнопке Finish (Готово) для генерации файлов развертывания и копирования их в место, указанное на шаге 1.
Развернутые файлы ClickOnce использует довольно простую структуру каталогов. Создается файл setup. exe в выбранном вами месте и подкаталог для приложения. Например, при развертывании приложения по имени ClickOnceText в каталоге c:\ ClickOnceTest вы получите следующие файлы: c:\ClickOnceTest\setup.exe c:\ClickOnceTest\publish.htm c:\ClickOnceTest\ClickOnceTest.application c:\ClickOnceTest\ClickOnceTest_1_0_0_0.application c:\ClickOnceTest\ClickOnceTest_1_0_0_0\ClickOnceTest.exe.deploy c:\ClickOnceTest\ClickOnceTest_1_0_0_0\ClickOnceTest.exe.manifest
Файл publish.htm представлен только в случае публикации на Web-сервере. Файлы .manifest и .application хранят информацию о необходимых файлах, установках обновлений и прочие детали. (Подробности об этих файлах и их XML-файл вы можете найти в справочной системе MSDN.) Файлы .manifest и .application снабжаются электронной подпись во время публикации, поэтому не могут модифицироваться вручную. Если вы внесете изменение, ClickOnce заметит разницу и откажется инсталлировать приложение. По мере публикации новых версий приложения ClickOnce будет добавлять новые подкаталоги для каждой новой версии. Например, если вы измените опубликованную версию вашего приложения на 1.0.0.1, то получите новый каталог: c:\ClickOnceTest\ClickOnceTest_1_0_0_1\ClickOnceTest.exe.deploy c:\ClickOnceTest\ClickOnceTest_1_0_0_1\ClickOnceTest.exe.manifest
Когда вы запустите программу setup.exe, она отработает процесс инсталляции всего обязательного программного обеспечения (такого как .NET Framework) и затем инсталлирует наиболее свежую версию вашего приложения.
Инсталляция приложения ClickOnce Чтобы увидеть ClickOnce в действии при Web-развертывании, выполните описанные ниже шаги. 1. Удостоверьтесь, что у вас инсталлированы необязательные компоненты Web-сервера IIS. В Windows XP выберите в меню StartSettingsControl PanelAdd or Remove
Book_Pro_WPF-2.indb 910
19.05.2008 18:11:47
Развертывание ClickOnce
911
Program (ПускНастройкаПанель управленияУстановка и удаление программ), затем выберите раздел Add/Remove Windows Components (Установка компонентов Windows) и прокрутите список до тех пор, пока не найдете элемент Internet Information Services (IIS). Флажок возле него должен быть отмечен. В Windows Vista следуйте инструкциям из ранее приведенной в этой главе врезки “Создание инсталляции ClickOnce в среде Windows Vista”. 2. Используя Visual Studio, создайте базовое приложение Windows и скомпилируйте его. 3. Запустите мастер публикации (выбрав Build Publish) и укажите http:// localhost/ClickOnceTest в качестве местоположения публикации. Часть URL, касающаяся местоположения, указывает на текущий компьютер. Если IIS инсталлирован, и вы работаете с достаточными привилегиями, Visual Studio сможет создать этот виртуальный каталог. 4. Выберите создание онлайнового и оффлайнового приложения, затем щелкните на Finish (Готово) для завершения работы мастера. Файлы будут развернуты в папке по имени ClickOnceTest в корне Web-сервера IIS (по умолчанию в c:\Inetpub\wwwroot). 5. Запустите программу setup.exe непосредственно или загрузите страницу publish.htm (показанную на рис. 27.6) и щелкните на кнопке Install (Инсталлировать). Вы получите предупреждающее сообщение о безопасности, которое запросит у вас подтверждения доверия приложению (подобно тому, как происходит, когда вы загружаете элемент управления ActiveX в Web-браузер).
Рис. 27.6. Инсталляционная страница publish.htm 6. Если вы решите продолжить, приложение будет загружено и последует запрос о том, хотите ли вы инсталлировать его. 7. После того, как приложение будет инсталлировано, вы сможете запустить его через ярлык в меню Start либо деинсталлировать, используя диалоговое окно Add/ Remove Programs (Установка и удаление программ). Ярлык приложения ClickOnce не является стандартным ярлыком вроде тех, к которым вы привыкли. На самом деле это ссылка на приложение — текстовый файл с информацией об имени приложения и расположением файлов развертывания. Действительные программные файлы вашего приложения располагаются в месте, которое трудно найти и невозможно контролировать.
Book_Pro_WPF-2.indb 911
19.05.2008 18:11:47
912
Глава 27
Местоположение следует такому шаблону: c:\Documents and Settings\[ИмяПользователя]\Local Settings\Apps\2.0\[...]\[...]\[...]
Конечные три части этого пути состоят из непонятных, автоматически генерируемых строк вроде C6VLXKCE.828. Понятно, что не предполагается, что вы будете обращаться к этому каталогу напрямую.
Установки ClickOnce и UAC Как вы, без сомнений, уже знаете, Windows Vista включает средство под названием User Account Control (UAC), которое ограничивает административные привилегии, чтобы сократить масштабы разрушений, которые могут быть вызваны злонамеренными приложениями. Если текущий пользователь попытается выполнить задачу, требующую административных привилегий, появится специальное диалоговое окно UAC, которое запросит у пользователя подтверждения повышения уровня прав, прежде чем продолжить работу. Этот шаг может быть выполнен только при запуске нового процесса. Повышать уровень привилегий работающего процесса невозможно. Проектируя UAC, в Microsoft столкнулись с необходимостью гарантировать, что большинство существующих программ будут работать корректно большую часть времени. Один из допущенных компромиссов касался инсталляционных программ. Поскольку многие программы установки требуют административных привилегий, Windows Vista предлагает пользователю повысить уровень его прав до уровня администратора при запуске программы инсталляции. Windows Vista “обнаруживает” программу инсталляции на основе имени файла, так что файл по имени setup.exe автоматически трактуется как инсталляционная программа, и ее запуск инициирует повышение уровня привилегий. Проблема в том, что не все программы инсталляции требуют такого повышения уровня прав. Установка программ посредством приложений ClickOnce — блестящий тому пример. Однако если вы запустите установку ClickOnce, то получите ненужное предупреждение UAC. Хуже того, если вы работаете под учетной записью, не имеющей административных привилегий, вы будете вынуждены применить удостоверение администратора (ввести имя и пароль администратора), и приложение ClickOnce будет инсталлировано для этой административной учетной записи, а не учетной записи текущего пользователя. В результате оно не появится в вашем меню Start. Возможно несколько решений этой проблемы. Один вариант — предложить пользователям запускать установку двойным щелчком на файле .application (типа ClickOnceText. application), а не использовать приложение setup.exe или страницу publish.htm (которая тоже использует setup.exe). К сожалению, это нарушает развертывание на базе Web, если только вы не создадите собственную инсталляционную страницу, которая будет указывать на файл .application. Такой подход также не будет работать, если у пользователя не инсталлирована, как минимум, исполняющая система .NET 2.0. Без нее расширение .application не будет распознаваться. Другой вариант — переименовать setup.exe во что-нибудь другое (вроде MyApp.exe). К сожалению, этот подход не работает, потому что Vista все равно определит, что исполняемый файл использует внутренний ресурс по имени setup. С помощью редактора ресурсов вы можете вручную изменить эту деталь, но такой подход представляет собой в лучшем случае неуклюжий обходной маневр. Microsoft планирует устранить упомянутые ограничения в следующей сборке Visual Studio.
Обновление приложения ClickOnce Чтобы увидеть, как приложение ClickOnce автоматически само себя обновляет, выполните следующие шаги с инсталляцией из предыдущего примера. 1. Внесите небольшое, но заметное изменение в приложение (например, добавьте кнопку).
Book_Pro_WPF-2.indb 912
19.05.2008 18:11:48
Развертывание ClickOnce
913
2. Перекомпилируйте приложение и заново опубликуйте его в то же самое местоположение. 3. Запустите приложение из меню Start. Приложение обнаружит новую версию и запросит у вас подтверждения на его инсталляцию (рис. 27.7). 4. Как только вы подтвердите обновление, новая версия приложения будет инсталлирована и запущена.
Рис. 27.7. Обнаружение новой версии приложения ClickOnce В следующих разделах вы узнаете, как настроить некоторые дополнительные опции ClickOnce. На заметку! Обновления и загрузки обрабатываются механизмом ClickOnce — dfsvc.exe.
Опции ClickOnce Мастер публикации представляет собой быстрый способ создания комплекта развертывания ClickOnce, но он не позволяет подстроить все возможные опции. Чтобы получить доступ к большему числу настроек ClickOnce, выполните двойной щелчок на узле Properties (Свойства) в Solution Explorer, затем перейдите на вкладку Publish (Публикация). Там вы увидите настройки, показанные на рис. 27.8.
Рис. 27.8. Настройки проекта ClickOnce
Book_Pro_WPF-2.indb 913
19.05.2008 18:11:48
914
Глава 27
Некоторые из этих настроек дублируют детали, которые вы уже видели в мастере. Например, первые два текстовых поля позволяют выбрать местоположение публикации (место, куда будет помещены файлы ClickOnce, как установлено на первом шаге мастера) и местоположение инсталляции (место, из которого пользователь запустит установку, как делается на втором шаге мастера). Настройка Install Mode (Режим инсталляции) позволяет выбрать, должно ли приложение устанавливаться на локальном компьютере или запускаться в режиме “только онлайн”, как было описано выше в настоящей главе. В нижней части окна кнопка Publish Wizard (Мастер публикации) запускает мастер, который вы видели ранее, а кнопка Publish Now (Опубликовать) публикует проект, используя предыдущие установки. В следующих разделах мы обсудим установки, которые вам еще не знакомы.
Версия публикации Раздел Publish Version (Версия публикации) устанавливает версию вашего приложения, которая сохраняется в файле манифеста ClickOnce. Это не то же самое, что версия сборки, которую вы можете установить на вкладке Application (Приложение), хотя можно установить их одинаковыми. Ключевое отличие в том, что версия публикации является критерием, служащим для определения доступности нового обновления. Если пользователь запускает версию 1.5.0.0 приложения, а при этом доступна версия 1.5.0.1, то инфраструктура ClickOnce покажет диалоговое окно обновления, представленное на рис. 27.7. По умолчанию флажок Automatically Increment Revision with Each Publish (Автоматически увеличивать номер редакции с каждой публикацией) отмечен, и в этом случае финальная часть версии публикации (номер редакции) увеличивается на 1 после каждой публикации, так что 1.0.0.0 превращается в 1.0.0.1, затем в 1.0.0.2 и т.д. Если вы хотите опубликовать одну и ту же версию вашего приложения во многих местоположениях, следует снять отметку с этого флажка. Однако имейте в виду, что средство автоматического обновления вступает в действие только тогда, когда обнаруживает более высокий номер версии. Штамп даты развернутых файлов не имеет значения (и не заслуживает доверия). Может показаться, что это ужасно не элегантно — отслеживать отдельные номера версий сборки и публикации. Однако иногда это имеет смысл. Например, при тестировании приложения вы можете пожелать сохранить фиксированный номер версии сборки, чтобы предотвратить попадание к тестировщикам самой последней версии. В этом случае вы можете использовать ту же версию сборки, но оставить автоматически увеличиваемым номер версии публикации. Когда вы будете готовы выпустить официальное обновление, то сможете установить версию сборки и версию публикации одинаковыми. К тому же опубликованное приложение может состоять из многих сборок с разными номерами версий. В этом случае было бы нереально применять номер версии сборки. Вместо этого инфраструктура ClickOnce должна рассматривать единственный номер версии, чтобы определить необходимость обновления.
Обновления Щелкните на кнопке Updates (Обновления), чтобы отобразить диалоговое окно Application Updates (Обновления приложения), показанное на рис. 27.9, в котором можно выбрать стратегию обновлений. На заметку! Кнопка Updates недоступна, если вы создаете приложение “только онлайн”. Такое приложение всегда запускается из его местоположения публикации на Web-сайте или сетевом ресурсе.
Book_Pro_WPF-2.indb 914
19.05.2008 18:11:48
Развертывание ClickOnce
915
Рис. 27.9. Установка опций обновления Сначала вы указываете, должно ли приложение проверять наличие обновлений. Если да, можете выбрать, когда должно происходить обновление. Здесь доступны две опции.
• Before the application starts (Перед стартом приложения). Если использовать эту модель, то инфраструктура ClickOnce проверяет наличие обновлений приложения (на Web-сайте или сетевом ресурсе) при каждом запуске приложения пользователем. Если обновление обнаружено, оно инсталлируется и затем запускается приложение. Эта опция — хороший выбор, если вы гарантировать получение пользователем обновлений немедленно после их появления.
• After the application starts (После старта приложения). Если выбрать эту модель, то инфраструктура ClickOnce проверяет новые обновления после запуска приложения. Если обнаруживается обновленная версия, она инсталлируется при следующем запуске приложения пользователем. Это — рекомендованная опция для большинства приложений, поскольку сокращает время загрузки. Если вы предпочтете выполнять проверки после запуска приложения, то такая проверка осуществляется в фоновом режиме. Вы можете выбрать выполнение проверки при каждом выполнении приложения (по умолчанию так и есть) или же проверять реже, через какой-то интервал времени. Например, вы можете ограничить проверки, указав выполнять их один раз в несколько часов, дней или недель. Вы также можете специфицировать необходимую минимальную версию. Это можно использовать для того, чтобы сделать обновления обязательными. Например, если вы установите версию публикации 1.5.0.1 и минимальную версию 1.5.0.0, а затем опубликуете приложение, то любой пользователь, у которого будет версия старше 1.5.0.0, будет вынужден выполнить обновление, прежде чем сможет запустить приложение (по умолчанию минимальная версия не устанавливается и все обновления необязательны). На заметку! Даже если вы специфицируете минимальную версию и заставите приложение проверять наличие обновлений перед запуском, то пользователь сможет запустить старую версию вашего приложения. Это происходит, когда он находится в оффлайновом режиме — при этом проверка обновлений завершается сбоем, не выдавая ошибок. Единственный способ обойти это ограничение — создать приложение “только онлайн”.
Book_Pro_WPF-2.indb 915
19.05.2008 18:11:48
916
Глава 27
Опции публикации Диалоговое окно Publish Options (Параметры публикации) включает массу разнообразных опций (рис. 27.10).
Рис. 27.10. Разнообразные опции ClickOnce Имена издателя и продукта используются для создания иерархии меню Start. В примере, представленном на рис. 27.11, будет сгенерирован ярлык как StartAcme SoftwareClickOnceTest. Эта информация, наряду с URL поддержки, также появляется в диалоговом окне Add/Remove Programs. Вы также можете использовать диалоговое окно Publish Options для изменения имени инсталляционной страницы в Web-развертывании (по умолчанию — publish.htm), и вы можете выбрать, нужно ли позволять Visual Studio запускать эту страницу автоматически после успешной публикации (предположительно для целей тестирования). Еще две опции предоставляют контроль над тем, как будет работать инсталляция — позволяя указать, что приложение должно запускаться немедленно после успешной инсталляции, и должен ли генерироваться файл autorun.inf, чтобы заставить программу чтения CD-дисков немедленно запускать программу установки после того, как CD-диск вставлен привод.
Резюме В этой главе был проведен краткий экскурс по модели развертывания ClickOnce, которая впервые появилась в .NET 2.0 и остается хорошим выбором для развертывания автономных приложений WPF. Как и с XBAP, ClickOnce влечет за собой некоторые компромиссы — например, вам придется смириться с тем, что некоторыми деталями клиентской конфигурации управлять не удастся. К тому же вам придется привыкнуть к тому факту, что ClickOnce пока не является действительно лучшим способом развертывания приложений — до тех пор, пока эта модель не станет более стабильной, что требует компьютеров, на которых работает Windows Vista или .NET 2.0 Framework. Однако, вероятно, что ClickOnce станет ключевой технологией развертывания в будущем, и ее важность продолжит расти.
Book_Pro_WPF-2.indb 916
19.05.2008 18:11:48
Предметный указатель A ActiveX, 862
B BAML (Binary Application Markup Language), 48
C Cascading Style Sheet (CSS), 343 ClickOnce, 902 инсталляция в Windows Vista, 905
D DirectX, 25; 38 DPI масштабирование в Windows Vista, 33 системная установка, 31 изменение, 33
L LINQ, 35
M Multiple Document Interface (MDI), 231
N .NET Framework, 37
R RGB, 189
S ScRGB, 189 Silverlight, 38
U URI (Uniform Resource Identifier), 195 User Account Control (UAC), 90; 912
V
W Windows Forms, 37; 846 взаимодействие с ActiveX, 862 взаимодействие с Win32, 867 классы, 853 элементы управления, 847 пользовательские, 861 размещение в WPF, 858 размещение элементов управления WPF в Windows Forms, 862 Windows Presentation Foundation (WPF), 25 WPF 3.0, 35 WPF 3.5, 35 анимация, 29 архитектура, 39 аудио, 29 видео, 29 декларативный пользовательский интерфейс, 29 единицы WPF, 30 классы фигур, 362 компоновка, 92 независимость от разрешения, 29 поддержка 3-D, 28 приложения на основе страниц, 29 события, 172 уровень визуализации, 27 фундаментальные классы, 41 Windows Vista Display Driver Model (WDDM), 27 Windows XP Display Driver Model (XPDM), 27
X XAML (Extensible Application Markup Language), 45 Silverlight XAML, 48 WF XAML, 48 WPF XAML, 48 XPS XAML, 48 компиляция, 48; 68 XBAP (XAML Browser Application), 35; 258
Visual Studio, 36
Book_Pro_WPF-2.indb 917
19.05.2008 18:11:49
918
Предметный указатель
А Адаптер дополнения, 888 хоста, 889 Анимация, 29; 689; 786 анимированные кисти, 724 базовая, 692 в коде, 695 декларативная, 704 ключевого кадра, 693; 727 дискретная, 728 сплайновая, 729 линейная интерполяция, 692 множества трансформаций, 723 на основе пути, 693; 730 на основе свойств, 691 на основе таймера, 690 на основе фрейма, 732 одновременная, 710 основанная на свойствах, 689 перекрывающаяся, 709 трансформаций, 720 Архитектура WPF, 39
Б Библиотека quartz.dll, 737 команд, 294 Блики, 395
В Вкладка, 141
Г Гиперссылка, 252 Графика векторная, 34 трехмерная, 760 Группа с общими размерами, 117
Д Данные поставщик данных, 562 привязка данных, 471 асинхронная, 565 шаблон данных, 527 Декоратор, 144 Border, 145 Viewbox, 146
Book_Pro_WPF-2.indb 918
Диспетчер, 869 Документ потоковый, 612 Дополнение (add-in), 880, 887 адаптер дополнения, 888 взаимодействие с хостом, 893 визуальное, 897 жизненный цикл дополнения, 892 Дуга, 405 простая, 406
Ж Журнал навигации, 261
З Затенение, 774 Захват мыши, 183
И Интерфейс, 880 ICommand, 292 IDataErrorInfo, 35 ISupportInitialize, 157 свойства, 154 страничный, 249
К Камера, 769 Канал, 880 Кисть, 379 черепичная, 385 Класс AdornedElementPlaceholder, 523 AmbientLight, 768 Application, 76 события, 80 ApplicationCommands, 294 ArcSegment, 405 BackgroundWorker, 872; 873 BevelBitmapEffect, 392 BezierSegment, 405 BitmapEffectGroup, 392 BlurBitmapEffect, 392 Border, 145 Brush, 188 ButtomChrome, 435 Button, 199 ButtonBase, 199 ButtonChrome, 840 Canvas, 834
19.05.2008 18:11:49
Предметный указатель CheckBox, 199; 201 CheckedListBox, 215 Chrome, 437 CollectionViewSource, 557 Colors, 188 CombinedGeometry, 398 ComboBox, 214; 217 ContainerUIElement3D, 796 ContentControl, 43; 130; 803 ContextMenu, 603 Control, 43; 187; 192; 803 ControllableStoryboardAction, 711 Cursors, 196 DataTrigger, 354 Decorator, 804 DependencyObject, 42 DiffuseMaterial, 766 DirectionalLight, 768 Dispatcher, 869 DispatcherObject, 42; 870 Drawing, 412 DrawingBrush, 379; 414 DrawingContext, 674 методы, 418 DrawingGroup, 413 DrawingImage, 414 DrawingVisual, 414 DropShadowBitmapEffect, 392 EditingCommands, 294 Ellipse, 360; 362 EllipseGeometry, 398 EmbossBitmapEffect, 392 EmissiveMaterial, 766 EventTrigger, 354 FlowDocument свойства, 637 FrameworkElement, 42; 803 FrameworkPropertyMetadata, 154 Freezable, 190 Geometry, 397; 412; 762 Geometry3D, 762 GeometryDrawing, 413; 763 GeometryGroup, 398; 401 GeometryModel3D, 763 GlyphRunDrawing, 413 GridView, 580 GroupStyle, 553 свойства, 553 Hyperlink, 253
Book_Pro_WPF-2.indb 919
919
Image, 840 ImageBrush, 379; 385 ImageDrawing, 413 InputEventArgs, 175 ItemsControl, 43; 213; 495; 570; 803 свойства, 571 Keyboard, 181 методы, 181 Label, 198 Line, 360; 368 LinearGradientBrush, 379; 380 LineGeometry, 398 LineSegment, 405 ListBox, 214 ListBoxChrome, 840 ListView, 578 LogicalTreeHelper методы, 432 MaskedTextBox, 830 MaterialGroup, 766 MatrixTransform, 375 MediaCommands, 294 MediaElement, 743; 840 MediaPlayer, 740 Menu, 600 MeshGeometry3D свойства, 764 MouseButtonEventArgs, 182 MultiDataTrigger, 354 MultiTrigger, 354 свойства, 346 NavigationCommands, 294 NavigationService, 264 события, 265 NavigationWindow, 251 OuterGlowBitmapEffect, 392 Page, 251 свойства, 252 Panel, 43; 94; 803 PasswordBox, 213 Path, 360; 397 PathFigure свойства, 404 PathGeometry, 398 PathSegment, 405 PauseStoryboard, 711 PointLight, 768 PolyBezierSegment, 405 Polygon, 360; 370
19.05.2008 18:11:49
920
Предметный указатель
Polyline, 360; 368; 369 PolyLineSegment, 405 PolyQuadraticBezierSegment, 405 Popup, 208; 285 PrintDialog, 664 ProgressBar, 219 QuadraticBezierSegment, 405 RadialGradientBrush, 379; 381 RadioButton, 199; 201 RangeBase, 217 свойства, 218 Rectangle, 360; 362 RectangleGeometry, 398 RemoveStoryboard, 711 RepeatButton, 200 свойства, 218 ResumeStoryboard, 711 RotateTransform, 375 RoutedCommand, 292 RoutedEventArgs, 166 свойства, 166 RoutedUICommand, 293 ScaleTransform, 375 свойства, 361 SeekStoryboard, 711 Selector, 803 SetStoryboardSpeedRatio, 711 Shape, 43; 360; 840 SkewTransform, 375 SkipStoryboardToFill, 711 SoundPlayer, 737; 738; 740 SoundPlayerAction, 739; 740 SpecularMaterial, 766 SpeechRecognizer, 758 SpotLight, 768 StopStoryboard, 711 StreamGeometry, 398 SystemColors, 188; 336 SystemFonts, 336 SystemParameters, 226; 336 SystemSounds, 740 TaskDialog, 245 TextBlock, 209; 840 TextBox, 211; 212 TimeLine, 701 ToggleButton, 200 ToolTip, 202 ToolTipService, 206 Transform, 763
Book_Pro_WPF-2.indb 920
Transform3D, 763 TransformGroup, 375 TranslateTransform, 375 Trigger, 354 TriggerBase, 354 UIElement, 42; 182 UIElement3D, 795 UserControl, 803; 814; 815 VideoDrawing, 413; 754 Viewbox, 146 Viewport2DVisual3D свойства, 798 Viewport3D, 761 Visual, 42; 417; 762 Visual3D, 762 VisualBrush, 379; 387 Window, 221 свойства, 222 XpsDocumentWriter, 687 компонента, 853 элементов, 378 Кнопка, 199 с градиентной заливкой, 450 Команда, 289 без привязки, 297 библиотека команд, 294 область действия, 307 специальная, 306 с привязкой, 298 Компоновка, 92 контейнеры компоновки, 94 свойства компоновки, 98 текста, 129 Контейнер Border, 129 Expander, 129 ScrollViewer, 129 Viewbox, 129 Контекстное окно указателя (tooltip), 202 Контракт, 881; 886 Кривая Безье, 407
М Маршрутизация событий, 161; 164 Маска непрозрачности, 389 Метка, 198 Многопоточность, 868 Модель дополнений (add-in), 36
19.05.2008 18:11:50
Предметный указатель
Н Навигация по Web-сайтам, 254 по фрагментам, 255 программная, 263 служба навигации, 263 страничная, 248 хронология навигации, 260
О Окно, 221 всплывающее с гиперссылкой, 209 в стиле Vista, 240; 246 диалоговое, 231 модальное, 224 немодальное, 225 непрямоугольное, 233 позиционирование окна, 225 прозрачное, 236 прокручивающееся, 137 самонастраивающееся, 126 со смешанным содержимым, 856
П Перетаскивание, 184 Перо (stylus), 122 Печать, 661 аннотаций, 670 асинхронная, 687 диапазонов страниц, 681 специальная, 673 через XPS, 685 Поставщик XmlDataProvider, 566 данных, 562 Привязка (anchoring), 92 данных, 471 Приложение XBAP, 276; 288 вставка в Web-страницу, 288 развертывание приложения, 901 Примитивы, 28; 359 Прозрачность, 190
Р Размывание, 392 Разрешение независимость от разрешения, 29 Раскадровка, 705
Book_Pro_WPF-2.indb 921
921
Ресурс динамический, 333 неразделяемый, 334 объекта, 315 приложения, 335 разделяемый, 334 сборки, 315 системы, 336 статический, 333 Рефлексия, 880
С Свойства зависимостей, 149 прикрепленные, 58 Селектор, 213 Сетка простая, 110 Событие времени существования, 172 клавиатуры, 172; 176 маршрутизированное, 149; 161 мыши, 172 навигации, 264 пера, 172 поднимающееся, 167 прикрепляемое, 169 прямое, 181 туннельное, 170 Стиль, 29; 343 наследование стилей, 352 создание на основе другого стиля, 351 Стыковка (docking), 92
Т Текст выделение, 211 компоновка, 129 Текстовое поле маскируемое, 824 Текстура, 782 Тень, 395 Технология Silverlight, 38 Трансформация, 375; 786 вращения, 788 фигур, 376 Трехмерная графика, 760 Триггер, 343; 353 простой, 354 события, 356; 705
19.05.2008 18:11:50
922
Предметный указатель
У Утилита locbaml, 328 msbuild, 324 Reflector, 317
Ф Фигура, 359 Line, 368 Polygon, 370 Polyline, 368 Фокус, 179
Х Хост, 890
Ц Цвет, 188
Ш Шаблон, 29; 343 Шаровой манипулятор (trackball), 791 Шрифт, 192 встраивание, 195 замена, 194
Э Элемент управления, 43; 130; 869 CheckBox, 201 ComboBox, 213; 217; 573 Expander, 141
Book_Pro_WPF-2.indb 922
GroupBox, 139 ListBox, 213; 576 ListView, 569 PasswordBox, 210; 213 Popup, 208; 285 ProgressBar, 219 RadioButton, 201 RichTextBox, 210 ScrollViewer, 136 Slider, 217 StatusBar, 609 TabItem, 139 TextBlock, 209 TextBox, 210; 212 ToolBar, 605 ToolBarTray, 608 TreeView, 569; 592 пользовательский, 802 расширение существующего элемента управления, 824 содержимым, 130
Я Язык BAML, 48 XAML, 45 Silverlight XAML, 48 WF XAML, 48 WPF XAML, 48 XPS XAML, 48 компиляция, 48; 68
19.05.2008 18:11:50
Научно-популярное издание Мэтью Мак-Дональд
WPF: Windows Presentation Foundation в .NET 3.5 с примерами на C# 2008 для профессионалов, 2-е издание Верстка Т.Н. Артеменко Художественный редактор В.Г. Павлютин
ООО “И.Д. ВИЛЬЯМС” 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Подписано в печать 25.05.2008. Формат 70×100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 74,82. Уч.-изд. л. 67,3. Тираж 1500 экз. Заказ № 0000.
Отпечатано по технологии CtP в ОАО “Печатный двор” им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15.
VD_Pro-WPF2.indd 923
20.05.2008 17:23:04
-*/2 ·³¢ ¥ª¨ ¨¦¥¥³§¨¦©¦ $ £·§¨¦¬©© ¦¥£¦ ¾Æ¿½Ì¨¸ÊÊÎÄÃ
5555)++)!,1/3"+)1()-'#.,
[\GSNY9ITTZJP
555!/0%11#.,
XXXXJMMJBNTQVCMJTIJOHDPN
*4#/
REKLAMA_Pro-WPF2.indd 924
£ÆÁ¼¹Ç½ÆǼÇÁÀÖÃÊȾÉËÇ» »ÇºÄ¹ÊËÁ˾ÎÆÇÄǼÁÂ/&5 Èɾ½Ê˹»ÄؾËÊǺÇÂÌоºÆǾ ÁÊÈɹ»ÇÐÆǾÈÇÊǺÁ¾ ½ÄØɹÀɹºÇËÐÁÃÇ»/&5 ÈÉÁÄÇ¿¾ÆÁ ÁÊÈÇÄÕÀÌ×ÒÁÎ ÆÇ»Ì×»¾ÉÊÁ×"41/&5 ÁÈɾ½ÄÇ¿¾ÆÆÌ×.JDSPTPGU ˾ÎÆÇÄǼÁ×ɹºÇËÔʽ¹ÆÆÔÅÁ ÈǽƹÀ»¹ÆÁ¾Å-*/2 ÃÇËÇÉ¹Ø Ø»ÄؾËÊØ»ÊËÉǾÆÆÇ» ØÀÔÃ$¨Ç½ÉǺÆÇ É¹ÊÊŹËÉÁ»¹×ËÊػʾ»ÇÈÉÇÊÔ Ê»ØÀ¹ÆÆÔ¾Ê-*/2 ƹÐÁƹØÊ ÇºÓ¾ÃËÆÇÂÅǽ¾ÄÁ ÇȾɹÏÁ Á"1*ÁÆ˾É;ÂÊÇ»-*/2UP 0CKFDUT -*/2UP9.- -*/2 UP%BUB4FU -*/2UP42-Á -*/2UP&OUJUJFT ÁÀ¹Ã¹ÆÐÁ»¹Ø ɹÀɾѾÆÁ¾ÅÃÇÆÍÄÁÃËÇ» ȹɹÄľÄÕÆǼǽÇÊËÌȹÁ ɹºÇ˾ÊÈɾ½Ê˹»Ä¾ÆÁØÅÁº¹À ½¹ÆÆÔÎ £ÆÁ¼¹É¹ÊÊÐÁ˹ƹƹ ÈÉǼɹÅÅÁÊËǻɹÀÆÇ û¹ÄÁÍÁùÏÁÁ ¹Ë¹Ã¿¾ ºÌ½¾ËÈÇľÀƹ½ÄØ ÊË̽¾ÆËÇ»ÁÈɾÈǽ¹»¹Ë¾Ä¾Â ½ÁÊÏÁÈÄÁÆ Ê»ØÀ¹ÆÆÔÎ ÊÈÉǼɹÅÅÁÉÇ»¹ÆÁ¾ÅÁ ɹÀɹºÇËÃǽÄØ/&5
ºÇÈƼ¸¾½
20.05.2008 16:00:49
.*$3040'5"41/&5 ©§¨ ¤¨¤ ¥$ £·§¨¦¬©© ¦¥£¦ ½À¿¼¸ÅÀ½ ¤ÕÊÔÖ¤¸ÂÆŸÃÔ¼ £ÆÁ¼¹ÁÀ»¾ÊËÆÔÎ ¤¸ÈÀÆ°ÇËÐʸ ÊȾÏÁ¹ÄÁÊËÇ»»ÇºÄ¹ÊËÁ
XXXXJMMJBNTQVCMJTIJOHDPN
*4#/
REKLAMA_Pro-WPF2.indd 925
˾ÎÆÇÄǼÁÂ/&5Èɾ½Ê˹»ÄØ¾Ë ÊǺÇÂÌоºÆǾÁÊÈɹ»ÇÐÆǾ ÈÇÊǺÁ¾½ÄØɹÀɹºÇËÐÁÃÇ» /&5ÈÉÁÄÇ¿¾ÆÁ ÁÊÈÇÄÕÀÌ×ÒÁÎÆÇ»Ì× »¾ÉÊÁ×"41/&5 Ä̺ÁƹÁÀÄÇ¿¾ÆÁØ Å¹Ë¾ÉÁ¹Ä¹Èɾ»É¹Ò¹¾Ë ÖËÌÃÆÁ¼Ì»Æ¾À¹Å¾ÆÁÅÔ ÁÊËÇÐÆÁÃÁÆÍÇÉŹÏÁÁ½ÄØ É¹ÀɹºÇËÐÁÃÇ»¨Ç½ÉǺÆÇ É¹ÊÊŹËÉÁ»¹×ËÊػʾ»ÇÈÉÇÊÔ Ê»ØÀ¹ÆÆÔ¾Ê"41/&5 ƹÐÁƹØÊǺӾÃËÆÇÂÅǽ¾ÄÁ ÁÀ¹Ã¹ÆÐÁ»¹Ø»À¹ÁÅǽ¾ÂÊË»Á¾Å Ê9.-ÁɹÀÄÁÐÆÔÅÁ ÈÇÊ˹»ÒÁùÅÁ½¹ÆÆÔΪɾ½Á ÆÇ»ÔÎ˾ŠÊȾÏÁÍÁоÊÃÁÎ ½ÄØ"41/&5 ÇÊÇºÇ Êľ½Ì¾ËÇËžËÁËÕ-*/2 "KBY Á"41/&5"+"9 ¹Ë¹Ã¿¾ ɹÀ»Á»¹×ÒÌ×ÊØ˾ÎÆÇÄǼÁ× 4JMWFSMJHIUÇË.JDSPTPGU £ÆÁ¼¹É¹ÊÊÐÁ˹ƹƹ ÈÉǼɹÅÅÁÊËǻɹÀÆÇ û¹ÄÁÍÁùÏÁÁ ¹Ë¹Ã¿¾ ºÌ½¾ËÈÇľÀƹ½ÄØ ÊË̽¾ÆËÇ»ÁÈɾÈǽ¹»¹Ë¾Ä¾Â ½ÁÊÏÁÈÄÁÆ Ê»ØÀ¹ÆÆÔÎ ÊÈÉǼɹÅÅÁÉÇ»¹ÆÁ¾Å ÁɹÀɹºÇËÃǽÄØ/&5 ºÇÈƼ¸¾½
20.05.2008 16:00:51
¦²¢ª¥¦¦¨ ¥ª ¨¦¥¥³¡ ¥£ §¨¦¢ª ¨¦¥ ©§¨ ¤¨¤ §¨ £¦¥ ¡ ª¨ª´ ¥
ȸ¼ÀËÏ ¨Æ¹½Èʤ¸ÂÉÀÄÏË ¤¸ÁÂëµÅ»Ã À¼È
XXXXJMMJBNTQVCMJTIJOHDPN
*4#/
REKLAMA_Pro-WPF2.indd 926
£ÆÁ¼¹Èɾ½Ê˹»ÄؾËÊǺÇ ÆǻǾÁÀ½¹ÆÁ¾º¾ÊËʾÄľɹ ɹ½ÁÌйÈÇǺӾÃËÆÇ ÇÉÁ¾ÆËÁÉÇ»¹ÆÆÇÅ̹ƹÄÁÀÌ ÁÈÉǾÃËÁÉÇ»¹ÆÁ×»ËÇÉÔ ÇÈÁÊÔ»¹×ËǺӾÃËÆԾžËÇ½Ô É¾Ñ¾ÆÁØÊÄÇ¿ÆÔÎÈÉǺľŠʻØÀ¹ÆÆÔ¾ÊɹÀɹºÇËÃÇ ÊÁÊ˾ÅÁÈÉǼɹÅÅÆÇ¼Ç Çº¾ÊȾоÆÁØ¡ÊÈÇÄÕÀÌØ ÅÆǼÇÐÁÊľÆÆÔ¾ÈÉÁžÉÔ ÇÆÁÁÄÄ×ÊËÉÁÉÌ×ËÇÊÆÇ»ÆÔ¾ ÃÇÆϾÈÏÁÁǺӾÃËÆÇ ÇÉÁ¾ÆËÁÉÇ»¹ÆÆǼÇÈǽÎǽ¹ ƹÈÉÁžɾɹÀɹºÇËÃÁ ÊÁÊ˾ÅÌÈɹ»Ä¾ÆÁØ ÊºÇɹ ½¹ÆÆÔÎÁÁÊÃÌÊÊË»¾ÆÆÇ¼Ç ÁÆ˾ÄľÃ˹°Á˹˾ÄÁƹ½ÌË »ÃÆÁ¼¾ÈɹÃËÁоÊÃÁ¾ÊÇ»¾ËÔ Ã¹Ê¹×ÒÁ¾ÊØ»¹¿ÆÔλÇÈÉÇÊÇ» ¹Æ¹ÄÁÀ¹ ÈÉǾÃËÁÉÇ»¹ÆÁØ É¾¹ÄÁÀ¹ÏÁÁÁÇÈËÁŹÄÕÆÇ¼Ç ÌÈɹ»Ä¾ÆÁØÈÉǾÃ˹ÅÁ £ÆÁ¼¹ºÌ½¾ËÈÇľÀƹ ÊÁÊ˾ÅÆÔŹƹÄÁËÁùÅÁ ¹ÉÎÁ˾ÃËÇɹŠÈÉǼɹÅÅÁÊ˹ŠÈɾÈǽ¹»¹Ë¾ÄØÅÁÊË̽¾Æ˹Š»ÔÊÑÁÎÌоºÆÔÎÀ¹»¾½¾ÆÁ ¹Ë¹Ã¿¾»Ê¾ÅÊȾÏÁ¹ÄÁÊ˹ŠÈÇÁÆÍÇÉŹÏÁÇÆÆÔŠ˾ÎÆÇÄǼÁØÅ
ºÇÈƼ¸¾½
20.05.2008 16:00:53
§¦ ©¢¦·¦§ª ¤ ® · ¥"41/&5£· §¨¦¬©© ¦¥£¦ ¨«¢¦¦©ª¦¨¨¦ª¯ ¢§¦4&0 ¢ÈÀÉÊÀ¸Å¸ÈÀ £ÆÁ¼¹ÈÇÊ»ØҾƹ˾ÎÆÇÄǼÁØÅ ¾½ÁÄÀ©ÀÈƺÀÏ ÊÇÀ½¹ÆÁØÁǺÊÄÌ¿Á»¹ÆÁØ
X X XEJBMFLUJLBDPN
*4#/
REKLAMA_Pro-WPF2.indd 927
8FCʹÂËÇ»ÊÈÇÅÇÒÕ×"41 /&5 ÃÇËÇÉÔ¾ÇÈËÁÅÁÀÁÉÇ»¹ÆÔ ½ÄØÈÇÁÊÃÇ»ÔΞιÆÁÀÅÇ» ¨Ç½ÉǺÆÇɹÊÊŹËÉÁ»¹×ËÊØ Ë¹ÃÁ¾»ÇÈÉÇÊÔ Ã¹ÃǺľ¼Ð¾ÆÁ¾ Áƽ¾ÃʹÏÁÁʹÂ˹ Êɾ½ÊË»¹ ½ÄØÈÉǽ»Á¿¾ÆÁØʹÂ˹ ÉÇÄÕ¹ÉÎÁ˾ÃËÌÉÔʹÂ˹ »ÈÇÁÊÃÇ»ÇÂÇÈËÁÅÁÀ¹ÏÁÁ ÊÈÇÊǺԽÇÊËÁ¿¾ÆÁØ ÎÇÉÇÑÁÎÈÇÁÊÃÇ»ÔÎɹƼǻ ÁÅÆǼÁ¾½É̼Á¾°Á˹˾ÄÁ ÇÀƹÃÇÅØËÊØÊžËǽ¹ÅÁ Ⱦɾ¹½É¾Ê¹ÏÁÁ 8FCùƹĹÅÁ ÁÊÇÏÁ¹ÄÕÆÔÅÁÀ¹ÃĹ½Ã¹ÅÁ ÈÉÁŹÆÁ»¹ÆÁ¾ÅÊÊÔÄÇà ÇϾÆÃÇÂÇÈËÁŹÄÕÆÇÊËÁ ʹÂ˹ÊËÇÐÃÁÀɾÆÁØ ÈÇÁÊÃÇ»ÔΞιÆÁÀÅÇ»£ÉÇž ËÇ¼Ç ÈÉÁ»Ç½ÁËÊØÈÉÁÅ¾É ÈÇÊËÉǾÆÁØÖľÃËÉÇÆÆÇ¼Ç Å¹¼¹ÀÁƹÊÌоËÇŹÊȾÃËÇ» ÈÇÁÊÃÇ»ÇÂÇÈËÁÅÁÀ¹ÏÁÁ £ÆÁ¼¹É¹ÊÊÐÁ˹ƹƹ ÈÉǼɹÅÅÁÊËÇ»Ážƾ½¿¾ÉÇ» ÈÇŹÉþËÁƼÌɹÀÆÇ û¹ÄÁÍÁùÏÁÁ ¹Ë¹Ã¿¾ ºÌ½¾ËÈÇľÀƹ½ÄØÊË̽¾ÆËÇ» ÁÈɾÈǽ¹»¹Ë¾Ä¾Â½ÁÊÏÁÈÄÁÆ Ê»ØÀ¹ÆÆÔÎÊɹÀɹºÇËÃÇ ÁºÁÀƾʹƹÄÁËÁÃǽÄØ8FC ºÇÈƼ¸¾½
20.05.2008 16:00:53
$ §£ª¬¦¨¤/&5 £·§¨¦¬©© ¦¥£¦ ¢ÈÀÉÊÀ¸Å¥½Á»½Ã ÀÃà ºÔ½Å ¾½ÁÃÀÅÅ ¢¸ÈÃÀ«ÆÊÉÆÅ ¤ÆÈ»¸Å©ÂÀÅŽÈ
XXXEJBMFLUJLBDPN
*4#/
REKLAMA_Pro-WPF2.indd 928
£ÆÁ¼¹ÁÀ»¾ÊËÆÔÎÊȾÏÁ¹ÄÁÊËÇ» »ÇºÄ¹ÊËÁɹÀɹºÇËÃÁ ÈÉÁÄÇ¿¾ÆÁÂÊÁÊÈÇÄÕÀÇ»¹ÆÁ¾Å /&5'SBNFXPSLÈÇÊ»ØҾƹ ÈÉǼɹÅÅÁÉÇ»¹ÆÁ×ƹØÀÔþ $»Êɾ½¹Î/&5'SBNFXPSL Á/&5'SBNFXPSL£ÆÁ¼Ì ÇËÄÁй¾ËÈÉÇÊËÇÂÁ½ÇÊËÌÈÆÔ ÊËÁÄÕÁÀÄÇ¿¾ÆÁØ ÁÀǺÁÄÁ¾ ÈÉÁžÉÇ»ÁÅÆÇ¿¾ÊË»Ç É¾ÃÇžƽ¹ÏÁÂÈÇƹÈÁʹÆÁ× »ÔÊÇÃÇùоÊË»¾ÆÆÔÎÈÉǼɹÅÅ ¨Ç½ÉǺÆÇɹÊÊŹËÉÁ»¹×ËÊØ Ë¹ÃÁ¾»ÇÈÉÇÊÔ Ã¹ÃÇÊÆÇ»Ô ØÀÔùÈÉǼɹÅÅÁÉÇ»¹ÆÁØ $ Çɼ¹ÆÁÀ¹ÏÁØÊɾ½Ô/&5 ɹºÇ˹ʽ¹ÆÆÔÅÁ ƹÈÁʹÆÁ¾ 8JOEPXTÁ8FCÈÉÁÄÇ¿¾ÆÁ »À¹ÁÅǽ¾ÂÊË»Á¾Ð¾É¾ÀʾËÕ ÊÇÀ½¹ÆÁ¾8FCÊÄÌ¿ºÁ ÅÆǼǾ½É̼Ǿ¦¾Å¹ÄǾ »ÆÁŹÆÁ¾Ì½¾Ä¾ÆÇÈÉǺľŹŠº¾ÀÇȹÊÆÇÊËÁÁÊÇÈÉǻǿ½¾ÆÁØ Ãǽ¹¨ÉÁĹ¼¹¾ÅÔÂÃÃÆÁ¼¾ ÃÇÅȹÃ˽ÁÊÃÊǽ¾É¿ÁË ÁÊÎǽÆÔ¾ÃǽԻʾÎÈÉÁžÉÇ» ÐËÇÊÌÒ¾ÊË»¾ÆÆÇÌÈÉÇÊËÁË ÇʻǾÆÁ¾Å¹Ë¾ÉÁ¹Ä¹ £ÆÁ¼¹É¹ÊÊÐÁ˹ƹƹ ÈÉǼɹÅÅÁÊËǻɹÀÆÇ û¹ÄÁÍÁùÏÁÁ ¹Ë¹Ã¿¾ ºÌ½¾ËÈÇľÀƹ½ÄØÊË̽¾ÆËÇ» ÁÈɾÈǽ¹»¹Ë¾Ä¾Â
ºÇÈƼ¸¾½
20.05.2008 16:00:54